深入拆解Java内存模型:从原子性、可见性到有序性,彻底搞懂happen-before规则
在Java并发编程中,Java内存模型(JMM)是最核心的概念之一。它不仅定义了线程与主内存之间的抽象关系,还为解决并发场景下的原子性、可见性、有序性问题提供了规范保障。理解JMM,是写出正确、高效并发代码的基础。
JMM将内存分为主内存(Main Memory)和工作内存(Working Memory)。主内存是所有线程共享的,存储了实例对象、静态变量等数据;而每个线程都有自己私有的工作内存,存储了该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
原子性:不可分割的操作
原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行,执行过程中不会被其他线程打断。
在Java中,对基本数据类型的读取和赋值操作通常是原子性的,但像count++这样的复合操作(读取-修改-写入)就不具备原子性。
示例:原子性问题
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
/**
* 原子性示例
*
* @author ken
*/
@Slf4j
public class AtomicityDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
int threadCount = 1000;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
count++;
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
log.info("count: {}", count);
}
}
这段代码启动1000个线程,每个线程对count进行1000次自增操作,预期结果是1000000,但实际运行结果往往小于这个值,因为count++是复合操作,不具备原子性。
保证原子性的方式
- synchronized关键字:通过管程(Monitor)机制保证同一时间只有一个线程能执行临界区代码。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
/**
* synchronized保证原子性示例
*
* @author ken
*/
@Slf4j
public class SynchronizedAtomicityDemo {
private static int count = 0;
private static synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
int threadCount = 1000;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
increment();
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
log.info("count: {}", count);
}
}
2. Lock接口:与synchronized类似,提供了更灵活的锁机制。 3. 原子类(java.util.concurrent.atomic) :基于CAS(Compare-And-Swap)操作实现无锁原子性。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* AtomicInteger保证原子性示例
*
* @author ken
*/
@Slf4j
public class AtomicIntegerDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
int threadCount = 1000;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
count.incrementAndGet();
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
log.info("count: {}", count.get());
}
}
可见性:线程间的变量同步
可见性是指当一个线程修改了共享变量的值,其他线程能立即看到这个修改。
CPU缓存模型与可见性问题
现代CPU通常有多级缓存(L1、L2、L3),每个核心有自己的L1、L2缓存,多个核心共享L3缓存。当线程修改变量时,会先将变量从主内存加载到工作内存(对应CPU缓存),修改后写回主内存,但写回时机不确定,导致其他线程可能看不到最新值。
示例:可见性问题
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 可见性问题示例
*
* @author ken
*/
@Slf4j
public class VisibilityDemo {
private static boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
while (!flag) {
// 空循环
}
log.info("线程A检测到flag变为true,结束循环");
}, "线程A").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
flag = true;
log.info("主线程将flag设置为true");
}
}
这段代码中,主线程将flag设置为true后,线程A可能永远不会结束循环,因为线程A的工作内存中flag还是旧值。
保证可见性的方式
- volatile关键字:通过内存屏障(Memory Barrier)保证变量的可见性。当写一个volatile变量时,JMM会把该线程工作内存中的变量值立即刷新回主内存;当读一个volatile变量时,JMM会把该线程工作内存中的变量置为无效,重新从主内存读取。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* volatile保证可见性示例
*
* @author ken
*/
@Slf4j
public class VolatileVisibilityDemo {
private static volatile boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
while (!flag) {
// 空循环
}
log.info("线程A检测到flag变为true,结束循环");
}, "线程A").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
flag = true;
log.info("主线程将flag设置为true");
}
}
2. synchronized关键字:在释放锁前,会将工作内存中的变量刷新回主内存;在获取锁后,会从主内存重新读取变量。 3. final关键字:final修饰的字段在初始化完成后,其他线程能看到其正确值(前提是对象没有逸出)。
有序性:禁止指令重排序
有序性是指程序执行的顺序按照代码的先后顺序执行。但在并发场景下,编译器和CPU可能会对指令进行重排序,以提高性能,这可能导致程序执行结果与预期不符。
指令重排序
- 编译器重排序:编译器在不改变单线程程序语义的前提下,调整指令的执行顺序。
- CPU重排序:CPU在执行指令时,可能会调整指令的执行顺序,以充分利用CPU流水线。
as-if-serial语义
as-if-serial语义保证:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和CPU都必须遵守as-if-serial语义。
示例:有序性问题(双重检查锁定)
package com.jam.demo;
/**
* 双重检查锁定示例(有序性问题)
*
* @author ken
*/
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这段代码看似没问题,但instance = new Singleton()这行代码可能会被重排序:
- 分配内存空间
- 初始化对象
- 将instance指向分配的内存地址
重排序后可能变成:
- 分配内存空间
- 将instance指向分配的内存地址
- 初始化对象
如果线程A执行到步骤2,此时instance不为null,但还没初始化对象,线程B在第一次检查时发现instance不为null,直接返回,就会拿到一个未初始化的对象。
保证有序性的方式
- volatile关键字:通过内存屏障禁止指令重排序。对于volatile变量的写操作,会在写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障;对于volatile变量的读操作,会在读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障。 修改后的双重检查锁定:
package com.jam.demo;
/**
* 双重检查锁定示例(volatile保证有序性)
*
* @author ken
*/
public class VolatileSingleton {
private static volatile VolatileSingleton instance;
private VolatileSingleton() {
}
public static VolatileSingleton getInstance() {
if (instance == null) {
synchronized (VolatileSingleton.class) {
if (instance == null) {
instance = new VolatileSingleton();
}
}
}
return instance;
}
}
2. synchronized关键字:保证同一时间只有一个线程执行临界区代码,相当于让临界区代码串行执行,自然保证了有序性。 3. happen-before规则:JMM定义的一套偏序关系,通过这些规则可以判断两个操作是否有序。
happen-before规则:JMM的核心偏序关系
happen-before规则是JMM定义的一套偏序关系,用于判断两个操作之间是否存在可见性保证。如果操作A happen-before 操作B,那么A的执行结果对B可见,且A的执行顺序排在B之前。
1. 程序次序规则
在一个线程内,按照代码顺序,前面的操作happen-before于后面的操作。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 程序次序规则示例
*
* @author ken
*/
@Slf4j
public class ProgramOrderRuleDemo {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
log.info("c: {}", c);
}
}
在主线程中,int a = 1 happen-before int b = 2,int b = 2 happen-before int c = a + b,所以a和b的赋值对c的计算可见。
2. 管程锁定规则
一个unlock操作happen-before于后面对同一个锁的lock操作。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 管程锁定规则示例
*
* @author ken
*/
@Slf4j
public class MonitorLockRuleDemo {
private static int count = 0;
public static void main(String[] args) {
new Thread(() -> {
synchronized (MonitorLockRuleDemo.class) {
count = 10;
}
}, "线程A").start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
synchronized (MonitorLockRuleDemo.class) {
log.info("count: {}", count);
}
}, "线程B").start();
}
}
线程A先释放锁,线程B后获取锁,所以线程A对count的修改对线程B可见。
3. volatile变量规则
对一个volatile变量的写操作happen-before于后面对这个变量的读操作。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* volatile变量规则示例
*
* @author ken
*/
@Slf4j
public class VolatileVariableRuleDemo {
private static volatile int value = 0;
private static boolean flag = false;
public static void main(String[] args) {
new Thread(() -> {
value = 10;
flag = true;
}, "线程A").start();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
if (flag) {
log.info("value: {}", value);
}
}, "线程B").start();
}
}
这里flag是volatile变量,线程A对flag的写操作happen-before线程B对flag的读操作,根据传递性,线程A对value的修改对线程B可见。
4. 线程启动规则
Thread对象的start()方法happen-before于此线程的每一个动作。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 线程启动规则示例
*
* @author ken
*/
@Slf4j
public class ThreadStartRuleDemo {
private static int value = 0;
public static void main(String[] args) {
value = 10;
Thread thread = new Thread(() -> {
log.info("value: {}", value);
}, "线程A");
thread.start();
}
}
主线程对value的修改happen-before线程A的start()方法,线程A的start()方法happen-before线程A的所有动作,所以主线程对value的修改对线程A可见。
5. 线程终止规则
线程中的所有操作都happen-before于对此线程的终止检测。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
/**
* 线程终止规则示例
*
* @author ken
*/
@Slf4j
public class ThreadTerminationRuleDemo {
private static int value = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread thread = new Thread(() -> {
try {
value = 10;
} finally {
countDownLatch.countDown();
}
}, "线程A");
thread.start();
countDownLatch.await();
log.info("value: {}", value);
}
}
线程A的所有操作happen-before主线程对线程A的终止检测(通过CountDownLatch.await()),所以线程A对value的修改对主线程可见。
6. 线程中断规则
对线程interrupt()方法的调用happen-before于被中断线程的代码检测到中断事件的发生。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 线程中断规则示例
*
* @author ken
*/
@Slf4j
public class ThreadInterruptRuleDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 空循环
}
log.info("线程A检测到中断,结束循环");
}, "线程A");
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
thread.interrupt();
}
}
主线程对thread的interrupt()调用happen-before线程A检测到中断事件,所以线程A能正确响应中断。
7. 对象终结规则
一个对象的初始化完成happen-before于它的finalize()方法的开始。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 对象终结规则示例
*
* @author ken
*/
@Slf4j
public class ObjectFinalizeRuleDemo {
private int value;
public ObjectFinalizeRuleDemo() {
this.value = 10;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
log.info("value: {}", value);
}
public static void main(String[] args) {
new ObjectFinalizeRuleDemo();
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
}
}
对象的初始化完成happen-beforefinalize()方法的开始,所以finalize()方法中能看到value的正确值。
8. 传递性
如果A happen-before B,B happen-before C,那么A happen-before C。 在volatile变量规则的示例中,线程A对value的修改(A)happen-before线程A对flag的写(B),线程A对flag的写(B)happen-before线程B对flag的读(C),所以线程A对value的修改(A)happen-before线程B对value的读(C)。
总结
JMM通过主内存与工作内存的抽象结构,定义了原子性、可见性、有序性的规范,并通过happen-before规则为并发编程提供了可见性保证。理解JMM的核心概念和规则,是写出正确、高效并发代码的关键。在实际开发中,我们可以通过synchronized、volatile、Lock、原子类等工具,结合happen-before规则,来解决并发场景下的各种问题。