一、多线程下变量的不可见性
在多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接 看到该线程修改后的变量的最新值。
我们首先让子线程去更改变量flag的值为true,主线程通过判断后执行。
public class MyThread extends Thread{
public static void main(String[] args) {
// 1、启动子线程,将线程中的flag值改为true
VolatileThread thread = new VolatileThread();
thread.start();
// 2、主线程
while (true){
if(thread.isFlag()){
System.out.println("主线程执行,此时flag已经改为true!!!!");
}
}
}
}
class VolatileThread extends Thread{
private boolean flag = false;
@Override
public void run() {
// 线程中修改变量值
flag = true;
System.out.println("子线程将flag值变为true");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
验证不可见性,我们加上一个延迟
此时当子线程将flag变量变成true,但是主线程是看不到的,导致主线程的输出语句并没有输出!
二、变量不可见性内存语意
在介绍多线程并发修改变量不可见现象的原因之前,我们需要了解回顾一下java多线程的内存模型(和java并发编程有关的内存模型)—JMM(注意和JVM的区别)
JMM(java Memory Model):java内存模型,是java虚拟机规范中所定义的一种内存模型,java内存模型是标志化的,屏蔽了不同计算机的底层区别。
java内存模型描述了java程序中各种变量(线程共享变量)的访问规则,以及在jvm中将变量存储到内存和从内存中读取变量这样的底层细节。
JMM有以下规定
- 所有的共享变量都存储于主内存中,这里说的变量不包含局部变量 ,因为局部变量是私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了共享变量的副本。
- 线程对变量的所有操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程间,也不能直接访问对方工作内存中的变量,线程间的变量的值的传递需要通过主内存中转完成。
总结一下:线程读取共享变量到字节的工作内存中,然后更改工作变量副本的值,最后在刷新到主内存当中。
上述代码的执行流程分析
①:子线程t从主内存读取到数据放入其对应的工作内存。
②:将flag的值更改为true,但是这个时候flag的值还没有写回主内存。(它首先更改自己的工作内存中共享变量副本,什么时候写会内存是不一定的)。
③:此时main方法读取到了flag的值为false。
④:当子线程t将flag的值写回去后,但是main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值,所以while(true)读取到的值一直是false。 (如果有一个时刻main线程从主内存中读取到了 主内存中flag的最新值,那么if语句就可以执行,但是注意main线程何时从主内存中读取最新的值,我们无法控制)。
总结:内存不可见性的原因是
每个线程都有自己的工作内存,线程都是从主内存拷贝共享变量的副本值,当一个线程修改了一个共享变量的值时,它可能首先将该值存储在自己的工作内存中,并不会立即写回主内存。其他线程在读取该共享变量时,可能会从自己的工作内存中读取值,而不是从主内存中获取最新值。这种情况下,如果一个线程修改了共享变量的值,其他线程可能无法立即感知到这个变化,导致不可见性问题。
三、变量不可见性解决方案
如何实现多线程间访问共享变量的可见性?
- 加锁
- 使用vloatitle关键字
- while(true)速度快调用本地线程内存,加延时即可访问主内存的共享变量
1.加锁的方式解决
public class MyThread extends Thread{
public static void main(String[] args) {
// 1、启动子线程,将线程中的flag值改为true
VolatileThread thread = new VolatileThread();
thread.start();
// 2、主线程
while (true){
synchronized ("1"){
if(thread.isFlag()){
System.out.println("主线程执行,此时flag已经改为true!!!!");
}
}
}
}
}
class VolatileThread extends Thread{
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程中修改变量值
flag = true;
System.out.println("子线程将flag值变为true");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
讲解:当main进入到synchronized代码块中,执行过程如下:
1.线程获取锁
2.清空工作内存(本地内存)
3.从主内存拷贝共享变量最新值到工作内存中称为副本。
4.执行代码,将修改后的副本刷新会主内存中
5.线程释放锁。
2 使用volatile关键字
public class MyThread extends Thread{
public static void main(String[] args) {
// 1、启动子线程,将线程中的flag值改为true
VolatileThread thread = new VolatileThread();
thread.start();
// 2、主线程
while (true){
if(thread.isFlag()){
System.out.println("主线程执行,此时flag已经改为true!!!!");
}
}
}
}
class VolatileThread extends Thread{
private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 线程中修改变量值
flag = true;
System.out.println("子线程将flag值变为true");
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
1.子线程t从主内存读取到数据放入其对应的工作内存.
2.将flag的值更改为true,但是这个时候flag的值还没有写会主内存.
3.此时main方法main方法读取到了flag的值为false.
4.当子线程t将flag的值写回去后,失效其他线程对此变量副本.
5.再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中.
四、Volatile是如何解决内存不可见性的
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效(MESI协议)。
解释一下我们的线程是由程序和数据组成的,在CU(控制器)的控制下在ALU(加法器)当中运行,其中我们讲的工作内存指的就是L1和L2级缓存。
通常为了提高处理速度,处理器不直接和主存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或寄存器)后再进行操作,但操作完不知道何时会写到内存。这种情况下,如果一个线程修改了共享变量的值,其他线程可能无法立即感知到这个变化,导致不可见性问题。
那么Volatile是如何解决内存不可见性的呢?
lock指令更底层的实现主要有两种方式:总线锁和缓存锁。
总线锁:
当其中一个处理器要对共享内存进行操作的时候,在总线上发出 一个LOCK#信号,这个信号就会将总线锁住,使得该 处理器内核可以独占任何共享内存,而其他处理器内核只能等待。在锁定期间,其他处理器内核不能操作其他内存地址的数据,所以总线锁定 的开销比较大,这种机制显然是不合适的。
缓存锁:
就是指内存区域如果被缓存在处理器的缓存行中,并且在Lock期间被锁定,那么当它执 行锁操作回写到内存时,不再总线上加锁,而是修改内部的内存地址,基于缓存一致性协议来保证操作 的原子性。
缓存一致性协议(MESI),将缓存行中的数据划分成了4个状态:修改、独占、共享、失效
- M (Modifhed修改) :当cpu对变量进行修改时,现在cpu内的缓存行中上锁,并向总线发信号,此时cpu中的变量状态为M。
- E (Exclusive独享) :当cpu读取一 个变量时,该变量在工作内存中的状态是E。
- S (Shared共享) :当cpu读取该变量时,两个cpu中该变量的状态由E转为S。
- | (Invalid无效) : cpu嗅探到变量被其他cpu修改的信号,于是将自己缓存行中的变量状态设置为i,即失效。则cpu再从内存中获取最新数据。
流程如下