章
目
录
本文主要讲解关于Java并发编程中volatile关键字可见性问题的详解相关内容,让我们来一起学习下吧!
volatile关键字在java并发编程中非常重要,但也有很多同学搞不清它的具体作用及其原理,这篇文章主要讲解volatile的其中一个特性——保证可见性,并简单讲解下其中的原理,后期再将其深入的原理,以及禁止指令重排序问题。
代码案例
为了方便大家理解volatile保证可见性问题,我们先看下一个代码案例:
public class VolatileDemo {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
// 创建t线程
new Thread(()->{
while (run) {
// do something
// 这里做输出或不要打印日志,后面解释原因
}
},"t").start();
// 主线程休眠1s
Thread.sleep(1000);
// 将run置为false
run= false;
}
}
你觉得1s后,主线程将run置为false,t线程while判断会结束运行吗?正常逻辑,我们认为t线程结束循环,但事实是t线程已经陷入死循环,无法结束,而原因就是,主线程对run的修改对于t线程是不可见的,那么问题就来了,为什么不可见?主要原因在于每个线程都有自己独立的工作内存,而成员变量是存放在主内存中的,每个线程在运行时会将成员变量的副本拷贝一份进自己的工作内存,我们一起看下图解。
原因分析
我们来分析一下:
1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
2. 因为 t 线程要频繁从主内存中读取 run的值,JIT 编译器会将 run的值缓存至自己工作内存中的高速缓存中,减少对主存中 run的访问,提高效率
3. 1 秒之后,main主线程修改了 run的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决方法
使用volatile(易变关键字)修饰线程间共享变量,使之在线程之间可见。
volatile可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。
我们修改代码之后:
static volatile boolean run = true;
再去测试,发现1s后t线程成功结束了。
注意
在上面的代码中do something位置,我们说“这里做输出或不要打印日志”,只要你在里面调用了System.out.println或log.debug等,就会发现,t线程会停止,run变成可见的了,而原因就在于println的源码中有synchronized同步了:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
这也是下面要讲的。
synchronized关键字保证可见性
其实synchronized 关键字也可以保证可见性,我们看下代码:
public class VolatileDemo {
static boolean run = true;
// 锁对象
final static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 创建t线程
new Thread(()->{
while (true) {
synchronized (lock) {
if(!run) {
break;
}
// do something
}
}
},"t").start();
// 主线程休眠1s
Thread.sleep(1000);
// 将run置为false
synchronized (lock) {
run = false;
}
}
}
以上代码也可以实现1s后t线程结束运行。
但相比于 synchronized 关键字(重量级锁)对性能影响较大,更推荐使用 volatile 关键字保证可见性。由于使用 volatile 不会引起上下文的切换和调度,所以 volatile 对性能的影响较小,开销较低。
总结
以上就是Java并发编程volatile关键字可见性详解,至于为什么volatile能保证可见性,我们后面有机会再讲,希望本文对你有帮助。欢迎持续关注潘子夜个人博客(www.panziye.com),学习愉快哦!