序言
Java内存模型(JMM)与多线程是构建高并发系统的核心基石。本文深入剖析JMM的核心原理、多线程的底层实现以及实际开发中的优化策略。
1. Java内存模型(JMM)核心原理
1.1 主内存与工作内存的交互
Java内存模型(JMM)定义了线程与内存之间的抽象关系:所有共享变量存储在主内存中,每个线程拥有独立的工作内存(线程私有缓存)。线程对变量的操作(如读取、赋值)必须在工作内存中进行,无法直接访问主内存。这种设计提升了性能,但也引入了线程间的数据可见性问题。
交互流程:
- 变量拷贝:线程启动时,从主内存拷贝变量副本到工作内存。
- 本地操作:线程在工作内存中读取或修改变量副本。
- 同步回主内存:修改完成后,将工作内存中的变量值写回主内存。
代码示例:
int sharedVar = 0; // 主内存中的共享变量
Thread threadA = new Thread(() -> {
int localCopy = sharedVar; // 从主内存拷贝到工作内存
localCopy = 42; // 修改工作内存副本
sharedVar = localCopy; // 同步回主内存
});
Thread threadB = new Thread(() -> {
System.out.println(sharedVar); // 可能输出0或42,取决于同步时机
});
threadA.start();
threadB.start();
关键点:
- 同步时机不确定性:JMM未强制规定工作内存何时同步回主内存,具体行为依赖JVM实现。这意味着,不同JVM可能表现出不同的同步延迟,导致线程B读取到旧值(0)或新值(42)。
- 解决可见性问题:使用
volatile
关键字或锁机制(如synchronized
)可以强制线程间的内存同步,确保数据一致性。 - 线程间通信依赖主内存,未显式同步可能导致数据不一致问题。
1.2 内存屏障与指令重排序
指令重排序是编译器和处理器为了优化性能而调整指令执行顺序的行为。在单线程中,重排序不会影响结果,但在多线程环境下,可能导致不可预期的行为。JMM通过内存屏障(Memory Barrier)控制重排序,确保多线程的正确性。
重排序来源:
- 编译器重排序:在生成字节码时优化指令顺序。
- 处理器重排序:利用指令级并行性调整执行顺序。
内存屏障类型:
屏障类型 | 作用 | 示例场景 |
---|---|---|
LoadLoad | 确保屏障前读操作早于屏障后读操作 | volatile 读后普通读 |
StoreStore | 确保屏障前写操作早于屏障后写操作 | volatile 写前普通写 |
LoadStore | 确保屏障前读操作早于屏障后写操作 | 普通读后volatile 写 |
StoreLoad | 确保屏障前写操作早于屏障后读操作 | volatile 写后普通读,性能开销最大 |
代码示例(双重检查锁定单例模式):
public class Singleton {
private static volatile Singleton instance; // volatile 防止指令重排
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 对象初始化可能被重排序
}
}
}
return instance;
}
}
关键点:
- 对象初始化过程:
instance = new Singleton()
包括三步:- 分配内存空间。
- 初始化对象。
- 将引用指向内存地址。
若无volatile
,步骤2和3可能被重排序,导致其他线程获取未初始化的对象。
- volatile的作用:
- 写操作插入
StoreStore
和StoreLoad
屏障,确保初始化完成后再赋值。 - 读操作插入
LoadLoad
和LoadStore
屏障,确保读取到完整对象。
- 写操作插入
2. 多线程同步机制对比与选择
2.1 锁机制:synchronized vs ReentrantLock
特性 | synchronized | ReentrantLock |
---|---|---|
实现方式 | JVM内置,基于Monitor锁 | 基于AQS(AbstractQueuedSynchronizer) |
锁释放 | 自动释放(代码块结束或异常) | 需手动调用unlock() |
可中断性 | 不支持 | 支持lockInterruptibly() |
公平锁 | 默认非公平锁 | 可选公平锁(构造参数true ) |
条件变量 | 单条件(wait/notify) | 支持多个Condition |
性能优化 | JDK 6后引入锁升级机制 | 高竞争场景可灵活调整 |
关键点:
- synchronized锁升级:
- 无锁 → 偏向锁(单线程优化) → 轻量级锁(低竞争,自旋) → 重量级锁(高竞争,阻塞)。
- 通过对象头的Mark Word记录锁状态,减少性能开销。
- ReentrantLock优势:
- 支持超时获取锁(如
tryLock(1, TimeUnit.SECONDS)
)。 - 可创建多个条件变量,用于复杂线程协调。
- 支持超时获取锁(如
代码示例(ReentrantLock):
ReentrantLock lock = new ReentrantLock(true); // 公平锁
public void criticalSection() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 确保释放锁
}
}
适用场景:
- synchronized:简单同步场景,锁竞争不激烈。
- ReentrantLock:需要中断、公平性或多条件等待的高级场景。
2.2 volatile 的局限性
volatile
保证变量的可见性和禁止指令重排序,但不保证原子性。
错误示例:
volatile int count = 0;
public void increment() {
count++; // 非原子操作:读-改-写
}
问题分析:
count++
分为三步:读取count、加1、写回count。多线程并发执行可能导致更新丢失。
解决方案:
- 原子类:使用
AtomicInteger
的CAS操作。 - 锁机制:用
synchronized
或ReentrantLock
保护操作。
代码示例(原子类):
AtomicInteger atomicCount = new AtomicInteger(0);
public void safeIncrement() {
atomicCount.incrementAndGet(); // CAS保证原子性
}
关键点:
- CAS原理:Compare-And-Swap,比较当前值与预期值,相等则更新,否则重试。
- ABA问题:CAS可能忽略中间状态变化,可用
AtomicStampedReference
解决。
3. 线程池优化与死锁预防
3.1 线程池配置最佳实践
Java通过Executors
提供多种线程池类型,但自定义ThreadPoolExecutor
更为灵活。
核心参数:
- corePoolSize:核心线程数,即使空闲也保留。
- maximumPoolSize:最大线程数,队列满后扩展至此值。
- keepAliveTime:非核心线程空闲存活时间。
- workQueue:任务队列,如
ArrayBlockingQueue
(有界)、LinkedBlockingQueue
(无界)。 - rejectedExecutionHandler:拒绝策略,如
CallerRunsPolicy
。
线程池类型对比:
类型 | 特点 | 适用场景 |
---|---|---|
FixedThreadPool | 固定线程数,无界队列 | 任务量稳定 |
CachedThreadPool | 动态线程数,空闲回收 |
| 短任务,高吞吐量 | | ScheduledThreadPool | 支持定时/周期任务 | 定时调度 | | WorkStealingPool | 任务窃取,多核优化 | CPU密集型并行计算 |
自定义示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60, TimeUnit.SECONDS, // 空闲存活时间
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadFactoryBuilder().setNameFormat("task-pool-%d").build(), // 线程命名
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时由提交线程执行
);
关键点:
- 线程数选择:
- CPU密集型:线程数 ≈ CPU核心数(如
Runtime.getRuntime().availableProcessors()
)。 - IO密集型:线程数 > CPU核心数(如2倍核心数)。
- CPU密集型:线程数 ≈ CPU核心数(如
- 队列选择:避免无界队列(如
LinkedBlockingQueue
默认无上限),防止内存溢出。
3.2 死锁检测与解决
死锁条件:
- 互斥:资源独占。
- 请求与保持:持有资源并请求新资源。
- 不可剥夺:资源不可被抢占。
- 循环等待:线程间形成等待环。
死锁示例:
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) {} // 等待lockB
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) {} // 等待lockA
}
});
预防策略:
- 按序加锁:统一锁获取顺序(如按对象hash值排序)。
- 超时机制:使用
ReentrantLock.tryLock()
设置等待超时。 - 资源分配优化:一次性申请所有资源,避免边持边等。
检测工具:
-
jstack:
jstack <pid> | grep -i deadlock
-
VisualVM:实时监控线程状态,检测死锁。
4. 实战:并发计数器性能对比
4.1 测试场景
在高并发环境下,比较以下同步机制的性能:
- 无同步(线程不安全)
synchronized
ReentrantLock
AtomicLong
4.2 代码实现
public class CounterBenchmark {
private long count = 0;
private final Object lock = new Object();
private final ReentrantLock reentrantLock = new ReentrantLock();
private AtomicLong atomicCount = new AtomicLong(0);
public void unsafeIncrement() { count++; } // 无同步
public void synchronizedIncrement() { synchronized(lock) { count++; } }
public void reentrantLockIncrement() {
reentrantLock.lock();
try { count++; } finally { reentrantLock.unlock(); }
}
public void atomicIncrement() { atomicCount.incrementAndGet(); }
}
4.3 性能结果(参考)
同步方式 | 吞吐量(ops/ms) | 适用场景 |
---|---|---|
无同步 | 最高(数据错误) | 不推荐 |
synchronized | 中等 | 低竞争,简单同步 |
ReentrantLock | 中高 | 高竞争,复杂场景 |
AtomicLong | 最高 | 单一变量原子操作 |
关键点:
- 无同步:吞吐量高但线程不安全,仅用于单线程或基准测试。
- synchronized:锁竞争加剧时性能下降,因涉及内核态切换。
- ReentrantLock:灵活性高,可通过非公平锁优化性能。
- AtomicLong:基于CAS无锁操作,适用于高并发单一变量场景。
5. 总结
- 可见性与原子性:
- 用
volatile
确保简单变量可见性。 - 用原子类(如
AtomicInteger
)或锁保护复合操作。
- 用
- 锁选择:
- 低竞争用
synchronized
,高竞争或复杂需求用ReentrantLock
。
- 低竞争用
- 线程池调优:
- CPU密集型:线程数 ≈ CPU核心数。
- IO密集型:线程数 > CPU核心数。
- 使用有界队列+合理拒绝策略(如
CallerRunsPolicy
)。
- 死锁预防:
- 统一锁顺序,避免嵌套锁。
- 设置锁超时,定期用
jstack
检测死锁。
参考资料
- 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
- Oracle JMM 官方文档