文章目录
- 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;
}
}
最后我们创建的线程会陷入死循环,按照正常的逻辑去理解,在主线程修改flag后创建的线程会从主存空间读取到主线程的修改然后退出while循环,而出现这种结果的原因是jvm中JIT的存在,JIT即java即时编译技术,它底层又c1和c2两个编译器组成(新版本可能是c1和Graal),c2或Graal会利用一些技术对我们的代码进行优化(这会导致一些问题)。再分析一下上面代码工作的底层流程:
- 初始状态,t线程刚开始从主内存中读取了flag的值到工作内存中
- 每一次新的while循环,t线程都要从主内存中读取flag的值,这是一个十分耗时的过程,所以JIT这里就对我们代码进行了优化,JIT会将flag的缓存至线程自己的工作内存中的高速缓存中,减少对flag的主存访问,提高程序运行效率。
3. 主线程更新flag时,main线程将对flag的更新更新到主存中的共享变量,而t线程还是使用自己缓存中的旧值。
最后就导致了可见性问题。
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;
}
}
这就解决了可见性问题,synchronized同样也可以解决可见性,但解决可见性还是推荐使用volatile,因为synchronized是重量级锁,消耗是很大的。但相比于volatile,sychronized也有其优势,sychronized语句块既可以保证代码的原子性,也同时可以保证代码块内变量的可见性。volatile是不能保证代码的原子性,它只能保证代码我们每次读到volatile修饰的共享变量值时,都是最新的值。