1 引入
并发编程下,多线程修改变量,会出现线程间变量的不可见性。即多个线程访问共享变量,会出现一个线程修改变量的值后,其他线程看不到最新值的情况。
代码示例:
public class VolatileThread extends Thread {
// 定义成员变量
private boolean flag = false ;
public boolean isFlag() { return flag;}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将flag的值更改为true
this.flag = true ;
System.out.println("flag=" + flag);
}
}
public class VolatileThreadDemo {// 测试类
public static void main(String[] args) {
// 创建VolatileThread线程对象
VolatileThread volatileThread = new VolatileThread() ;
volatileThread.start();
// main方法
while(true) {
if(volatileThread.isFlag()) {
System.out.println("执行了======");
}
}
}
}
运行结果:
falg=true
由此可见,VolatileThread
线程中已将flag设置为true,但main()
方法中始终没有读到,即出现了变量不可见问题。
2 变量不可见性出现的原因
在此之前,我们先来了解一下Java内存模型(java memory model, JMM),其屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
JMM有如下规定:
- 所有的共享变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数,因为局部变量和方法参数是线程私有的,不存在竞争问题。
- 每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存变量的副本拷贝。
- 线程对变量的操作(读、写)都必须在工作内存中进行。
线程不能直接读写主内存中的变量。 - 不同线程之间也不能直接访问对方工作内存中的变量,线程间的变量的值的传递需要通过主内存中转来完成。
工作内存和主内存的关系图:
通过JMM我们可以分析得出现变量不可见的原因:
- VolatileThread线程从主内存读取到数据放入其对应的工作内存
- 将flag的值更改为true,但是这个时候flag的值还没有写会主内存
- 此时main方法读取到了flag的值为false
- 当VolatileThread线程将flag的值写回去后,但是main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值,所以while(true)读取到的值一直是false。(如果有一个时刻main线程从主内存中读取到了主内存中flag的最新值,那么if语句就可以执行,main线程何时从主内存中读取最新的值,我们无法控制)
3 变量不可见性的解决方法
3.1 加锁
// main方法
while(true) {
synchronized (volatileThread) {
if(volatileThread.isFlag()) {
System.out.println("执行了======");
}
}
}
某一个线程进入synchronized代码块前后,执行过程入如下:
- 线程获得锁
- 清空工作内存
- 从主内存拷贝共享变量最新的值到工作内存成为副本
- 执行代码
- 将修改后的副本的值刷新回主内存中
- 线程释放锁
总结:线程每次获得锁对象时会清空自己的工作内存,重写读取主内存的最新值。
3.2 使用volatile关键字
volatiled的三大特性:
- 保证可见性。当一个线程修改共享变量的值,其他线程能够立即知道被修改了。
- 不保证原子性。短时间内,两个线程工作内存中的变量值同时更新到主内存,会导致主内存中变量的值与预期不同。
- 禁止指令重排。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测
private volatile boolean flag ;
工作原理:
- VolatileThread线程从主内存读取到数据放入其对应的工作内存
- 将flag的值更改为true,但是这个时候flag的值还没有写会主内存
- 此时main方法main方法读取到了flag的值为false
- 当VolatileThread线程将flag的值写回去后,失效其他线程对此变量副本
- 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中
总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。
volatile不能用来修饰final类型的变量,因为没有意义
3.3 volatile与synchronized的区别
- volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
- volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制,