再说线程可见性之前,先来说一下主内存和本地内存的关系,两者是造成线程可见性的关键原因
1. 主内存和本地内存
JMM有以下规定:
- 所有的全局变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成
所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
2. 什么是happens-before(两种解释,一种意思)
-
happens- before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A ,这就是happens-before.
-
两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。
通过这个规则,可以解决线程间的可见性问题
3. 可见性
指当多个线程访问同一个变量时,如果其中一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。但是在某些情况下,并不能保证线程间的可见性。如下代码:
/**
* 描述: 演示可见性带来的问题
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("a=" + a + ";b=" + b);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
运行结果如下:
- a=3;b=3
- a=1,b=2;
- a=3;b=2
a=1;b=3
出现 a=1;b=3
的原因便是线程的可见性。修改线程
对全局变量的操作不是在主内存中进行的。他会先将数据读入其本地内存中,然后再修改。修改后再将数据同步到主内存中。这时就会出现线程可见性的问题。在本地内存中对a 与 b修改后a还未来得及同步到主内存中,读线程
就开始执行,此时 a 还是原来的数据.便会出现 a=1;b=3
情况
利用happens-before原则便可解决 线程可见性问题
4. volatile关键字
解释: 使变量在多个线程间可见
是什么:
-
volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
-
如果一个变量别修饰成volatile ,那么JVM就知道了这个变量可能会被并发修改。
-
但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护, volatile仅在很有限的场景下才能发挥作用。
作用:
- 保证可见性
- 不保证原子性 :程序中的所有操作是不可中断的,要么全部执行成功要么全部执行失败
- 禁止指令重排
可见性:
写入volatile变量之后,会立即将其同步到主内存中。再读之前,会先使本地缓存失效,然后强制从主内存中读取。这样便可保证可见性
禁止指令重排
解释: 为了提高程序运行效率,编译器可能会对输入指令进行重新排序,即程序中各个语句的执行先后顺序同代码中的顺序不一定一致。(但是它会保证单线程程序最终执行结果和代码顺序执行的结果是一致的,它忽略了数据的依赖性)
源代码 -> 编译器优化重排 -> 指令并行重排 -> 内存系统重排 -> 最终执行指令
volatile能够实现禁止指令重排的底层原理:
-
内存屏障(Memory Barrier):它是一个CPU指令。由于编译器和CPU都能够执行指令重排,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,任何指令都不能和该条Memory Barrier指令进行重排序,即通过插入内存屏障指令能够禁止在内存屏障前后的指令执行重排序 优化
-
内存屏障的另外一个作用是强制刷新各种CPU的缓存数据
(将本地内存的数据刷新同步到主内存中)
,因此任何CPU上的线程都能够读取到这些数据的最新版本。以上两点正好对应了volatile关键字的禁止指令重排序和内存可见性的特点
-
对volatile变量进行写操作时,会在写操作之后加入一条store屏障指令,将工作内存中的共享变量copy刷新回主内存中;对volatile变量进行读操作时,会在读操作之前加入一条load的屏障指令,从主内存中读取共享变量
适用场合:
- 不适用: a++
- 适用场合
- boolean flag ,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
- 作为触发器,实现轻量级同步。
代码展示;使用volatile保证线程的可见性