线程安全-Volatile关键字
Volatile关键字的作用是:保证可见性。禁止指令重排序。
字数很多,仔细阅读,从一个初学者,第一次接触volatile视角解释理解该概念。相信对你有所帮助
Volatile关键字的作用是:保证可见性。禁止指令重排序。
保证可见性
- 当一个线程修改了
volatile变量的值,其他线程能立即看到最新值boolean flag = false; new Thread(() -> { try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } flag = true; System.out.println("线程1启动"); }).start(); new Thread(() -> { try { Thread.sleep(200); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("线程2启动:" + flag); }).start(); new Thread(() -> { int i = 0; while (!flag) { i++; } System.out.println("线程3停止:"+i); }).start(); }上面有三个线程,定义了一个变量(没有volatile修饰)。运行结果如下:

可以看到线程1启动了,并且将flag的值修改为true,线程2得到了修改后的flag的值:true。
但是线程3的flag仍然是flase。这是为什么呢?
解释:因为线程1在休眠100毫秒之后修改flag的值为true,线程2在休眠200毫秒之后的会重新刷新自己线程的工作内存,得到了就是新的flag。但是线程3并没有休眠,所以它拿到的就是最开始的flag,并且将最开始的flag值放到自己的工作内存中,并不知道线程1已经修改了。
ps:JMM(java内存模型)规定了线程有自己的工作内存和共享内存,自己的工作内存是其他线程不可见的,而共享内存是所有线程可见的。在操作数据的时候,会首先将共享内存中的数据拷贝到自己的私有内存中然后再操作。
这里可能有一个概念JIT,即时编译器。JIT会对重复执行的代码进行优化,将其升级为局部变量,升级为机器码。这样就变成了死循环。但并不能说是因为JIT导致了变量不可见问题,而是JIT让这个问题变得明显了,主要是因为线程内存的独立性。
禁止指令重排序
首先我们解释一下为什么出现指令重排序问题。这是一种优化,Java 内存模型(JMM)允许编译器和处理器对指令进行重排序以优化性能。可以看一下代码,下面就是重排序,在单线程情况下没有任何影响,在一些情况下,重排序执行可以优化我们的代码执行效率。
int a= 1; ①
int b =2; ②
int c= 3; ③
// 上面三行代码, 如果按顺序执行是①②③。实际上我们的代码可能是先执行② - ③- ①
// 虽然顺序变了,但是结果并没有任何影响
但是在多线程情况下就不一样了。
看一下下面的代码,线程A和线程B并行执行。线程A执行的时候发生了重排序,我们以为的执行顺序是 a= 1; b =2 ; c= 3 (1-2-3); 实际顺序变成了 c = 3; a =1 ;b =2;(3-1-2)。当线程A顺序变成了(3-1-2)的时候,c=3赋值完成之后,线程B也执行到了if(c==3) 这样就得到结果为true,而这个时候a,b还没赋值呢。最终输出的结果就是0,0(默认值)。
int a = 0;
int b = 0;
int c = 0;
// 线程 A
public void writer() {
a = 1; // (1)
b = 2; // (2)
c = 3; // (3)
}
// 线程 B
public void reader() {
if (c == 3) { // 读
System.out.println(a); // 0
System.out.println(b); // 0
}
}
出现了上面这情况是我们想避免的,这个时候就需要使用volatile关键字了。这个关键字可以让线程B执行到c==3的时候,a,b一定是赋值的状态。解释一下:
volatile关键字并不能阻止重排序,我们使用了volatile关键字之后,并不是把顺序执行强行修改为了(1-2-3),线程A中的代码执行顺序可能还是(3-1-2)。这个关键字的作用是当线程B执行到(c==3)的时候,a和b一定是赋值完成的状态。也就是说线程A,线程B同时执行,假设线程A执行顺序是3-1-2,那么执行完c=3的时候,线程B也无法得到c==3,必须等Volatile关键字修饰的变量之前的写操作都完成之后(也就是a,b都赋值之后),线程B才能得到c==3。这样就能保证线程B执行的时候ab都是有值的。
--------------------使用volatile---------------------
int a = 0;
int b = 0;
volatile int c = 0;
// 线程 A
public void writer() {
a = 1; // (1)
b = 2; // (2)
c = 3; // (3)
}
// 线程 B
public void reader() {
if (c == 3) { // 读
System.out.println(a); // 2
System.out.println(b); // 3
}
}
底层逻辑:为什么线程A已经给c赋值了,但是线程B无法得到c==3的值呢?
JVM 会在 c = 3 之前插入 StoreStore 屏障,确保:
a = 1和b = 2的写操作 先于c = 3刷入主存或缓存;- CPU 不会把
c = 3的写入“提前”让其他核心看到,而a、b还在写缓冲区(Store Buffer)里。
缓存一致性协议保证原子可见性
在多核 CPU 中:
- 线程 A 所在的核心在执行
volatile写时,会触发缓存行的 flush 和 invalidate 广播; - 但这个广播只有在所有前置写操作完成后才发出;
- 所以线程 B 所在的核心不可能先看到
c=3,却看不到a=1或b=2。
也就是说即使c先执行完了,因为加了一个屏障,其他线程看不到,必须等a,b都执行完了其他线程才能看到。

图片中讲的:写操作加的屏障是阻止上方其他写操作越过屏障排序到volatile变量之下。这里说的是逻辑上的不能,只要别的线程看不到volatile修饰的变量先赋值,那么就是其他写操作在Volatile写操作之上。但是底层物理写还是会重排序的。
而“读操作加的屏幕是阻止下方其他写操作越过屏障操作到volatile变量读之上”。这句话的意思,如果你在读取的时候,需要Volatile的禁止重排序读,那么就必须先读取c(volatile修饰的)且建立因果关系,然后在读取a,b,这样获取到的a,b才是有值的,否则a,b可能是0,0。
为什么必须建立因果关系?可以参考线程C,虽然线程C先读取了c再读取a,b。但是可能存在线程A还没赋值给c,这个时候线程C执行了,那么这个时候的c就等于0。a,b也是等于0。
而带了条件判断(因果关系),就只有当c==3的时候,那么a,b一定有值且值正确。
int a = 0;
int b = 0;
volatile int c = 0;
// 线程 A
public void writer() {
a = 1; // (1)
b = 2; // (2)
c = 3; // (3)
}
// 线程 B
public void reader() {
if(c==3){
System.out.println(b);
System.out.println(a);
}
}
-------------------------
// 线程 C
public void reader() {
System.out.println(c);
System.out.println(b);
System.out.println(a);
}
}
更多推荐


所有评论(0)