JMM即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着cpu寄存器、缓存、硬件内存、cpu指令优化等。
JMM 体现在以下几个方面:
cpu 缓存的影响。cpu 指令并行优化的影响。演示:
main线程对 flag 变量的修改对于 t1 线程不可见,导致了 t1 线程无法停止。
public class VisibilitySample {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
// ignore...
}
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
// 重新赋值成员变量。
flag = false;
// 一直运行...
}
}

初始状态, t1 线程刚开始从主内存读取了 flag 的值到工作内存。
因为 t1 线程要频繁从主内存中读取 flag 的值,JIT 编译器会将 flag 的值缓存至自己工作内存中的高速缓存中,减少对主存中 flag 的访问,提高效率。
等待1秒后,main 线程修改了 flag 的值,并同步至主存,而 t1 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值。
volatile(易变关键字),它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。// 使用 volatile 修饰后,变量操作对其他线程可见。
private static volatile boolean flag = true;
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况。
注意 :synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。


cpu 取数据过程:当 cpu 需要访问内存中某个数据的时候,如果寄存器有这个数据,cpu 就直接从寄存器取数据即可,如果寄存器没有这个数据,cpu 就会查询 L1 高速缓存,如果 L1 没有,则查询 L2 高速缓存,L2 还是没有的话就查询 L3 高速缓存,L3 依然没有的话,才去内存中取数据。
速度比较:
| 从 cpu 到 | 大约需要的时钟周期 |
|---|---|
| 寄存器 | 1 cycle |
| L1 | 3~4 cycle |
| L2 | 10~20 cycle |
| L3 | 40~45 cycle |
| 内存 | 120~240 cycle |
内存屏障(Memory Barrier / Memory Fence):也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是
cpu或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

Balking (犹豫)模式:用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做了,直接结束返回。
@Slf4j
public class BalkingSample {
private volatile boolean runnning;
public void start() {
log.debug("try start monitor-thread");
synchronized (this) {
if (runnning) {
return;
}
runnning = true;
}
log.debug("thread is start? ({})", runnning);
log.debug("{} doSomething...", Thread.currentThread().getName());
}
public static void main(String[] args) {
BalkingSample balkingSample = new BalkingSample();
new Thread(balkingSample::start, "t1").start();
new Thread(balkingSample::start, "t2").start();
// [t2] try start monitor-thread
// [t1] try start monitor-thread
// [t2] thread is start? (true)
// [t2] t2 doSomething...
}
}
public class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static synchronized Singleton getInstance() {
if (null != INSTANCE) {
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
JVM会在不影响正确性的前提下,可以调整语句的执行顺序,这种特性称之为指令重排。
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
cpu 执行时间,即我们前面提到的 user + system 时间(可以用下面的公式来表示:)。程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的
cpu指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段。
IF );ID );EX );MEM );WB )。
// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
现代
cpu支持多级指令流水线,例如支持同时执行取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回的处理器,就可以称之为五级指令流水线。

cpu 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1。超标量(superscalar)
cpu架构是指在一颗处理器内核中实行了指令级并行的一类并行运算。这种技术能够在相同的cpu主频下实现更高的cpu吞吐率(throughput)。
cpu 可以在一个时钟周期内,执行多于一条指令,IPC > 1。
此处使用
jcstress(The Java Concurrency Stress) 工具进行验证:它是一个实验工具和一套测试工具,用于帮助研究JVM、类库和硬件中并发支持的正确性。
<dependency>
<groupId>org.openjdk.jcstressgroupId>
<artifactId>jcstress-coreartifactId>
<version>0.3version>
dependency>
<dependency>
<groupId>org.openjdk.jcstressgroupId>
<artifactId>jcstress-samplesartifactId>
<version>0.3version>
dependency>
/* *
* @JCStressTest 该注解标记一个类为一个并发测试的类。
*
* @Outcome描述测试结果,并处理这个结果:
* 1.ACCEPTABLE 结果不一定会存在;
* 2.ACCEPTABLE_INTERESTING 和 ACCEPTABLE 差不多,唯一不一样的是,这个结果会在生成的报告中高亮;
* 3.FORBIDDEN 表示永远不应该出现的结果,若测试过程中有该结果,意味着测试失败;
* 4.UNKNOWN 没有评分,不使用。
*
* @State标记这个类是有状态的,有状态的意思是拥有数据,而且数据是可以被修改的。
*/
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "expected result")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "unexpected result!")
@State
public class ConcurrencyTest {
int num = 0;
volatile boolean ready = false;
/**
* actor1
*
* @param r I_Result 是一个对象,有一个属性 r1 用来保存结果。
*/
@Actor
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
jcstress 启动类路径:org.openjdk.jcstress.Main


volatile boolean ready = false;

volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对
volatile变量的写指令后会加入写屏障。- 对
volatile变量的读指令前会加入读屏障。
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
// ready 是 volatile 赋值带『写屏障』。
}
@Actor
public void actor1(I_Result r) {
// ready 是 volatile 读取值带『读屏障』。
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}

lock 指令来多核 cpu之间的可见性与有序性。以著名的 double-checked locking 单例模式为例。
public class Singleton {
/* *
* 注意:此处位未修饰变量可见性。
*/
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
getInstance() 才使用 synchronized 加锁,后续使用时无需加锁。if 使用了 instance 变量,是在同步块之外但在多线程环境下,上面的代码是有问题的。(getInstance() 方法对应的字节码如下)
jvm 会优化为:先执行 26,再执行 23,此时两个线程 t1,t2 对 getInstance() 方法执行。
1: getstatic 这行代码在 monitor 控制之外,它是可以越过 monitor 读取 instance 变量的值。instance 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效。private static volatile Singleton instance;
happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,
JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。
线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见。
@Slf4j
public class HappensBeforeSample {
private static int x;
private static Object m = new Object();
public static void main(String[] args) {
// 写线程。
new Thread(() -> {
synchronized (m) {
x = 10;
}
}, "t1").start();
// 读线程。
new Thread(() -> {
synchronized (m) {
log.debug("x={}", x);
}
}, "t2").start();
}
}
线程对
volatile变量的写,对接下来其它线程对该变量的读可见。
private static volatile int x;
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用
t1.isAlive()或t1.join()等待它结束)。
@Slf4j
public class HappensBeforeSample {
private static int x;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
x = 10;
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("x={}", x);
}, "t2");
t1.start();
// 等待写线程执行结束。
t1.join();
t2.start();
// x=10
}
}
线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过
t2.interrupted()或t2.isInterrupted())。
@Slf4j
public class HappensBeforeSample {
private static int x;
public static void main(String[] args) throws InterruptedException {
Thread t2 = new Thread(() -> {
while (true) {
// 打断后读最新值。
if (Thread.currentThread().isInterrupted()) {
log.debug("x={}", x);
break;
}
}
}, "t2");
t2.start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
// 打断前对变量进行修改。
x = 10;
t2.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
while (!t2.isInterrupted()) {
// yield() 提示线程调度器让出当前线程对 cpu 的使用。
Thread.yield();
}
log.debug("x={}", x);
// [main] x=10
// [t2] x=10
}
}
线程内的操作都会同步到主存中 ,具有传递性,配合
volatile的防指令重排。
@Slf4j
public class HappensBeforeSample {
private volatile static int x;
private static int y;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
y = 10;
x = 20;
}, "t1").start();
new Thread(() -> {
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见。
log.debug("x={}", x);
}, "t2").start();
// [t2] x=20
}
}
start() 前对变量的写,对该线程开始后对该变量的读可见。false,null)的写,对其它线程对该变量的读可见。“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。