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

  常说的单例有懒汉跟饿汉两种写法。饿汉由于类加载的时候就创建了对象,因此不存在并发拿到不同对象的问题,但会由于开始就加载了对象,可能会造成一些启动缓慢等性能问题;而懒汉虽然避免了这个问题,但普通的写法会在高并发环境下创建多个对象,单纯加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的原子性呢,执行到一半,异常了,,,程序员自己考虑异常处理,执行到一半,断电了,,,,没得办,来电重启呗,相关业务数据处理,程序员自己想办法搞定(一般设计上应该会有相关处理)。

原文地址:https://www.cnblogs.com/nevermorewang/p/10114870.html

时间: 2024-09-29 08:46:26

从单例的双重检查锁想到的的相关文章

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

之前我曾经写过一篇文章<单例模式有8种写法,你知道么?>,其中提到了一种实现单例的方法-双重检查锁,最近在读并发方面的书籍,发现双重检查锁使用不当也并非绝对安全,在这里分享一下. 单例回顾 首先我们回顾一下最简单的单例模式是怎样的? /** *单例模式一:懒汉式(线程安全) */ public class Singleton1 { private static Singleton1 singleton1; private Singleton1() { } public static Singl

Java并发笔记——单例与双重检测

单例模式可以使得一个类只有一个对象实例,能够减少频繁创建对象的时间和空间开销.单线程模式下一个典型的单例模式代码如下: ① 1 class Singleton{ 2 private static Singleton singleton; 3 private Singleton(){} 4 5 public static Singleton getInstance(){ 6 if(singleton == null){ 7 singleton = new Singleton(); //1 8 }

单例模式中用volatile和synchronized来满足双重检查锁机制

背景:我们在实现单例模式的时候往往会忽略掉多线程的情况,就是写的代码在单线程的情况下是没问题的,但是一碰到多个线程的时候,由于代码没写好,就会引发很多问题,而且这些问题都是很隐蔽和很难排查的. 例子1:没有volatile修饰的uniqueInstance public class Singleton { private static Singleton uniqueInstance; private Singleton(){ } public static Singleton getInsta

关于并发场景下,通过双重检查锁实现延迟初始化的优化问题隐患的记录

首先,这个问题是从<阿里巴巴Java开发手册>的1.6.12(P31)上面看到的,里面有这样一句话,并列出一种反例代码(以下为仿写,并非与书上一致): 在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患,推荐解决方案中较为简单的一种(适用于JDK5及以上的版本),即目标属性声明为volatile型. 1 public class Singleton { 2 private static Singleton instance=null; 3

[杂谈]C++的双重检查锁并不安全

原文地址 http://www.cnblogs.com/hebaichuanyeah/p/6298513.html 一个典型的单例模式构建对象的双重检查锁如下: static Singleton * getSingleObject() { if(singleObject==NULL) { lock(); if(singleObject==NULL) { singleObject = new Singleton(); } unlock(); } return singleObject; } 该代码

线程安全的单例模式及双重检查锁—个人理解

在web应用中服务器面临的是大量的访问请求,免不了多线程程序,但是有时候,我们希望在多线程应用中的某一个类只能新建一个对象的时候,就会遇到问题. 首先考虑单线程,如果要求只能新建一个对象,那么构造函数我们要设为private.简单的想法: class singleton{ private singleton(){ //..... } private static singleton instance; public static singleton getinstance(){ if(insta

C++的双重检查锁并不安全(转)

一个典型的单例模式构建对象的双重检查锁如下: 1 static Singleton * getSingleObject() 2 { 3 if(singleObject==NULL) 4 { 5 lock(); 6 if(singleObject==NULL) 7 { 8 singleObject = new Singleton(); 9 } 10 unlock(); 11 } 12 return singleObject; 13 } 该代码的逻辑是:getSingleObject()函数获得对象

单例模式中 的 双重检查锁 概念与用法

public class Singleton { //私有的 静态的 本类属性 private volatile static Singleton _instance; //私有化构造器 private Singleton() {} /* * 1st version: creates multiple instance if two thread access * this method simultaneouslyX */ public static Singleton getInstance

为什么双重检查锁模式需要 volatile ?

双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量.这个模式还可以用来创建单例.下面来看一个 Spring 中双重检查锁定的例子. 这个例子中需要将配置文件加载到 handlerMappings中,由于读取资源比较耗时,所以将动作放到真正需要 handlerMappings 的时候.我们可以看到 handlerMappings 前面使用了volatile .有没有想过为什么一定需要 volatile?虽然之前了解了双重检查锁定模式的原理