线程同步与内存模型 (Thread Synchronization & Memory Model)¶
深入理解synchronized、volatile关键字,掌握Java内存模型(JMM)和happens-before规则
目录¶
- 1. 为什么需要同步
- 2. synchronized关键字
- 3. volatile关键字
- 4. Java内存模型(JMM)
- 5. happens-before规则
- 6. synchronized和volatile的区别
- 7. 最佳实践
- 8. 面试高频问题
1. 为什么需要同步 (Why Synchronization?)¶
1.1 线程安全问题¶
当多个线程访问共享资源时,如果没有同步机制,可能出现:
- 竞态条件(Race Condition) - 多个线程同时修改共享变量
- 数据不一致 - 读取到中间状态的数据
- 可见性问题 - 一个线程的修改对另一个线程不可见
1.2 示例:线程不安全的计数器¶
/**
* 线程不安全的计数器示例
* Unsafe Counter Example
*/
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 不是原子操作!
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
// 创建10个线程,每个线程增加1000次
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
// 期望结果:10000,实际可能小于10000
System.out.println("最终计数: " + counter.getCount());
}
}
问题分析:
count++ 不是原子操作,实际包含3步:
1. 读取count的值
2. 将值加1
3. 写回count
多个线程可能同时读取到相同的值,导致丢失更新。
1.3 在算力平台中的应用¶
在算力平台的结算系统中,多个线程可能同时处理用户钱包的扣费操作,必须保证线程安全:
// 线程不安全的钱包操作
public class UnsafeWallet {
private Long balance = 1000L;
public void deduct(Long amount) {
if (balance >= amount) {
balance -= amount; // 线程不安全!
}
}
}
// 线程安全的钱包操作(使用synchronized)
public class SafeWallet {
private Long balance = 1000L;
public synchronized void deduct(Long amount) {
if (balance >= amount) {
balance -= amount; // 线程安全
}
}
}
2. synchronized关键字 (synchronized Keyword)¶
2.1 synchronized的作用¶
synchronized关键字提供了一种**互斥锁(Mutex Lock)**机制,保证同一时刻只有一个线程能执行被保护的代码块。
作用: 1. 原子性(Atomicity) - 保证操作的原子性 2. 可见性(Visibility) - 保证变量的可见性 3. 有序性(Ordering) - 保证一定的执行顺序
2.2 synchronized的用法¶
方式一:同步方法(Synchronized Method)¶
/**
* 同步方法示例
* Synchronized Method Example
*/
public class SynchronizedCounter {
private int count = 0;
// 同步实例方法:锁对象是this
public synchronized void increment() {
count++;
}
// 同步静态方法:锁对象是类对象(SynchronizedCounter.class)
public static synchronized void staticMethod() {
// 静态方法同步
}
public int getCount() {
return count;
}
}
锁对象:
- 实例方法:锁对象是this(当前实例)
- 静态方法:锁对象是类对象(Class对象)
方式二:同步代码块(Synchronized Block)¶
/**
* 同步代码块示例
* Synchronized Block Example
*/
public class SynchronizedBlock {
private int count = 0;
private final Object lock = new Object(); // 锁对象
public void increment() {
// 同步代码块:锁对象是lock
synchronized (lock) {
count++;
}
}
// 也可以使用this作为锁对象
public void increment2() {
synchronized (this) {
count++;
}
}
// 使用类对象作为锁(静态同步)
public void staticSync() {
synchronized (SynchronizedBlock.class) {
// 静态同步代码块
}
}
}
优势: 可以更细粒度地控制锁的范围,提高性能。
2.3 synchronized的可重入性(Reentrant)¶
/**
* synchronized可重入性示例
* Reentrant Synchronization Example
*/
public class ReentrantDemo {
public synchronized void method1() {
System.out.println("method1");
method2(); // 调用另一个同步方法
}
public synchronized void method2() {
System.out.println("method2");
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.method1(); // 可以正常执行,不会死锁
}
}
可重入性: 同一个线程可以多次获取同一个锁,不会死锁。
2.4 锁的释放¶
/**
* 锁的释放时机
* Lock Release Timing
*/
public class LockRelease {
public synchronized void method1() {
// 执行代码
// 方法执行完毕,自动释放锁
}
public void method2() {
synchronized (this) {
// 执行代码
// 代码块执行完毕,自动释放锁
}
}
public void method3() {
synchronized (this) {
if (condition) {
return; // 提前返回,锁也会自动释放
}
// 其他代码
}
}
public void method4() {
synchronized (this) {
try {
// 可能抛出异常
riskyOperation();
} catch (Exception e) {
// 即使抛出异常,锁也会自动释放
}
}
}
}
锁的释放时机: 1. 同步方法执行完毕 2. 同步代码块执行完毕 3. 方法提前返回(return) 4. 抛出异常
2.5 对象锁 vs 类锁¶
/**
* 对象锁 vs 类锁
* Instance Lock vs Class Lock
*/
public class LockType {
private int instanceVar = 0;
private static int staticVar = 0;
// 对象锁:不同实例之间不互斥
public synchronized void instanceMethod() {
instanceVar++;
}
// 类锁:所有实例共享,互斥
public static synchronized void staticMethod() {
staticVar++;
}
public static void main(String[] args) {
LockType obj1 = new LockType();
LockType obj2 = new LockType();
// 对象锁:obj1和obj2不互斥,可以同时执行
new Thread(() -> obj1.instanceMethod()).start();
new Thread(() -> obj2.instanceMethod()).start();
// 类锁:所有实例共享,互斥
new Thread(() -> LockType.staticMethod()).start();
new Thread(() -> LockType.staticMethod()).start();
}
}
3. volatile关键字 (volatile Keyword)¶
3.1 volatile的作用¶
volatile关键字保证变量的**可见性**和**有序性**,但不保证**原子性**。
作用: 1. 可见性 - 一个线程的修改对其他线程立即可见 2. 有序性 - 禁止指令重排序 3. 不保证原子性 - 不能替代synchronized
3.2 可见性问题示例¶
/**
* 可见性问题示例
* Visibility Problem Example
*/
public class VisibilityProblem {
// 不使用volatile:可能出现可见性问题
private boolean flag = false;
public void setFlag() {
flag = true;
}
public void checkFlag() {
while (!flag) {
// 可能永远循环,因为看不到flag的变化
}
System.out.println("Flag is true");
}
public static void main(String[] args) {
VisibilityProblem demo = new VisibilityProblem();
// 线程1:设置flag
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.setFlag();
}).start();
// 线程2:检查flag(可能永远看不到变化)
new Thread(() -> {
demo.checkFlag();
}).start();
}
}
使用volatile解决:
public class VisibilitySolution {
// 使用volatile:保证可见性
private volatile boolean flag = false;
public void setFlag() {
flag = true; // 修改立即刷新到主内存
}
public void checkFlag() {
while (!flag) {
// 每次循环都会从主内存读取flag的最新值
}
System.out.println("Flag is true");
}
}
3.3 volatile不保证原子性¶
/**
* volatile不保证原子性示例
* volatile Does Not Guarantee Atomicity
*/
public class VolatileAtomicity {
private volatile int count = 0;
public void increment() {
count++; // 不是原子操作!
}
public static void main(String[] args) throws InterruptedException {
VolatileAtomicity demo = new VolatileAtomicity();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
// 结果可能小于10000,因为volatile不保证原子性
System.out.println("最终计数: " + demo.count);
}
}
volatile适用场景: - 单写多读场景 - 状态标志位 - 不依赖当前值的操作
3.4 volatile禁止指令重排序¶
/**
* volatile禁止指令重排序示例
* volatile Prevents Instruction Reordering
*/
public class ReorderingExample {
private int a = 0;
private int b = 0;
private volatile boolean flag = false;
// 线程1
public void writer() {
a = 1; // 1
b = 2; // 2
flag = true; // 3 - volatile写,之前的操作不会被重排序到之后
}
// 线程2
public void reader() {
if (flag) { // volatile读,之后的操作不会被重排序到之前
int sum = a + b; // 保证看到a=1, b=2
}
}
}
4. Java内存模型(JMM)(Java Memory Model)¶
4.1 JMM概述¶
**Java内存模型(JMM)**定义了Java虚拟机如何与计算机内存交互,规定了多线程环境下变量的读写规则。
4.2 内存区域划分¶
┌─────────────────────────────────────┐
│ 主内存(Main Memory) │
│ 共享变量(所有线程可见) │
└─────────────────────────────────────┘
↑ ↓
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ 线程1 │ │ 线程2 │
│ ┌─────────┐ │ │ ┌─────────┐ │
│ │工作内存 │ │ │ │工作内存 │ │
│ │本地变量 │ │ │ │本地变量 │ │
│ │副本 │ │ │ │副本 │ │
│ └─────────┘ │ │ └─────────┘ │
└─────────────┘ └─────────────┘
内存区域: 1. 主内存(Main Memory) - 所有线程共享,存储共享变量 2. 工作内存(Working Memory) - 线程私有,存储变量的副本
4.3 内存交互操作¶
JMM定义了8种内存操作:
- lock(锁定) - 作用于主内存,标识变量为线程独占
- unlock(解锁) - 作用于主内存,释放锁定状态
- read(读取) - 作用于主内存,将变量值传输到工作内存
- load(载入) - 作用于工作内存,将read的值放入工作内存变量副本
- use(使用) - 作用于工作内存,将变量值传递给执行引擎
- assign(赋值) - 作用于工作内存,将执行引擎的值赋给变量
- store(存储) - 作用于工作内存,将变量值传输到主内存
- write(写入) - 作用于主内存,将store的值写入主内存变量
操作顺序:
4.4 可见性问题的根源¶
/**
* 可见性问题的根源
* Root Cause of Visibility Problem
*/
public class JMMExample {
private int count = 0; // 主内存中的变量
public void increment() {
// 线程1的工作内存
int temp = count; // read + load:从主内存读取到工作内存
temp = temp + 1; // use + assign:在工作内存中计算
count = temp; // store + write:写回主内存(可能延迟)
}
public int getCount() {
// 线程2的工作内存
return count; // read + load:从主内存读取(可能读到旧值)
}
}
问题: 工作内存的更新可能不会立即刷新到主内存,导致其他线程看不到最新值。
5. happens-before规则 (happens-before Rules)¶
5.1 什么是happens-before?¶
**happens-before**是JMM定义的内存可见性规则,如果操作A happens-before操作B,那么A的结果对B可见。
5.2 happens-before规则¶
1. 程序顺序规则(Program Order Rule)¶
规则: 同一线程中,前面的操作happens-before后面的操作。
2. 监视器锁规则(Monitor Lock Rule)¶
synchronized (lock) {
// 操作A
}
// 操作A happens-before 释放锁
synchronized (lock) {
// 操作B
}
// 获取锁 happens-before 操作B
规则: 解锁操作happens-before后续的加锁操作。
3. volatile变量规则(Volatile Variable Rule)¶
volatile int flag = 0;
// 线程1
flag = 1; // volatile写
// 线程2
if (flag == 1) { // volatile读
// 能看到flag=1
}
规则: volatile写happens-before后续的volatile读。
4. 线程启动规则(Thread Start Rule)¶
规则: start()调用happens-before线程中的任何操作。
5. 线程终止规则(Thread Termination Rule)¶
Thread thread = new Thread(() -> {
// 操作A
});
thread.start();
thread.join(); // 操作A happens-before join()返回
// 操作B
规则: 线程中的所有操作happens-before其他线程的join()返回。
6. 中断规则(Interruption Rule)¶
规则: interrupt()调用happens-before被中断线程检测到中断。
7. 传递性规则(Transitivity Rule)¶
6. synchronized和volatile的区别 (synchronized vs volatile)¶
6.1 对比表¶
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | ✅ 保证 | ❌ 不保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 保证 | ✅ 保证 |
| 性能 | 较低(重量级锁) | 较高(轻量级) |
| 使用场景 | 多写场景、复杂同步 | 单写多读、状态标志 |
| 锁机制 | 互斥锁 | 无锁 |
6.2 使用建议¶
使用synchronized: - 需要保证原子性 - 多线程写操作 - 复杂的同步逻辑
使用volatile: - 单写多读场景 - 状态标志位 - 不依赖当前值的操作
示例:单例模式(双重检查锁定)
/**
* 双重检查锁定单例模式
* Double-Checked Locking Singleton
*/
public class Singleton {
private static volatile Singleton instance; // 必须使用volatile
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 需要volatile保证可见性
}
}
}
return instance;
}
}
为什么需要volatile?
new Singleton()不是原子操作,包含:
1. 分配内存空间
2. 初始化对象
3. 将引用赋值给instance
如果没有volatile,步骤2和3可能重排序,导致其他线程看到未初始化的对象。
7. 最佳实践 (Best Practices)¶
7.1 减少锁的粒度¶
// ❌ 不推荐:锁范围太大
public synchronized void process() {
// 不需要同步的代码
doSomething();
// 需要同步的代码
synchronized (this) {
count++;
}
// 不需要同步的代码
doSomethingElse();
}
// ✅ 推荐:只锁必要的代码
public void process() {
doSomething();
synchronized (this) {
count++;
}
doSomethingElse();
}
7.2 避免死锁¶
// ❌ 不推荐:可能死锁
public void method1() {
synchronized (lockA) {
synchronized (lockB) {
// ...
}
}
}
public void method2() {
synchronized (lockB) {
synchronized (lockA) { // 不同的锁顺序,可能死锁
// ...
}
}
}
// ✅ 推荐:统一锁顺序
public void method1() {
synchronized (lockA) {
synchronized (lockB) {
// ...
}
}
}
public void method2() {
synchronized (lockA) { // 相同的锁顺序
synchronized (lockB) {
// ...
}
}
}
7.3 使用final字段¶
// ✅ 推荐:final字段天然线程安全
public class SafeObject {
private final int value; // final字段,线程安全
public SafeObject(int value) {
this.value = value;
}
}
7.4 避免在循环中加锁¶
// ❌ 不推荐:在循环中加锁
public void process(List<String> items) {
for (String item : items) {
synchronized (this) {
processItem(item);
}
}
}
// ✅ 推荐:在循环外加锁
public void process(List<String> items) {
synchronized (this) {
for (String item : items) {
processItem(item);
}
}
}
8. 面试高频问题 (Interview Questions)¶
Q1: synchronized和volatile的区别?¶
答案: 见6. synchronized和volatile的区别
Q2: volatile能保证原子性吗?¶
答案: 不能。volatile只保证可见性和有序性,不保证原子性。例如count++不是原子操作。
Q3: 什么是Java内存模型(JMM)?¶
答案: JMM定义了Java虚拟机如何与内存交互,规定了多线程环境下变量的读写规则,包括主内存和工作内存的概念。
Q4: happens-before规则有哪些?¶
答案: 程序顺序规则、监视器锁规则、volatile变量规则、线程启动规则、线程终止规则、中断规则、传递性规则。
Q5: 双重检查锁定为什么需要volatile?¶
答案: 防止指令重排序,确保其他线程看到完全初始化的对象。
📖 扩展阅读¶
返回: 07-Java并发编程
上一章: 07-01 - 线程基础
下一章: 07-03 - 锁机制 →