Java多线程之内存可见性
什么叫“可见性”?
一个线程对共享变量值的修改,能够被其他线程及时看到。
共享变量:如果一个变量在多个线程的工作内存中存在副本,那么这个变量就是这几个线程的共享变量。
所有变脸都存在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用大的变量副本,关系如下图所示:
多线程遵守的两条规定
1.线程对共享变量所有的操作都只能在自己的工作内存中完成,无法直接从主内存中读写
2.不同线程之间无法访问其他线程中的变量,线程中变量值的传递需要通过主内存来完成。
共享变量可见性的实现原理
线程1对共享变量的修改如果要被线程2及时看到,需要经过2个步骤:
1.把工作内存1中更新过的共享变量值刷新到主内存中
2.把主内存中最新的共享变量的值更新打工作内存2中
以上2个步骤,任意一个出现问题,都会导致共享变量无法被其他线程及时看到,无法实现可见性,导致其他线程读取的数据不准确从而产生线程不安全。
共享变量可见性的实现方式
Java语言层面支持的可见性实现方式有2种,分别是synchronized、volatile。
synchronized:能够实现原子性(同步)和可见性
volatile:能够保证可见性,但是无法保证原子性
synchronized是如何实现可见性?
java内存模型(JMM)中关于synchronized的两条规定:
1).线程解锁前,必须把共享变量的最新值刷新到主内存中
2).线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新值(注意:加锁与解锁需要是同一把锁)
线程执行互斥代码的过程
1.获得互斥锁
2.清空工作内存
3.从主内存拷贝变量的最新副本到工作内存中
4.执行代码
5.将更改后的共享变量值刷新到主内存中
6.释放互斥锁
指令重排序
代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或者处理器为了提高程序性能而做的优化。
目前的指令从排序有3种方式:
1.编译器优化的重排序(编译器优化)
2.处理器优化的重排序(处理器优化)
3.内存优化的重排序(处理器优化)
as-if-serial
无论如何重排序,程序执行的结果都应该与代码顺序执行的结果一致(java编译器和处理器运行时都会保证在单线程中遵循as-if-serial规则,多线程存在程序交错执行时,则不遵守)
举例:
int num1 = 1;
int num2 = 2;
int num3 = num1 + num2;
上面3行代码,在单线程时,第1、2行可以进行重排序,但是第3行不可以,否则结果将不一样,所以从排序不会给单线程带来内存可见性的问题。
而在多线程中,程序交错执行时,重排序则会造成内存可见性的问题。
Synchronized实现可见性的代码,以下的这个类SynchronizedDemo
public class SynchronizedDemo { // 共享变量 private boolean ready = false; private int num = 1; private int result = 0; // 写操作 public void write() { ready = true; // 1.1 num = 2; // 1.2 } // 读操作 public void read() { if (ready) { // 2.1 result = num * 3; // 2.2 } System.out.println("result = " + result); } private class ReadWriteThread extends Thread { private boolean flag; public ReadWriteThread(boolean flag) { this.flag = flag; } @Override public void run() { if (flag) { write(); } else { read(); } } } public static void main(String[] args) { SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); synchronizedDemo.new ReadWriteThread(false).start(); synchronizedDemo.new ReadWriteThread(true).start(); } }
上面的这一段代码重排序后的执行顺序可能是
1. 1.2-->2.1-->2.2-->1.1; result=0
2. 1.1-->2.1-->2.2-->1.2; result=3
......
导致共享变量在线程之间不可见的原因
1.线程的交叉执行
2.重排序结合线程交叉执行
3.共享变量更新后的值,没有在工作内存与主内存间及时刷新
安全的代码,加入synchronized关键字
// 写操作 public synchronized void write() { ready = true; // 1.1 num = 3; // 1.2 } // 读操作 public synchronized void read() { if (ready) { // 2.1 result = num * 2; // 2.2 } System.out.println("result = " + result); }
volatile是如何实现可见性?
深入来说,是通过加入内存屏障和禁止重排序优化来实现的。
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令,会将cup数据强制刷新到主内存中去
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令,强制缓存器中的缓存失效,每次使用都要去主内存中重新获取数据
通俗地讲,volatile变量在每次被访问的时候,都强迫从主内存中读取该变量的值,而当该变量在发生变化时,又会强迫变量讲最新的值刷新到主内存中,这样,任意时刻,不同的线程总能看到该变量的最新值。
线程写volatile变量的过程:
1.改变线程工作内存中volatile变量副本的值
2.将改变的副本的值从工作内存中刷新到主内存中
线程读volatile变量的过程:
1.从主内存中读取volatile变量的最新值到工作内存中
2.从工作内存中读取volatile变量的副本
volatile不能保证原子性,请看下面的代码:
public class VolatileDemo { private volatile int num = 0; public int getNum() { return this.num; } public void increase() { // num++,不是原子操作,这里会先读取,再加1 this.num++; } public static void main(String[] args) { final VolatileDemo volatileDemo = new VolatileDemo(); // 创建500个子线程,执行increase方法,每次都让num加1 for (int i = 0; i < 500; i++) { new Thread(new Runnable() { @Override public void run() { volatileDemo.increase(); } }).start(); } // 等到所有子线程执行完毕,eclipse这里是1,IntelliJ IDEA执行用户代码的时候,实际是通过反射方式去调用,而与此同时会创建一个Monitor Ctrl-Break 用于监控目的,所有是2 while (Thread.activeCount() > 2) { Thread.yield(); } // 由于num使用了volatile关键字,所以预期值应该是500 System.out.println("当前的num值=" + volatileDemo.getNum()); } }
执行后,会发现,有时候不是500,而是499或者498或者497等等,原因是num++不是原子操作,volatile只能保证变量修改后的可见性,但是无法保证原子性,请看下面的步骤:
假设现在num=5
1.线程A读取num的值,线程A的工作内存中,num=5
2.线程B也读取了num的值,线程B的工作内存中,num=5
3.线程B进行加1操作,线程B的工作内存中,num=6
4.线程B写入最新的num值,主线程中num的值变为6
5.线程A执行加1操作,线程A的工作内存中,num=6
6.线程A写入最新的num值,主线程中num的值变为6
这样,两个线程各自执行了一次加1操作,但是主线程中的数据num=6,这就是由于volatile没办法保证代码的原子性,使得读和写不是一起的
解决方案:
1.使用synchronized关键字
2.使用ReentrantLock
3.使用AtomicInteger
volatile的适用场景
1.对变量的写入操作不依赖其当前值
不满足:num++、count = count * 5
满足:boolean值变量,记录温度变化的变量等等
2.该变量没有包含在具有其他变量的不变式中
不满足:low < up
一般的应用场景很多会不满足其中一个,所以volatile是使用没哟synchronized这么广泛。
synchronized与volatile比较
1.volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
2.从内存的角度,volatile读操作相当于加锁,写操作相当于解锁
3.synchronized既能保证原子性又能保证可见性,而volatile只能保证可见性无法保证原子性