JMM就使用happens-before的概念来阐述多线程之间的内存可见性
1. 为什么就会存在可见性问题?
相对于内存,CPU的速度是极高的,如果CPU需要存取数据时都直接与内存打交道,在存取过程中,CPU将一直空闲,这是一种极大的浪费,所以,现代的CPU里都有很多寄存器,多级cache,他们比内存的存取速度高多了。某个线程执行时,内存中的一份数据,会存在于该线程的工作存储中(working memory,是cache和寄存器的一个抽象,这个解释源于《Concurrent Programming in Java: Design Principles and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有不少人觉得working memory是内存的某个部分,这可能是有些译作将working memory译为工作内存的缘故,为避免混淆,这里称其为工作存储,每个线程都有自己的工作存储),并在某个特定时候回写到内存。单线程时,这没有问题,如果是多线程要同时访问同一个变量呢?内存中一个变量会存在于多个工作存储中,线程1修改了变量a的值什么时候对线程2可见?此外,编译器或运行时为了效率可以在允许的时候对指令进行重排序,重排序后的执行顺序就与代码不一致了,这样线程2读取某个变量的时候线程1可能还没有进行写入操作呢,虽然代码顺序上写操作是在前面的。这就是可见性问题的由来。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
2. happens-before原则定义
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则规定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
3. happens-before原则规则
-
程序次序规则
一个线程内,按代码顺序,在前面的操作先行发生于在后面的操作;
一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。
-
锁定规则
一个unLock操作先行发生于后面对同一个锁的lock操作;
如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。
-
volatile变量规则
对一个变量的写操作先行发生于后面对这个变量的读操作;
如果线程1写入了volatile变量v(这里和后续的“变量”都指的是对象的字段、类字段和数组元素),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)。
-
传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
体现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C
-
线程启动规则
Thread对象的start()方法先行发生于此线程的每个一个动作;
假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。
线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;线程终结规则
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
线程t1写入的所有变量(所有action都与那个join有happens-before关系,当然也包括线程t1终止前的最后一个action了,最后一个action及之前的所有写入操作,所以是所有变量),在任意其它线程t2调用t1.join()成功返回后,都对t2可见。
-
对象终结规则
一个对象的初始化完成先行发生于它的finalize()方法的开始。
4. 推导出其他满足happens-before的规则
- 将一个元素放入一个线程安全队列的操作Happens-Before从队列中取出这个元素的操作
- 将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作
- 在CountDownLatch上的倒数操作Happens-Before CountDownLatch#await()操作
- 释放Semaphore许可的操作Happens-Before获得许可操作
- Future表示的任务的所有操作Happens-Before Future#get()操作
- 向Executor提交一个Runnable或Callable的操作Happens-Before任务开始执行操作
如果两个操作不存在上述(前面8条 + 后面6条)任一一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的。
5. 简单的例子
private int i = 0;
public void write(int j ){
i = j;
}
public int read(){
return i;
}
(1) 我们约定线程A执行write(),线程B执行read(),且线程A优先于线程B执行,那么线程B获得结果是什么?;我们就这段简单的代码一次分析happens-before的规则(规则5、6、7、8 + 推导的6条可以忽略,因为他们和这段代码毫无关系):
- 由于两个方法是由不同的线程调用,所以肯定不满足程序次序规则;
- 两个方法都没有使用锁,所以不满足锁定规则;
- 变量i不是用volatile修饰的,所以volatile变量规则不满足;
- 传递规则肯定不满足;
(2) 我们无法通过happens-before原则推导出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,但是就是无法确认线程B获得的结果是什么,所以这段代码不是线程安全的。那么怎么修复这段代码呢?满足规则2、3任一即可。
(3) 修复代码 - 满足规则2
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
private int i = 0;
private Lock lock = new ReentrantLock();
public void write(int j) {
try {
lock.lock();
i = j;
} finally {
lock.unlock();
}
}
public int read() {
try {
lock.lock();
return i;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Demo demo = new Demo();
Thread t1 = new Thread(() -> {
demo.write(200);
}, "线程t1");
Thread t2 = new Thread(() -> {
try {
// 睡眠1秒,让线程t1先执行write, 然后线程t2才执行read
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int readValue = demo.read();
System.out.println(Thread.currentThread().getName() + " 读取到值: " + readValue);
}, "线程t2");
t1.start();
t2.start();
}
}
结果:
线程t2 读取到值: 200
- 满足规则3
package com.example.myproject.happensBefore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo2 {
private volatile int i = 0;
public void write(int j) {
i = j;
}
public int read() {
return i;
}
public static void main(String[] args) {
Demo2 demo = new Demo2();
Thread t1 = new Thread(() -> {
demo.write(200);
}, "线程t1");
Thread t2 = new Thread(() -> {
try {
// 睡眠1秒,让线程t1先执行write, 然后线程t2才执行read
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int readValue = demo.read();
System.out.println(Thread.currentThread().getName() + " 读取到值: " + readValue);
}, "线程t2");
t1.start();
t2.start();
}
}
结果:
线程t2 读取到值: 200
参考文献
参考书籍
1.《Java Memory Model》、
2.《Java Concurrency in Practice》、
3.《Concurrent Programming in Java: Design Principles and Patterns》