记双重锁验证与volatile关键字的理解

目录

前言:

一、从单例说起 

二、保证可见性

题外:


前言:

并发编程中有两个非常重要的概念“一致性”、“可见性”。这两个是独立的不同的两个概念,不能混为一谈。而并发因为不可控性,本质其实就是想尽各种办法解决这两个东西。

一、从单例说起 

说到并发必然离不开单例模式这一经典设计模式。

首先我们来看一个经典的双重检查锁的单例模式:

class Singleton{
    private static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

这种双重检查锁目的有两个:

    1.保证只有第一次创建instance的时候才进入锁块。如果锁方法的话则每次获取实例对象时都排队执行,降低了并发速度。

    2.每次进入锁后不是直接执行new操作,而是再次判断。因为此时可能线程1进入if,并进入锁代码块,但没执行new操作。此时线程2也进入if,并等待锁释放。如果此时锁代码块中没有判断,则会导致线程1执行new操作并返回示例。线程2进入锁并同样执行new操作,此时就new了两个对象。而锁里加了判断则会让进入锁的线程再次判断,不会直接new对象。保证了单例唯一。

但这样做还是有问题。接下来就要说说java内存模型的问题。

cpu在执行java程序时,在单线程情况下是按照代码顺序执行,比如:

int a =10;          //步骤1
String b = "20";    //步骤2
int c = 30;         //步骤3
int d = a++;      //步骤4

cpu会按照1、2、3、4的步骤顺序执行。但当多线程并发时,java会根据cpu性能进行指令重排序。真实的执行顺序可能是1234、1324、2314...等等,但有一个原则,那就是最后的执行结果一定和顺序执行是一致的。也就是说d依赖a的结果,那么步骤4一定是在步骤1执行完后执行的。

肯定有人会说,虽然指令重排序了,但似乎也不影响并发问题?

但请注意了,并发过程中,线程可能会在“任何”地方停止。这里的“任何”指的是每个原子性操作的地方。

那么上述代码中哪些是是原子性的?只有1、3是。java中“只有”基本类型的直接赋值操作是原子性的。

例如:步骤1和步骤3是原子性的。步骤4不是原子性的,因为这一步需要1:读取a数据:、2:赋值给d、3:进行加一操作

每个线程有自己的内存区域,而读取数据时不一定是直接从内存中读取共享数据,而是从缓存行(主要针对多核cpu有多个独立的缓存行)读取。

volatile关键字就是保证,每个线程对volatile关键字修饰的变量都更新到内存。其他线程要用到该变量时会锁住缓存行,直接从内存读取。这样保证了变量对各个线程的可见性。

回到开头的单例模式。

new singleton()操作在Java中不是一个原子性的操作。这个动作大概分了3步

1.申请一个内存区域(空白内存)

2.调用构造方法等对singleton进行初始化(写内存)

3.将变量指针指向该对象内存区域(变量声明)

那么问题来了。虽然双重验证锁保证了进入锁时进行判断。但如果线程1在进行new操作时,由于指令重排序先执行了1、3而没有执行初始化的步骤。此时线程2进来判断instance非null直接返回了。那么线程2获得的是个不完整的对象,使用时就会报错。

如何优化?

二、保证可见性

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {
 
    }
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

在instance变量上加上volatile标志,这时能保证instance变量的修改对其他变量是可见的。避免上述的问题。

volatile关键字不仅保证多线程可见,还会保证volatile关键字前、后的步骤一定是在其前后。

比如:

int a =10;          //步骤1
volatile String b = "20";    //步骤2
int c = 30;         //步骤3
int d = a++;      //步骤4

这样能保证步骤1一定在步骤2之前执行完,虽然不能保证步骤3、4的顺序但步骤3,4一定在步骤2之后执行。

应用volatile关键字的三个原则

(1)写入变量不依赖此变量的值,或者只有一个线程修改此变量(否则写入时可能不是当时对应的值)

(2)变量的状态不需要与其它变量共同参与不变约束

(3)访问变量不需要加锁

题外:

上文中双重检查锁是早期一种为了减轻锁影响的效率而发明的“聪明”的写法。因为1.5以前内置锁效率很低,为此搞出了很多奇门偏方,双重检查锁就是其中之一。实际上也是因为早期内存、cpu资源紧缺,很多东西都希望懒加载导致。双重检查锁这种方式是最不推荐的单例写法。这种方式只考虑了同步的一致性没考虑可见性,volatile只保证可见性不保证一致性。因此,两个东西结合起来才能真正保证并发安全。实际1.5之后的jdk,锁已经很轻量了。就单例而言,初始加载或者枚举是最简单也是最推荐的做法。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值