从单例的双重检查锁想到的

Posted nevermorewang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从单例的双重检查锁想到的相关的知识,希望对你有一定的参考价值。

  常说的单例有懒汉跟饿汉两种写法。饿汉由于类加载的时候就创建了对象,因此不存在并发拿到不同对象的问题,但会由于开始就加载了对象,可能会造成一些启动缓慢等性能问题;而懒汉虽然避免了这个问题,但普通的写法会在高并发环境下创建多个对象,单纯加synchronize又会明显降低并发效率,较好的两种写法是静态内部类跟双重检查锁两种。
  双重检查锁这个,大家都很熟悉了,上代码:
public class SingleTest {
    private static SingleTest singleTest;
    //获取单例的方法
    public static SingleTest getInstance() {
        if(singleTest == null){
            synchronized (SingleTest.class){
                if(singleTest == null){
                    singleTest = new SingleTest();
                }
            }
        }
        return singleTest;
    }
}
  这个完美了么,并没有。实际是,在多线程环境下以上写法有时会报错。原因是java的线程内重排序导致的,正确做法应该在singleTest上添加volatile进行修饰。
  我们知道volatile可以保证可见性,synchronized可以保证可见性一致性跟原子性。既然synchronized比volatile还要强大,为什么还要volatile进行修饰呢?
  先来分析错误原因:AB两个线程同时获取单例,A先进入同步块,进行new操作,但这个new实际要分为3步进行:
    memory = allocate(); // 1、分配对象内存空间
    ctorInstance(memory); // 2、初始化对象
    instance = memory; // 3、设置instance指向刚分配的内存地址
  其中2,3是可以重排序的。如果发生了重排序,这时候虽然synchronized并没有执行结束,但如果已经执行了3,则B线程在执行第5行时,可能会读到singleTest的值已经不是null,而此时并没有执行2,如果B线程直接将数据返回出去使用,是会有问题的。
  注意:这里是可能会有这种情况,并不一定每次都会发生。《java并发编程的艺术》书中有对此部分的解释。记得在第二次看的时候发现了一个疑问,并且跟一群人讨论了好久:synchronized是基于monitor机制的,在monitorexit的时候回强制刷新线程内存到主存,这也是synchronized可见性的保证;但这个地方,A线程还没有执行monitorexit,为什么B线程就读到了还没完全赋值的singleTest呢,这里是不是有什么问题,说好的原子性呢,不是说在synchronized结束前其它线程无法访问么?
  先解释问题:这个synchronized的可见性,上面的理解没啥问题。但jvm发展至今,实际已经针对现在的硬件做了很大程度优化,基本上很大程度的保障了工作内存跟主内存的及时同步,相当于默认使用了一个不太靠谱的volatile。也就是有monitorexit当然会刷新会主存,但没有到monitorexit的时候,其实也是会刷会主存的,这就解释了这里多线程的时候会出问题的原因。解决的话,加上volatile,禁止了重排序,等读到有值的时候已经初始化完了,当然也就不会有问题了。
  最后再来看一下:synchronized的这个原子性,网上的解释是这么说的:
  【众所周知,原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分”;由不可分性可知,原子性是拒绝多线程操作的(只有分解为多步操作,多个线程才能对其操作:就像一个盒子里有多个兵乓球,多个人能够从盒子里拿乒乓球;如果盒子只有一个兵乓球,一个人拿的话,其他人就拿不到了;这就是原子性,乒乓球就具有原子性,人就相当于线程)
   简而言之——不被线程调度器中断的操作,如:赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性。
  原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作!】
  这里强调的是同一时间段只有一个线程在执行,
  再回想一下数据库事务的原子性,进行两个操作,如果一个失败了那么另一个也会回滚;跟synchronized的原子性说的不太一样吧,synchronized的原子性只是同一时间只让一个线程访问而已,如果里边修改了某个公共变量,由于jvm不定时刷新导致的可见性问题,在synchronized还没有执行完的时候,其它线程也是大概率可以看到这个改动的。这是jvm决定的,synchronized的原子性“管不到这里”。(jvm怎么决定的,有啥依据~~这个,水平有限,没搞清楚,就只能先赖给jvm了)
  再说一点,数据库的原子性我们是很熟悉的,可是它是执行结束了,其它事务才会看到的么?显然不是,有事务的隔离级别么,如果把隔离级别降到最低(读未提交),A事务一修改,还没提交,B事务就能看见啦。跟这里的原子性还有可见性对比一下,就是java的这个可见性相当于数据库的最低级别了。说到数据库事务的原子性,真的一定能保证要么全部执行,要么回滚么?这个当然不能,假设一个事务非常庞大,执行了一半,断电了~~数据库在设计上当然会考虑这个,重启数据库后会根据日志来继续执行或者回滚,但如果日志跟数据库的数据对不上怎么办,数据库自己搞不定了,这时候当然就需要专业dba出手了。那java的原子性呢,执行到一半,异常了,,,程序员自己考虑异常处理,执行到一半,断电了,,,,没得办,来电重启呗,相关业务数据处理,程序员自己想办法搞定(一般设计上应该会有相关处理)。
 

以上是关于从单例的双重检查锁想到的的主要内容,如果未能解决你的问题,请参考以下文章

单例陷阱——双重检查锁中的指令重排问题

设计模式一:饱汉式单例(双重锁)

双重检查锁实现单例(java)

Java单例模式中双重检查锁的问题

双重检查锁实现单例模式的线程安全问题

性能比较好的单例写法