Java并发学习 3 :volatile的应用

在Java并发编程中,synchronized和volatile 都扮演着重要的角色,volatile是轻量级的synchronzied,其在多处理器开发时保证了共享变量的"可见性".

问题引入:多个CPU的不可见性造成脏读

我们知道CPU速度非常快,比内存快百倍以上,所以CPU更希望和速度相近的CPU cache打交道。

而一个多核的CPU本质上就是多个CPU共用一个外壳,每个核就是一个单核CPU,其都有属于自己的cache。

当更改一个变量时,CPU将值写入到对应的cache中,不一定写入内存中,这个CPU认为值已经改变了,但是其他CPU认为值没有改变。这样就会出现脏读。

解决方式:volatile 指令实现可见性

volatile 底层是使用Lock指令。

CPU读取到lock指令后,会做以下操作。

1. 将对应的值先到CPU对应的cache中

2. 将对应的值写入到内存中(就是内存条,我们称其为系统内存)

3. 第二部的写操作会使在其他CPU里缓存了该地址的数据无效。

4. 其他CPU当需要这个数值时,必须去重新读取内存。

这样就保证了多CPU之间的可见性。

问题引入:指令重排序造成线程安全问题

JVM会在不影响单线程运行结果的准则下,对代码进行重排序。举个例子,

public void writer() {
    a = 1;                   //1
    flag = true;             //2
}

 虽然你写的代码是按照,"1","2"排序,但是在JVM编译后,可能顺序就是"2","1"等.

JVM为什么这样做?

public void writer() {
    int i = 0;   
     a = 1;                   //1
    while (i++ != 2){
        flag = true;             //2
        a++;
    }

}

 很明显,循环每次都会调用flag = true,完全不必要,可以将2插入到1前后.可以增加运行效率。

为什么会有线程安全问题?

简单来说,代码在多线程下本来就难以掌控,重排序往往会造成一些意想不到的问题。

因为我们写出来的多线程代码可能会依赖于特定的代码顺序,如果更改,那么会导致线程安全问题。

经典案例:基于双重检查的单例模式

单例模式就是保证一个类始终只有一个对象的一种编码方法。我们可以很容易的写出来一个单线程下的单例饿汉模式

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            // 可能同时有多个线程进入if中
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

在多线程时,可以会new出多个实例.

我们可以通过synchronized关键子锁住这个方法,保证其线程安全。

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

synchronized是把这个方法锁成一个单线程方法,效率降低十分明显。

我们通过volatile进行优化,将其synchronized锁的粒度减小。

// 3 所在行其实有三句话

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

JVM会对代码进行重排序(为了优化速度)

Time Thread A Thread B
T1 检查到uniqueSingleton为空  
T2 获取锁  
T3 再次检查到uniqueSingleton为空  
T4 uniqueSingleton分配内存空间  
T5 uniqueSingleton指向内存空间  
T6   检查到uniqueSingleton不为空
T7   访问uniqueSingleton(此时对象还未完成初始化)
T8 初始化uniqueSingleton  

所以需要使用volatile关键字,防止指令重排序导致的线程安全问题。

 

 

 

你可能感兴趣的:(并发编程学习)