synchronized和volatile关键字解决内存可见性问题的比较
1. 内存可见性问题的简述
两个线程同时操作一个共享变量,就可能发生可见性问题:
- 主存中有变量sum,初始值为0
- 线程A计划将sum加1,先将sum=0复制到自己的私有内存中,然后更新sum的值。线程A操作完成之后其私有内存中sum的值为1,然而线程A将更新后的sum值回刷到主存的时间是不固定的
- 在线程A没有回刷sum到主存前,刚好线程B同样从主存中读取sum,此时值为0,和线程A进行同样的操作,最后期盼的sum=2目标没有达成,最终sum=1
线程B没有将sum变成2的原因是:线程A的修改还在其工作内存中,对线程B不可见,因为线程A的修改还没有刷入主存。这就发生了典型的内存可见性问题
解决内存可见性问题有两种方法:
-
synchronized关键字 volatile关键字
2. synchronized关键字
先介绍以下synchronized的内存语义,使用其内存语义可以解决可见性问题:
进入synchronized块:synchronized 块内使用到的变量从线程的工作内存中清除,这样在 synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取 退出 synchronized 块的内存语义是把在 synchronized 块内对共享变量的修改刷新到主内存
3. volatile关键字
内存语义:
当 一个变量被声明为 volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值
4. 代码实践
//不安全的变量操作 public class ThreadNotSafeInteger { private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
//使用synchronzied进行同步 public class ThreadSafeInteger { private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
//使用volatile进行同步 public class ThreadSafeInteger { private volatile int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
上面的第2处和第3处代码在这里是等价的,都保证了共享变量的可见性,但二者还是存在区别的:
- synchronized解决了变量的内存可见性和原子性问题,但是volatile关键字值解决了变量的内存可见性问题,并不能解决原子性问题(比如将上面的赋值操作改成value++操作,则使用volatile关键字的代码会在多线程的情况下发生错误,因为volatile关键字不能保证源字性,自增操作不是原子操作)
- synchronized是阻塞的,volatile是非阻塞的;
使用volatile关键字的场景:
1. 写入变量值不依赖变量的当前值时 。 因为如果依赖当前值,将是获取一计算一写入 三步操作,这三步操作不是原子性的,而 volatile 不保证原子性 2. 读写变量值时没有加锁 。 因为加锁本身已经保证了内存可见性,这时候不需要把变 量声明为volatile的
参考: 《Java高并发核心编程》 《Java并发编程之美》