volatile 的语义之一,意思是一个线程修改了共享变量时,另一个线程能够读到这个修改后的值。
每个线程都拥有独有的本地内存,而 JMM 控制着主内存和本地内存的数据交换。
public class VolatileExample01 { public static void main(String[] args) throws Exception { MyThread thread = new MyThread(); thread.start(); try { Thread.sleep(3000); } finally { thread.setFlag(true); } }}class MyThread extends Thread { PRivate boolean flag = false; @Override public void run() { while (!flag) { //System.out.println("Running..."); } } public void setFlag(boolean flag) { this.flag = flag; }}上述代码运行起来有可能形成死循环。
过程分析: 上述代码中主线程将 flag 读取到本地内存进行修改,然后刷新到主内存;然而线程 thread 一直在读取其本地内存中的 flag 值,无法看到主线程对 flag 变量做的修改,因此造成死循环。
解决办法: 用 volatile 修饰共享变量
volatile private boolean flag = false结论: volatile 修饰共享变量能够保证其的可见性。
原理:【附录】volatile 的内存语义。
此为 volatile 的语义之一。 查看:重排序 原理:【附录】volatile 的内存语义。
如上代码运行三次,输出的结果分别为:
9998、9941、9895
都没有达到理想的结果 10000;
原因分析: 此处 count++ 不具有原子性。
结论: volatile 不具有原子性,不能保证变量同步; 要保证变量同步还得用 synchronized 关键字。
修改上述代码,去掉 volatile 关键字,使用 synchronized 将 add() 同步,如下:
public class SynchronizedExample01 { public static void main(String[] args) throws Exception { Mythread02[] threadArray = new Mythread02[100]; for (int i = 0; i < 100; i++) { threadArray[i] = new Mythread02(); } for (int i = 0; i < 100; i++) { threadArray[i].start(); } Thread.sleep(10000); System.out.println(Mythread02.getCount()); }}class Mythread02 extends Thread{ private static int count = 0; @Override public void run() { for (int i = 0; i < 100; i++) { add(); } } synchronized public void add(){ count++; } public static int getCount(){ return count; }}运行输出的结果还是会小于 10000。
分析: synchronized 加在非 static 方法上意思是锁住当前对象,而多个线程使用的是多个对象,一个线程进来后出来之前,另一个线程还是会进来的。
对策: 将 add() 改为静态方法,保证所有线程在这个方法上使用的是同一个对象锁。
synchronized static public void add(){ count++;}结论: synchronized 能够保证变量的同步性;但是一定要注意,同步方法或同步块一定要使用同一个对象锁。
我们回到文章开头的死循环的例子; 除了使用 volatile 修饰共享变量能保证可见性之外,synchronized 同样可以实现。 我们可以这样改:
public class VolatileExample01 { public static void main(String[] args) throws Exception { MyThread thread = new MyThread(); thread.start(); try { Thread.sleep(3000); } finally { thread.setFlag(true); } }}class MyThread extends Thread { private boolean flag = false; @Override public void run() { while (!flag) { synchronized (this) { // do something ... } } } public void setFlag(boolean flag) { this.flag = flag; }}分析:
结论: synchronized 也是可以实现变量对所有线程可见的。
原理:【附录】锁的内存语义。
volatile 的读内存语义:当读一个 volatile 变量时,JMM 将该线程对应的本地内存置为无效,从主内存中读取变量; volatile 的写内存语义:当写一个 volatile 变量时,JMM 将该线程对应的本地内存中的共享变量值刷新到主内存。
什么是内存屏障:用于实现对内存操作的顺序限制。
为了实现 volatile 的内存语义,JMM 会限制重排序:
限制 volatile 修饰的共享变量之间的重排序;限制 volatile 修饰的共享变量与普通共享变量之间的重排序;下面是 JMM 制定的 volatile 重排序规则表:
JMM 通过插入内存屏障的方式限制重排序,如下图:
详解:
在每个 volatile 写操作前插入 StoreStore 屏障在每个 volatile 写操作后插入 StoreLoad 屏障在每个 volatile 读操作后插入 LoadLoad 屏障在每个 volatile 读操作后插入 LoadStore 屏障注意:加锁解锁必须是同一把锁。
可以看出,锁释放和 volatile 读具有相同的内存语义;锁获取和 volatile 写具有相同的内存语义
内置锁,即使用 synchronized 形成的锁。
内置锁依赖于 JVM。编译器会在同步块的入口位置和退出位置分别插入 monitorenter 和 monitorexit字节码指令。而对于 synchronized 方法,编译器会在 Class 文件的方法表中将该方法的 access_flags 字段中的 synchronized 标志位置 1,表示该方法是同步方法并使用调用该方法的对象。
以 ReentrantLock(分为公平锁和非公平锁) 为例:
公平锁 加锁:首先会调用 getState() 方法读 volatile 变量 state; 解锁:setState(int newState) 方法写 volatile 变量 state。 实质上还是在使用 volatile 共享变量。
非公平锁 加锁:首先会使用 CAS 更新 volatile 变量 state,更新不成功再去采用公平锁的方式(比较粗鲁) 解锁:setState(int newState) 方法写 volatile 变量 state。 CAS 先读后写,CAS 读(volatile 读)不会与后面的任何操作重排序,CAS 写(volatile 写)不会与前面的任何操作重排序,所以 CAS 操作不会与 CAS 前面和后面的任意操作重排序。
利用了 CAS 附带 volatile 变量实现。
Q:为什么 volatile 写只加入了Store-Store屏障呢,这样普通读不就可以重拍到volatile写的下方了?
新闻热点
疑难解答