文章目录
- 1.什么是可见性问题
- 2.为什么会有可见性问题
- 3.JMM的抽象:主内存和本地内存
- 3.1 什么是主内存和本地内存
- 3.2 主内存和本地内存的关系
- 4.Happens-Before原则
- 4.1 什么是Happens-Before
- 4.2 什么不是Happens-Before
- 4.3 Happens-Before规则有哪些
- 4.4 演示:使用volatile修正可见性问题
- 5.volatile关键字
- 5.1 volatile是什么
- 5.2 volatile的适用场合
- 5.3 volatile的两点作用
- 5.4 volatile和synchronized的关系
- 5.5 volatile小结
- 6.能保证可见性的措施
- 7.升华:对synchronized可见性的正确理解
1.什么是可见性问题
首先来看第一个代码案例,演示什么是可见性问题。
/**
* 演示可见性带来的问题
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b = " + b + ", a = " + a);
}
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();
}
}
}
关于上述程序的运行结果,我们可以很容易分析得到如下三种情况:
- b = 2, a = 3
- b = 2, a = 1
- b = 3, a = 3
然而,在实际运行过程中,还有可能会出现第四种情况(概率低),即 b = 3, a = 1。这是因为 a 虽然被修改了,但是其他线程不可见,而 b 恰好其他线程可见,这就造成了 b = 3, a = 1。
2.为什么会有可见性问题
接下来,尝试分析第二个案例。
至此,解答一个问题:为什么会有可见性问题?
- CPU有多级缓存,导致读的数据过期。
- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层。
- 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
- 如果所有的核心都只用一个缓存,那么也就不存在内存可见性问题了。
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
3.JMM的抽象:主内存和本地内存
3.1 什么是主内存和本地内存
Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。
3.2 主内存和本地内存的关系
JMM有以下规定:
- 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝。
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。
总结:所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
4.Happens-Before原则
4.1 什么是Happens-Before
下面的两种解释其实是一种意思。
Happens-Before规则是用来解决可见性问题的:在时间上,动作 A 发生在动作 B 之前,B 保证能看见 A,这就是Happens-Before。
两个操作可以用Happens-Before来确定它们的执行顺序:如果一个操作Happens-Before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。
4.2 什么不是Happens-Before
两个线程没有相互配合的机制,所以代码 X 和 Y 的执行结果并不能保证总被对方看到的,这就不具备Happens-Before。
4.3 Happens-Before规则有哪些
(1) 单线程规则
(2) 锁操作(synchronized和Look)
(3) volatile变量
(4) 线程启动
(5) 线程join
(6) 传递性
传递性:如果 hb(A,B) 而且 hb(B,C),那么可以推出 hb(A,C)。
(7) 中断
中断:一个线程被其他线程 interrupt 时,那么检测中断(isInterrupted)或者抛出 InterruptedException 一定能看到。
(8) 构造方法
构造方法:对象构造方法的最后一行指令Happens-Before于 finalize() 方法的第一行指令。
(9) 工具类的Happens-Before原则
- 线程安全的容器get一定能看到在此之前的put等存入动作
- CountDownLatch
- Semaphore
- Future
- 线程池
- CyclicBarrier
4.4 演示:使用volatile修正可见性问题
Happens-Before有一个原则是:如果 A 是对 volatile 变量的写操作,B 是对同一个变量的读操作,那么 hb(A,B)。
根据上面的原则,可以使用 volatile 关键字解决本文开头第一个案例的可见性问题。
/**
* 使用volatile关键字解决可见性问题
*/
public class FieldVisibility {
int a = 1;
volatile int b = 2; // 只给b加volatile即可
// writerThread
private void change() {
a = 3;
b = a; // 作为刷新之前变量的触发器
}
// readerThread
private void print() {
System.out.println("b = " + b + ", a = " + a);
}
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();
}
}
}
这里体现了 volatile 的一个很重要的功能:近朱者赤。给 b 加了 volatile,不仅 b 被影响,也可以实现轻量级同步。
b 之前的写入(对应代码b=a)对读取 b 后的代码(print b)都可见,所以在 writerThread 里对 a 的赋值,一定会对 readerThread 里的读取可见,所以这里的 a 即使不加 volatile,只要 b 读到是 3,就可以由Happens-Before原则保证了读取到的都是 3 而不可能读取到 1。
5.volatile关键字
5.1 volatile是什么
volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改。
但是开销小,相应的能力也小,虽然说volatile是用来同步地保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。
5.2 volatile的适用场合
(1) 不适用于a++
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile的不适用场景
*/
public class NoVolatile implements Runnable {
volatile int a;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile) r).a);
System.out.println(((NoVolatile) r).realA.get());
}
}
(2) 适用场景一
如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。例如,boolean flag 操作。
注意:volatile 适用的关键并不在于 boolean 类型,而在于和之前的状态是否有关系。
在下面的程序中,setDone() 的时候,done 变量只是被赋值,而没有其他的操作,所以是线程安全的。
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile的适用场景
*/
public class UseVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new UseVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((UseVolatile) r).done);
System.out.println(((UseVolatile) r).realA.get());
}
}
在下面的程序中,虽然 done 变量是 boolean 类型的,但 flipDone() 的时候,done 变量取决于之前的状态,所以是线程不安全的。
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile的不适用场景
*/
public class NoUseVolatile implements Runnable {
volatile boolean done = false;
AtomicInteger realA = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
flipDone();
realA.incrementAndGet();
}
}
private void flipDone() {
done = !done;
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoUseVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoUseVolatile) r).done);
System.out.println(((NoUseVolatile) r).realA.get());
}
}
(3) 适用场景二
作为刷新之前变量的触发器。
5.3 volatile的两点作用
可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
禁止指令重排序优化:解决单例双重锁乱序问题。
5.4 volatile和synchronized的关系
volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
5.5 volatile小结
- volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如 boolean flag 或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
- volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取。
- volatile提供了happens-before保证,对volatile变量 v 的写入happens-before所有其他线程后续对 v 的读操作。
- volatile可以使得long和double的赋值是原子的。关于long和double的原子性,可以参考这篇文章。
6.能保证可见性的措施
除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join() 和 Thread.start() 等都可以保证可见性。
具体看上述happens-before原则的规定。
7.升华:对synchronized可见性的正确理解
synchronized不仅保证了原子性,还保证了可见性。
synchronized不仅让被保护的代码安全,还近朱者赤。