这一节来对比下synchronized和volatile关键字在三大性质中的不同。
1. 原子性
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。即使在多线程情况下,也能保证不被其它线程干扰。
我们来看下面几个例子
int a = 10; // 1
++a; // 2
int b = a; // 3
a = a+1; // 4
在上面的三个操作中,只有第一个操作时具有原子性的。第二个自增操作实际上时分为三步的:取a的值、对a的值+1、赋值给a,所以是无法保证原子性的。3、4同理也不具有原子性。
在JMM内存模型中,只有如下8个操作是具有原子性的:
- lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态。
- unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来。
- read(读取):作用于主内存中的变量,它把从主内存中读取的变量传送到工作内存,以便后面的load。
- load(载入):作用于工作内存的变量,将read操作读取的变量放入工作内存中的变量副本。
- use(使用):作用域工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎。
- assign(赋值):作用于工作内存中的变量,它把从执行引擎计算的变量赋值给工作内存的变量。
- store(存储):作用于工作内存的变量,它把工作内存中的变量值送给主内存中以便随后的write操作。
- write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存中。
值得注意的是:Java内存模型只是要求上述两个操作时顺序执行的而不是连续执行的。也就是说read和load之间可以插入其它指令,store和write可以插入其它指令。例如可以出现这样的顺序:read a,read b,load b,load a。
synchronized
synchronized关键字的实现原理是基于monitorenter和monitorexit,其实就相当于lock和unlok的原子性操作。
因此,synchronized是满足原子性的。
volatile
我们来看下面的代码
public class Demo1 {
private static volatile int num = 0;
public static void add(){
for(int i=0;i<1000;i++){
num++;
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<10;i++){
Thread thread = new Thread(){
@Override
public void run() {
Demo1.add();
}
};
thread.start();
}
Thread.sleep(1000);
System.out.println(Demo1.num);
}
}
实际的执行结果可能是1000、也可能是9991,9983…
我们可以设想这样一个场景:首先线程A从主内存中拷贝了一份变量副本到工作内存中,但是还没来得及修改,就被阻塞了。这时,线程B也从主内存中读取了变量到工作内存中,因为这时num还没有被修改,所以是有效的。然后线程B对num进行了自增操作,并写回到了主内存中。虽然volatile关键字是具有可见性的,但是由于线程A已经完成了读取的这个原子性操作了,所以就不会再检查。这时线程A会直接将100加1,然后写回到主内存中。这就造成了数据不一致的问题。因此volatile不能使得本身不具有原子性的操作变成具有原子性的。
因此,volatile操作是不满足原子性的。
2. 有序性
Java程序的天然的有序性可以解释为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另外一个线程,所有操作都是无序的。
synchronized
synchronized关键字使得在同一个时刻,只能有一个线程获得监视器,进入临界区。其实也就相当于,要求读写共享变量是串行执行的。
因此synchronized操作是满足有序性的。
volatile
我们利用懒汉式的双重检验单例模式(有篇文章介绍双重检验单例模式讲的很好,值得一看)来分析volatile是否具有有序性。代码如下:
public class Demo {
public volatile static Demo demo;
private Demo(){
}
public Demo getDemo(){
if(demo==null){
synchronized (Demo.class){
if(demo==null){
demo = new Demo();
}
}
}
return demo;
}
}
创建一个对象实际分为三步:
- 分配对象的内存空间
- 初始化对象
- 设置demo指向刚分配的内存地址
第一步和第二步是存在依赖关系的,不能被重排序。而第三步和第二步没有依赖关系,实际上是可以重排序的。这样就有可能出现这种情况:
如果2和3进行了重排序,就有可能造成线程B拿到了没有被初始化的对象。而volatile关键字能够禁止操作2和3的重排序,从而避免这种情况。
因此,volatile关键字是满足有序性的。
3. 可见性
可见性是指当一个线程修改了共享变量后,其它线程能够立即得知这个修改。
synchronized
synchronized保证,当线程获取到锁的时候,会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。
因此synchronized关键字是满足可见性的。
volatile
由于lock前缀指令,volatile写之后会使得其它缓存中的共享变量也失效,强制从主内存中重新读取,从而实现可见性。
因此volatile关键字是满足可见性的。
4. 总结
synchronized:原子性、有序性、可见性
volatile:有序性、可见性。
参考了这篇文章:三大性质总结:原子性,有序性,可见性