首页 > 学院 > 开发设计 > 正文

volatile 和 synchronized

2019-11-08 02:58:25
字体:
来源:转载
供稿:网友

volatile 与可见性

可见性

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 禁止指令重排序

此为 volatile 的语义之一。 查看:重排序 原理:【附录】volatile 的内存语义。

volatile 并不能保证变量同步

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{ volatile private static int count = 0; @Override public void run() { for (int i = 0; i < 100; i++) { add(); } } public void add(){ count++; } public static int getCount(){ return count; }}

如上代码运行三次,输出的结果分别为:

9998、9941、9895

都没有达到理想的结果 10000;


原因分析: 此处 count++ 不具有原子性。

结论: volatile 不具有原子性,不能保证变量同步; 要保证变量同步还得用 synchronized 关键字。

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 能够保证变量的同步性;但是一定要注意,同步方法或同步块一定要使用同一个对象锁。

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 也是可以实现变量对所有线程可见的。

synchronized 实现可见性的原理

原理:【附录】锁的内存语义。

volatile 和 synchronized 对比总结

这里写图片描述

【附录】使用 synchronized 实现同步的表现形式

当用在普通方法的时候,锁是当前的实例对象;当用在静态方法的时候,锁是当前类的 Class 对象;当用在代码块的时候,锁是括号中配置的对象;

【附录】volatile 的内存语义

volatile 的内存语义

volatile 的读内存语义:当读一个 volatile 变量时,JMM 将该线程对应的本地内存置为无效,从主内存中读取变量; volatile 的写内存语义:当写一个 volatile 变量时,JMM 将该线程对应的本地内存中的共享变量值刷新到主内存。

volatile 内存语义实现原理

什么是内存屏障:用于实现对内存操作的顺序限制。

这里写图片描述


为了实现 volatile 的内存语义,JMM 会限制重排序:

限制 volatile 修饰的共享变量之间的重排序;限制 volatile 修饰的共享变量与普通共享变量之间的重排序;

下面是 JMM 制定的 volatile 重排序规则表:

这里写图片描述

JMM 通过插入内存屏障的方式限制重排序,如下图:

这里写图片描述

这里写图片描述

详解:

在每个 volatile 写操作前插入 StoreStore 屏障在每个 volatile 写操作后插入 StoreLoad 屏障在每个 volatile 读操作后插入 LoadLoad 屏障在每个 volatile 读操作后插入 LoadStore 屏障

【附录】锁的内存语义

锁的内存语义

线程释放锁前,JMM 会将共享变量的最新值刷新到主内存中;线程获取锁时,JMM 会将线程对应的本地内存置为无效,从而在需要共享变量的时候必须去主内存中读取,同时保存在本地内存。

注意:加锁解锁必须是同一把锁。

可以看出,锁释放和 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写的下方了?


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表