共享变量可见性问题以及解决方案

文章目录

    • 1. 简介
    • 2. 解决方案

1. 简介

首先在了解可见性问题之前我们首先需要给出Java 内存模型的定义(JMM),java讲内存模型抽象为两个部分,主存以及工作内存,主 存也就是所有线程所共享的一段存储空间,工作内存是所有线程各种私有的一块内存空间,若此时某个线程想操作一个共享变量,它需要先将主存主的共享变量读取到自己的工作内存中,然后进行操作。此时就出现了一个问题,一个线程将对自己的更新推送至主存后,其它线程没有及时读取主存中共享变量的更新,而是一直使用自己工作内存中缓存的旧值,最后导致更新不可见,这就是所谓的可见性问题。看下面一段代码:

@Slf4j
public class Hello{
	//共享变量
	static  boolean flag=false;
	public static void main(String[] args) throws InterruptedException {
	  new Thread(()->{
		  while(!flag)
		  {
			  log.info("我还在等地flag等于true");
		  }
	  }).start();
	  Thread.sleep(1000);
      flag=true;
	}
}

共享变量可见性问题以及解决方案_第1张图片
最后我们创建的线程会陷入死循环,按照正常的逻辑去理解,在主线程修改flag后创建的线程会从主存空间读取到主线程的修改然后退出while循环,而出现这种结果的原因是jvm中JIT的存在,JIT即java即时编译技术,它底层又c1和c2两个编译器组成(新版本可能是c1和Graal),c2或Graal会利用一些技术对我们的代码进行优化(这会导致一些问题)。再分析一下上面代码工作的底层流程:

  1. 初始状态,t线程刚开始从主内存中读取了flag的值到工作内存中
    共享变量可见性问题以及解决方案_第2张图片
  2. 每一次新的while循环,t线程都要从主内存中读取flag的值,这是一个十分耗时的过程,所以JIT这里就对我们代码进行了优化,JIT会将flag的缓存至线程自己的工作内存中的高速缓存中,减少对flag的主存访问,提高程序运行效率。

共享变量可见性问题以及解决方案_第3张图片
3. 主线程更新flag时,main线程将对flag的更新更新到主存中的共享变量,而t线程还是使用自己缓存中的旧值。

共享变量可见性问题以及解决方案_第4张图片
最后就导致了可见性问题。

2. 解决方案

java为我们提供了volatilo关键字,volatile是一个特征修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值(线程不会在缓存汇中读值)。下面查看更新后的代码:

@Slf4j
public class Hello{
	//共享变量
	volatile static  boolean flag=false;
	public static void main(String[] args) throws InterruptedException {
	  new Thread(()->{
		  while(!flag)
		  {
			  log.info("我还在等地flag等于true");
		  }
	  }).start();
	  Thread.sleep(1000);
      flag=true;
	}
}

共享变量可见性问题以及解决方案_第5张图片

这就解决了可见性问题,synchronized同样也可以解决可见性,但解决可见性还是推荐使用volatile,因为synchronized是重量级锁,消耗是很大的。但相比于volatile,sychronized也有其优势,sychronized语句块既可以保证代码的原子性,也同时可以保证代码块内变量的可见性。volatile是不能保证代码的原子性,它只能保证代码我们每次读到volatile修饰的共享变量值时,都是最新的值。

你可能感兴趣的:(JUC,java)