CAS非锁实现单例的一个缺陷

JAVA herman 668浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

最近又是一年新春的面试季,有人说这是金三银四。但是说到面试,并发和锁肯定是少不了的。关于并发可以访问我的这篇文章:极客时间《Java并发编程实战》购买返现24,今天我们要说的是,无锁实现单例模式,以及这种 CAS 实现的单例的缺点。

传统的 7 种单例模式大致如下:

单例模式原理

它们都是用锁来实现。但是如果在面试过程中面试官问你如何使用非锁来实现一个单例呢?

答案就是下图这种实现。

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE
            = new AtomicReference<Singleton>();
    private Singleton() {
        System.out.println("我被初始化了");
        CasSingletonTest.objectcount.getAndIncrement();
    }
    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }
            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

这是网上一位大牛的实现,他的这种非锁 CAS 实现的单例,挺好的。但是平时可能没有人使用,比用锁稍微复杂了一点,这也是为什么没有被列入单例模式的 7 大写法之中了。我在他的基础上,也就是他的构造方法里添加了两行代码。

System.out.println("我被初始化了");
CasSingletonTest.objectcount.getAndIncrement();

我主要是想看看它到底是实例化了几次。加上这两行代码,可以方便我观察控制台,和统计实例化的总次数。

然后,我的测试代码如下:

package com.xttblog.canal.test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * CasSingletonTest
 * @author www.xttblog.com
 * @date 2019/2/27 下午2:39
 */
public class CasSingletonTest {
    public static AtomicInteger objectcount = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        final CountDownLatch begin = new CountDownLatch(1);
        final CountDownLatch last = new CountDownLatch(1000);
        for(int i=0;i<1000;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        begin.await();
                        System.out.println(Thread.currentThread().getName()+":begin...");
                        Singleton sba = Singleton.getInstance();
                        System.out.println(Thread.currentThread().getName()+":OK");
                        last.countDown();
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }).start();
        }
        begin.countDown();
        last.await();
        System.out.println("new objects: "+objectcount.get());
    }
}

关于 CountDownLatch 有不会的,可以看我的《CountDownLatch 压测教程》一文。

我这里主要是想压测一下,非锁 CAS 单例模式是否会创建多次对象。

运行上面的 main 方法,我截图了一下最终结果。

无锁实现单例

结论:CAS 以原子方式更新内存中相应的值,从而保证了多线程环境下共享变量更新操作的同步。的确,这种方式可以保证每次调用getInstance() 方法得到的一定是同一个实例。因此,从功能实现的角度来看,这种做法达到了预期的目的。但是,经过分析和测试,却发现这种方式有一些预期之外的弊病:可能会创建不止一个对象。

CAS 本身的操作的确是原子方式,但是包装 CAS 指令的方法并非是全程同步的,当然,在包含 CAS 指令的方法开始调用之前,参数计算过程中更不是互斥执行的!当一个线程测试 instance.get() == null 得到 true 之后,往下它就一定会调用 new Singleton()。因为,这并不是 CAS 方法的一部分,而是它的参数。在调用一个方法之前,需要先将其参数压入栈,当然,需要先计算参数表达式,因此,产生如上结果也就不难预料了。

CAS 与锁的区别在于,它是非阻塞的,也就是说,它不会去等待一个条件,而是一定会去执行,结果要么成功,要么失败。它的操作时间是可预期的。如果我们的目的是一定要成功执行 CAS,那就需要不断循环执行直至成功,同时,建立在成功预期之上大量的准备工作是值得的,但是,如果我们不希望操作一定成功,那为成功操作而做的准备工作就浪费掉了。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号1:xmtxtt(5000人已满),微信号2:codedq(5000人已满),微信号3:xttblog(超2800)。备注:“xttblog”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » CAS非锁实现单例的一个缺陷