在Java并发编程中,synchronized和volatile 都扮演着重要的角色,volatile是轻量级的synchronzied,其在多处理器开发时保证了共享变量的"可见性".
我们知道CPU速度非常快,比内存快百倍以上,所以CPU更希望和速度相近的CPU cache打交道。
而一个多核的CPU本质上就是多个CPU共用一个外壳,每个核就是一个单核CPU,其都有属于自己的cache。
当更改一个变量时,CPU将值写入到对应的cache中,不一定写入内存中,这个CPU认为值已经改变了,但是其他CPU认为值没有改变。这样就会出现脏读。
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"等.
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 所在行其实有三句话
JVM会对代码进行重排序(为了优化速度)
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton 为空 |
|
T2 | 获取锁 | |
T3 | 再次检查到uniqueSingleton 为空 |
|
T4 | 为uniqueSingleton 分配内存空间 |
|
T5 | 将uniqueSingleton 指向内存空间 |
|
T6 | 检查到uniqueSingleton 不为空 |
|
T7 | 访问uniqueSingleton (此时对象还未完成初始化) |
|
T8 | 初始化uniqueSingleton |
所以需要使用volatile关键字,防止指令重排序导致的线程安全问题。