一、可见性问题

可见性问题是指由于CPU缓存,一个线程对主存内容修改,另一个线程对修改的内容不可见的情况。

  1. public class TestSeeing {
  2. static boolean run =true;
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t = new Thread(()->{
  5. while(run){
  6. //...
  7. }
  8. });
  9. t.start();
  10. Thread.sleep(1000);
  11. System.out.println("停止t");
  12. run = false;//线程不会预期停止下来
  13. }
  14. }

主线程改变t线程的循环条件,并不会使得t线程停止,原因如下:
image.png

image.png

image.png

这里的主内存实际是JVM中的方法区,存放共享静态变量,而工作内存指的是CPU缓存。
区分好这里的CPU缓存与线程的栈区内存。CPU缓存中存储的是一种优化策略后的值,而这些值本来存放的位置位于主内存中,只不过由于频繁读取放在缓存中要更方便。而线程的栈区内存也是主内存的一部分,但实际在JVM中是每个线程独有的栈区内存。

注意:

1、【主内存】vs 【CPU缓存】 与【主内存中的共享方法区】 vs 【主内存中线程独有的栈区】,这是两组不同的概念。

2、多线程的可见性问题,引发的原因是:某个线程读取值从cache缓存中读值,而不是从主内存中读值,与写值没关系,写值都是重新写回主存,没有写进cache中的情况,试想如果程序存在写回cache中的情况,那么程序的正确性将难以保证。 image.png

二、可见性问题解决方案

1、volatile关键字

volatile是指易变关键字,它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。

2、synchronized加锁

Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存中,接着在主内存中拷贝最新变量的副本到工作内存,在执行完代码后,会将更改后的共享变量值刷新到主内存中,最后再释放互斥锁。但此种方法语法较为复杂,属于重量级操作,不推荐使用。

三、可见性 vs 原子性

可见性保证的是多个线程之间,一个线程对volatile变量的修改对另一个线程是可见的,但volatile只能保证每次访问变量都是最新值,但是无法解决原子性问题,仅用在一个写线程,多个读线程的情况:
上例从字节码角度理解是这样的:

  1. getstatic run //线程t获取 run true
  2. getstatic run //线程t获取 run true
  3. getstatic run //线程t获取 run true
  4. getstatic run //线程t获取 run true
  5. putstatic run //线程 main 获取 run 为 flase,仅此一次
  6. getstatic run //线程t 获取 run flase

比较一下之前我们将线程安全时举的例子:两个线程一个i++ 一个i—,只能保证看到最新值,但是不能解决指令交错问题
所以综上:volatile修饰变量只能解决可见性、无法解决原子性

注意:

synchronized 语句块既可以保证代码的原子性、也同时保证代码块内变量的可见性。但缺点是synchronized属于重量级操作,性能相对较低。