并发编程的三大特性:原子性、有序性和可见性,这三大特性也是并发编程bug产生的根源。今天我们聊一聊其中的可见性问题。
可见性定义
可见性是指当一个线程对共享变量的值进行变更,变更后的值能对其他线程可见。
可见性问题的产生
可见性问题是如何产生的呢?这需要从两方面来回答,一方面从计算机体系结构去解释,另一方面从JVM的内存逻辑划分解释。
从计算机体系结构来讲,当前计算机大都源自于冯诺依曼架构,冯诺伊曼架构约定了五大部件,即
- 运算器
- 控制器
- 存储器
- 输入设备
- 输出设备
现在看来,运算器和控制器单元集成在CPU中实现,存储器的容量不断扩大、输入输出设备不断更新,这些部件构成了当代计算机硬件系统的基本组成。随着计算机体系结构和性能的不断优化,计算机的存储系统已形成一个分层多级的存储结构。
寄存器和高速缓存一般封装于cpu内部速度快但价格贵,下层的存储设备价格便宜但速度慢,于是用少量的价格构成的一个分层存储体系,将高速设备当做是低速设备的缓存,来降低访问速度差异过大造成的性能开销,从而形成了一个多级缓存的结构。但是有缓存,就有数据一致性问题,上层的存储器依赖的是他直接下层而非源数据,故而就存在这种不一致造成的数据一致性性问题也就是我们常说的可见性问题。
从JVM层面来讲,Java使用的是共享内存的并发模型,JVM线程和系统线程1:1绑定。而JVM逻辑上又把内存分为主存,和工作内存,那么线程各自的工作内存就是互相隔离的共享的只能是主存,工作内存中的数据是主存中数据的一个副本,可以理解为工作内存对主存数据有一份缓存,线程使用的就是缓存副本,有缓存就会有数据一致性问题(更新可见性问题)。
可见性的保证
Java中有哪些常见的手段保证可见性呢?我们通过一些常见的例子看看Java保证可见性的一些方式。以下代码,是在测试可见性时很常用的例子,如果不做任何额外处理,将会一直无法停止。
@Slf4j
public class VisibilityDemo {
private boolean flag = true;
private int count = 0;
public void refresh() {
flag = false;
log.info("modify flag to:{}", flag);
}
public void load() {
log.info("start to execute");
while (flag) {
count++;
}
log.info("break from loop, count={}", count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::load, "Thread-A").start();
Thread.sleep(1000);
new Thread(demo::refresh, "Thread-B").start();
}
}
volatile
volatile是Java中的轻量级同步方案,保证了共享变量在线程之间的可见性,以及禁止指令重排序。底层借助的是内存屏障实现。我们发现volatile修饰flag或者count都能保证可见性【这里搞清楚volatile的原理就不难理解】
@Slf4j
public class VisibilityDemo {
//v1
private volatile boolean flag = true;
private int count = 0;
//v2 这两个版本都能保证flag变量的可见性
// private boolean flag = true;
// private volatile int count = 0;
public void refresh() {
flag = false;
log.info("modify flag to:{}", flag);
}
public void load() {
log.info("start to execute");
while (flag) {
count++;
}
log.info("break from loop, count={}", count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::load, "Thread-A").start();
Thread.sleep(1000);
new Thread(demo::refresh, "Thread-B").start();
}
}
synchronized
synchronized是Java的同步原语关键字,保证有序性原子性及可见性。底层基于管程(Monitor)实现同时也依赖于内存屏障
@Slf4j
public class VisibilityDemo {
// private volatile boolean flag = true;
private boolean flag = true;
// private volatile int count = 0;
private int count = 0;
public void refresh() {
flag = false;
log.info("modify flag to:{}", flag);
}
public synchronized void load() {
log.info("start to execute");
while (flag) {
count++;
System.out.println(count);//隐式synchronized,内部使用到了synchronized
}
log.info("break from loop, count={}", count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::load, "Thread-A").start();
Thread.sleep(1000);
new Thread(demo::refresh, "Thread-B").start();
}
}
Lock
Lock同synchronized类似,是Java语言实现的同步框架。底层借助volatile保证有序性和可见性,借助CAS来实现同步状态变更的原子性。
@Slf4j
public class VisibilityDemo {
// private volatile boolean flag = true;
private boolean flag = true;
// private volatile int count = 0;
private int count = 0;
private Lock lock = new ReentrantLock();
public void refresh() {
lock.lock();
flag = false;
lock.unlock();
log.info("modify flag to:{}", flag);
}
public synchronized void load() {
log.info("start to execute");
boolean continues = true;
while (continues) {
lock.lock();
continues = flag;
lock.unlock();
count++;
}
log.info("break from loop, count={}", count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::load, "Thread-A").start();
Thread.sleep(1000);
new Thread(demo::refresh, "Thread-B").start();
}
}
内存屏障
内存屏障利用处理器架构的数据一致性协议,x86架构中使用lock前缀指令,利用MESI协议以及更兜底的总线锁定来保证数据一致性(可见性)
@Slf4j
public class VisibilityDemo {
// private volatile boolean flag = true;
private boolean flag = true;
// private volatile int count = 0;
private int count = 0;
// private Lock lock = new ReentrantLock();
public void refresh() {
flag = false;
log.info("modify flag to:{}", flag);
}
public synchronized void load() {
log.info("start to execute");
while (flag) {
getUnsafe().storeFence(); //内存屏障
count++;
}
log.info("break from loop, count={}", count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::load, "Thread-A").start();
Thread.sleep(1000);
new Thread(demo::refresh, "Thread-B").start();
}
public static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
final语义
final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去,那么在其他线程就能看见final字段的值(无须同步)。底层大概率也是借助内存屏障实现(有不一样看法的小伙伴欢迎探讨)。
@Slf4j
public class VisibilityDemo {
// private volatile boolean flag = true;
private boolean flag = true;
// private volatile int count = 0;
private Integer count = 0; //内部的值是 private final int value;
// private Lock lock = new ReentrantLock();
public void refresh() {
flag = false;
log.info("modify flag to:{}", flag);
}
public synchronized void load() {
log.info("start to execute");
while (flag) {
count++;
}
log.info("break from loop, count={}", count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::load, "Thread-A").start();
Thread.sleep(1000);
new Thread(demo::refresh, "Thread-B").start();
}
}
上下文切换
操作系统进行线程上下文切换时,会转储寄存器上的数据,并使硬件上建立的状态失效(缓存失效),当重新获取到时间片执行时,会重新加载数据(新的状态就获取到了)
@Slf4j
public class VisibilityDemo {
// private volatile boolean flag = true;
private boolean flag = true;
// private volatile int count = 0;
private int count = 0;
// private Lock lock = new ReentrantLock();
public void refresh() {
flag = false;
log.info("modify flag to:{}", flag);
}
public synchronized void load() {
log.info("start to execute");
while (flag) {
count++;
Thread.yield();//让出时间片
}
log.info("break from loop, count={}", count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::load, "Thread-A").start();
Thread.sleep(1000);
new Thread(demo::refresh, "Thread-B").start();
}
<img src="}" alt="" width="70%" />
总结
从上述保证可见性的例子中我们可以发现,JVM层面保证可见性主要有两种手段:
- 基于内存屏障
- 缓存失效(上下文切换,或者其他方式引起的)