一、可见性问题
可见性问题是指由于CPU缓存,一个线程对主存内容修改,另一个线程对修改的内容不可见的情况。
public class TestSeeing {
static boolean run =true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
//...
}
});
t.start();
Thread.sleep(1000);
System.out.println("停止t");
run = false;//线程不会预期停止下来
}
}
主线程改变t线程的循环条件,并不会使得t线程停止,原因如下:
这里的主内存实际是JVM中的方法区,存放共享静态变量,而工作内存指的是CPU缓存。
区分好这里的CPU缓存与线程的栈区内存。CPU缓存中存储的是一种优化策略后的值,而这些值本来存放的位置位于主内存中,只不过由于频繁读取放在缓存中要更方便。而线程的栈区内存也是主内存的一部分,但实际在JVM中是每个线程独有的栈区内存。
注意:
1、【主内存】vs 【CPU缓存】 与【主内存中的共享方法区】 vs 【主内存中线程独有的栈区】,这是两组不同的概念。
2、多线程的可见性问题,引发的原因是:某个线程读取值从cache缓存中读值,而不是从主内存中读值,与写值没关系,写值都是重新写回主存,没有写进cache中的情况,试想如果程序存在写回cache中的情况,那么程序的正确性将难以保证。
二、可见性问题解决方案
1、volatile关键字
volatile是指易变关键字,它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
2、synchronized加锁
Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存中,接着在主内存中拷贝最新变量的副本到工作内存,在执行完代码后,会将更改后的共享变量值刷新到主内存中,最后再释放互斥锁。但此种方法语法较为复杂,属于重量级操作,不推荐使用。
三、可见性 vs 原子性
可见性保证的是多个线程之间,一个线程对volatile变量的修改对另一个线程是可见的,但volatile只能保证每次访问变量都是最新值,但是无法解决原子性问题,仅用在一个写线程,多个读线程的情况:
上例从字节码角度理解是这样的:
getstatic run //线程t获取 run true
getstatic run //线程t获取 run true
getstatic run //线程t获取 run true
getstatic run //线程t获取 run true
putstatic run //线程 main 获取 run 为 flase,仅此一次
getstatic run //线程t 获取 run flase
比较一下之前我们将线程安全时举的例子:两个线程一个i++ 一个i—,只能保证看到最新值,但是不能解决指令交错问题
所以综上:volatile修饰变量只能解决可见性、无法解决原子性
注意:
synchronized 语句块既可以保证代码的原子性、也同时保证代码块内变量的可见性。但缺点是synchronized属于重量级操作,性能相对较低。