Java 并发编程总结
最近系统梳理了 Java 并发编程的核心知识点,将一些关键内容整理如下,方便日后回顾。
1. JMM 与 Happens-Before 规则理解
JMM(Java 内存模型)定义了线程如何通过内存进行交互,以及何时能够看到其他线程的写入操作。它主要解决了可见性与有序性问题(注意:原子性不在其范畴内)。
Happens-Before 是 JMM 的核心概念,常见的规则包括:
- 程序次序规则:单线程内顺序执行
- 监视器锁规则:解锁操作先于后续的加锁操作
- volatile 变量规则:写操作先于后续的读操作
- 线程启动与终止规则:start() 先于线程内操作,线程内操作先于终止检测
- 传递性规则:A → B,B → C,则 A → C
- 中断规则:对线程的 interrupt() 调用先于被中断线程的响应
- final 字段规则:正确构造的 final 字段在构造完成后对其他线程可见
指令重排序是允许的,但前提是不能破坏 Happens-Before 约束。JIT 编译器的重排序受到内存屏障的限制,synchronized 和 volatile 在编译后都会生成相应的屏障指令。
// 典型示例:volatile 保证可见性
volatile boolean ready = false;
int data = 0;void writer() {data = 42; // 普通写ready = true; // volatile 写,建立HB关系
}void reader() {if (ready) { // volatile 读,看到true则之前的写操作都可见System.out.println(data); // 保证输出42}
}
易错点:
• 误以为 volatile 能保证复合操作的原子性(如 i++)
• 不了解 final 字段的发布规则导致对象逸出
延伸思考:为什么 final 字段构造完成后对其他线程可见?
参考答案:因为 JMM 在构造结束时插入了内存屏障,且要求对象引用在构造完成前不能逸出。
2. volatile 与 synchronized 的区别与适用场景
这两个关键字解决了不同层面的并发问题:
volatile:
• 保证可见性和禁止指令重排序
• 不保证原子性
• 适用场景:状态标志位、一次性发布(如配置加载)
synchronized:
• 保证互斥访问和可见性
• 通过进入/退出临界区时的内存屏障实现
• 适用场景:需要原子性操作的复合逻辑
// volatile 典型用法:停止标志
class StoppableTask {private volatile boolean stopRequested;public void run() {while (!stopRequested) {// 执行任务}}public void stop() {stopRequested = true;}
}
易错点:使用 volatile 进行计数操作(如 ++)无法保证原子性
延伸思考:AtomicInteger 如何实现原子性?
答:AtomicInteger 通过 CAS + 自旋循环实现原子性,适合计数器场景。
3. CAS 原理与 ABA 问题解决方案
CAS(Compare-And-Swap)是无锁编程的核心操作:
• 比较内存中的值与期望值,相等则写入新值
• 基于硬件指令实现,冲突时通过自旋重试
• JDK9+ 使用 VarHandle 替代 Unsafe
ABA 问题:值从 A → B → A 的变化过程,CAS 无法感知中间状态变化
• 解决方案:添加版本号(AtomicStampedReference)或使用带标记的指针
// 使用版本号解决ABA问题
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);int[] stampHolder = new int[1];
int currentStamp = ref.getStamp();
int currentValue = ref.get(stampHolder);// 更新时同时检查值和版本号
ref.compareAndSet(currentValue, 200, currentStamp, currentStamp + 1);
注意:高竞争场景下自旋可能导致 CPU 占用过高,需考虑退避策略
4. synchronized 与 ReentrantLock 选择策略
两者都是可重入互斥锁,但各有特点:
synchronized:
• 语法简洁,JVM 内置支持
• JIT 编译器会进行偏向锁、轻量级锁等优化
• 自动释放锁,不易遗漏
ReentrantLock:基于 AQS(state + CLH 队列)
• 支持公平锁选项
• 支持可中断的锁获取
• 支持超时尝试获取锁
• 支持多个条件变量(Condition)
// ReentrantLock 的典型用法
ReentrantLock lock = new ReentrantLock(true); // 公平锁try {if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {try {// 临界区操作} finally {lock.unlock(); // 必须手动释放}}
} catch (InterruptedException e) {Thread.currentThread().interrupt();
}
5. AQS 核心设计思想
AQS(AbstractQueuedSynchronizer
)是 Java 并发包的核心基础框架:
核心组件:
• state:同步状态,不同子类有不同语义(如重入次数、许可数等)
• CLH 队列:双向队列管理等待线程
• 两种模式:独占模式(ReentrantLock)和共享模式(Semaphore)
条件变量:ConditionObject 维护单独的条件队列,signal 时转移到同步队列
工作流程:
- 尝试获取锁(tryAcquire)
- 失败则加入队列,park 等待
- 释放锁时唤醒后继节点
注意:自定义 AQS 时需要正确处理可重入和中断响应
共享模式的典型实现?(Semaphore、CountDownLatch)
6. Condition 与 wait/notify 对比
Condition 提供了比传统 wait/notify 更精细的线程协调机制:
优势:
• 一个锁可以关联多个条件队列
• 支持精确唤醒(signal vs signalAll)
• 更清晰的语义表达(wait/notify 依赖对象监视器,只有一个等待集)
// 生产者-消费者模式中使用两个Condition
class BoundedBuffer {final Lock lock = new ReentrantLock();final Condition notFull = lock.newCondition();final Condition notEmpty = lock.newCondition();final Object[] items = new Object[100];int putptr, takeptr, count;public void put(Object x) throws InterruptedException {lock.lock();try {while (count == items.length)notFull.await();items[putptr] = x;if (++putptr == items.length) putptr = 0;++count;notEmpty.signal();} finally {lock.unlock();}}// take方法类似
}
注意:必须在持有锁时调用 await/signal,且 signal 可能早于 await 导致信号丢失。
为什么必须在 try/finally 中 unlock()?(异常路径也要释放)
7. 读写锁选择:ReadWriteLock vs StampedLock
根据读写模式选择不同的锁实现:
ReentrantReadWriteLock:
• 读锁共享,写锁独占
• 可重入,支持条件变量
• 写锁可降级为读锁
StampedLock:
• 提供乐观读模式,性能更高
• 不可重入,不支持条件变量
• 通过 stamp 验证锁有效性
// StampedLock 乐观读示例
StampedLock lock = new StampedLock();
int value = 0;int readValue() {long stamp = lock.tryOptimisticRead(); // 乐观读int currentValue = value;if (!lock.validate(stamp)) { // 验证期间是否有写操作stamp = lock.readLock(); // 转为悲观读try {currentValue = value;} finally {lock.unlockRead(stamp);}}return currentValue;
}
选型建议
- 读多写少且性能要求高 → StampedLock;
- 需要可重入或条件变量 → ReadWriteLock
8. 线程池参数配置实践
线程池 ThreadPoolExecutor 配置需要根据业务特点设计:
核心参数:
• corePoolSize:核心线程数,CPU 密集型建议 Ncpu+1
• maximumPoolSize:最大线程数,I/O 密集型可适当增大
• workQueue:任务队列,推荐有界队列避免 OOM
• keepAliveTime+unit:空闲线程存活时间
• threadFactory:线程工厂,建议设置线程名称和异常处理器
• handler:拒绝策略,根据业务需求选择(Abort(默认)、CallerRuns、Discard、DiscardOldest)
// 线程池配置示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, // corePoolSize16, // maximumPoolSize 60, TimeUnit.SECONDS, // keepAliveTimenew LinkedBlockingQueue<>(1000), // 有界队列r -> { // 线程工厂Thread t = new Thread(r, "biz-thread-" + counter.getAndIncrement());t.setUncaughtExceptionHandler((thread, ex) -> logger.error("Uncaught exception in {}", thread.getName(), ex));return t;},new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
监控指标:活跃线程数、队列大小、拒绝任务数等需要纳入监控。
任务提交速率远超处理能力怎么办?(背压/限流/降级/拆分池/熔断)
9. CompletableFuture 组合编程
CompletableFuture 提供了强大的异步编程能力:
常用操作:
- supplyAsync(task, executor): 指定线程池,默认是 ForkJoinPool.commonPool
• thenApply/thenAccept:转换和消费结果
• thenCompose:扁平化嵌套 Future
• thenCombine:合并多个 Future 结果
• allOf/anyOf:等待所有/任意任务完成
异常处理:
• exceptionally:异常恢复
• handle:统一处理结果和异常
• whenComplete:完成时回调
// 并行调用多个服务并合并结果
CompletableFuture<String> futureA = CompletableFuture.supplyAsync(this::callServiceA, executor);
CompletableFuture<String> futureB = CompletableFuture.supplyAsync(this::callServiceB, executor);CompletableFuture<String> combined = futureA.thenCombine(futureB, (a, b) -> a + b).exceptionally(ex -> {logger.warn("Service call failed, using fallback", ex);return "fallback-value";});String result = combined.join();
与 Future 的区别?(可组合、非阻塞回调)
10. ForkJoinPool 工作窃取机制
ForkJoinPool 采用工作窃取(work-stealing)算法提升性能:
核心机制:
• 每个工作线程维护自己的双端队列
• 本地任务 LIFO 执行,窃取其他队列任务时从尾部获取
• 减少竞争,提高 CPU 利用率
并行流注意事项:
• 并行流底层使用 ForkJoinPool.commonPool
• 避免在并行流中执行阻塞 I/O 操作
• 注意 ThreadLocal 上下文传递问题
// 在自定义ForkJoinPool中执行并行流
ForkJoinPool customPool = new ForkJoinPool(4);
try {customPool.submit(() -> dataList.parallelStream().map(this::processItem).collect(Collectors.toList())).get();
} finally {customPool.shutdown();
}
Java 并发工具实践总结(续)
11. 同步器选择:CountDownLatch vs CyclicBarrier vs Phaser
在实际项目中,根据不同的同步需求选择合适的同步工具非常重要:
CountDownLatch - 一次性门闩
• 典型场景:主线程等待多个子任务完成初始化
• 特点:计数不可重置,等待线程在计数归零后继续执行
// 等待三个服务初始化完成
CountDownLatch latch = new CountDownLatch(3);executor.execute(() -> {initServiceA();latch.countDown();
});executor.execute(() -> {initServiceB(); latch.countDown();
});executor.execute(() -> {initServiceC();latch.countDown();
});// 主线程等待所有服务初始化完成
latch.await();
logger.info("所有服务初始化完成");
CyclicBarrier - 可复用栅栏
• 典型场景:多线程数据分片处理,需要同步点
• 特点:可重置,支持栅栏动作(barrier action)
Phaser - 多阶段栅栏
• 典型场景:多阶段任务协作,参与者动态变化
• 优势:支持阶段推进、动态注册/注销参与者
未处理屏障破裂异常(BrokenBarrierException)
异常路径忘记调用 countDown(),导致主线程永久等待
Phaser 相比 CyclicBarrier 的优势?(动态参与者、阶段控制)
12. BlockingQueue 的几种实现差异
不同的 BlockingQueue 实现适用于不同场景:
-
ArrayBlockingQueue:基于数组的有界队列,支持公平策略(减少线程饥饿),固定容量,内存占用可控。
-
LinkedBlockingQueue:基于链表的队列,默认无界(Integer.MAX_VALUE),吞吐量通常更高,但可能导致 OOM。适合任务量相对可控的场景
-
PriorityBlockingQueue:无界优先级队列,消费顺序取决于优先级,非入队顺序,适合需要优先处理某些任务的场景
-
DelayQueue:基于优先级队列的延迟队列,元素需实现 Delayed 接口,适合定时任务、缓存过期等场景
注意:生产禁用无界队列,避免无界队列导致的内存溢出和背压失效问题。
13. ThreadLocal 的正确使用与内存泄漏防范
ThreadLocal 非常适合存储线程上下文信息,但需要谨慎使用。
// 线程安全的日期格式化
private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));public String formatDate(Date date) {try {return DATE_FORMATTER.get().format(date);} finally {// 关键:在线程池环境中必须清理,避免内存泄漏DATE_FORMATTER.remove();}
}
内存泄漏风险:
• 线程池中的线程会复用,如果不调用 remove(),ThreadLocal 值会一直存在
• 值对象如果较大,会导致严重的内存泄漏
• 建议使用 try-finally 确保清理
InheritableThreadLocal在线程池下为何危险 :在线程池中,子任务可能由不同线程执行,继承语义会错乱,不建议在异步场景使用。
14. 死锁预防与诊断实践
死锁是并发编程中的经典问题,需要从预防和诊断两方面着手:
预防措施:
// 使用 tryLock 避免死锁
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();public void safeOperation() {if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {try {if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {try {// 临界区操作doCriticalWork();} finally {lock2.unlock();}}} finally {lock1.unlock();}}
}
统一加锁顺序是预防死锁的有效方法,确保所有线程以相同顺序获取锁。
诊断工具:
• jstack:查看线程状态和锁持有情况
• JFR:Java Flight Recorder 记录详细并发事件
• ThreadMXBean.findDeadlockedThreads():编程式检测死锁
概念区分:
• 死锁:相互等待对方释放锁
• 活锁:线程不断重试但无法前进(如消息处理失败不断重试)
• 饥饿:某些线程长期得不到执行机会
15. ConcurrentHashMap 内部机制理解
核心改进:
• 摒弃分段锁,使用 bin-level 锁(synchronized)+ CAS 初始化桶。
• 高冲突时链表转红黑树(treeify),阈值 8/6,提升查询效率
• 扩容时支持多线程协同迁移
使用注意:
• size() 返回的是近似值,因为并发环境下精确计数代价太高
• 复合操作(如 putIfAbsent)仍需外部同步保证原子性
• computeIfAbsent 在并发环境下可能被多次执行,需要保证幂等性
16. 结构化并发改善代码可维护性
结构化并发让异步代码具有同步代码的清晰结构。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {// 并行发起多个RPC调用Supplier<String> userTask = scope.fork(() -> userService.getUser(userId));Supplier<String> orderTask = scope.falk(() -> orderService.getOrders(userId));// 等待所有任务完成或任一失败scope.join();scope.throwIfFailed();// 组合结果return new UserResponse(userTask.get(), orderTask.get());
}
优势:
• 任务生命周期管理自动化
• 异常传播和取消机制更完善
• 避免"悬挂任务"问题
- 与 CompletableFuture.allOf 相比,结构化并发提供了更好的可观察性和控制力。
17. 性能优化:减少竞争与伪共享
在高并发场景下,减少竞争是关键优化方向:
减少锁竞争:
• 数据分片(如 LongAdder 的分段计数)
• 读写分离(读写锁、CopyOnWrite)
• 乐观策略(StampedLock 乐观读)
解决伪共享:
// 使用缓存行填充避免伪共享
@jdk.internal.vm.annotation.Contended
public class Counter {private volatile long value;// 自动添加填充字节,避免与其他热点字段共享缓存行
}
LongAdder 的热点规避原理:通过分桶(Cell数组)分散写压力,sum时合并结果,在高并发写场景下性能远超AtomicLong。