Java 并发编程 JUC:线程池、锁与原子类
Java 并发编程 JUC 核心详解:线程池、锁与原子类
1. JUC 是什么?
JUC 是 java.util.concurrent 包的简称,它是 Java 5 引入的并发编程核心工具集。JUC 提供了比传统 synchronized 和 wait/notify 更高级、更灵活的并发控制能力,主要包括:
- 线程池:高效管理和复用线程资源
- 锁框架:显式锁、读写锁等更精细的锁控制
- 原子类:无锁的线程安全变量
- 并发集合:如
ConcurrentHashMap、CopyOnWriteArrayList - 同步工具:
CountDownLatch、CyclicBarrier、Semaphore等
本教程聚焦三个最常用的模块:线程池、锁与原子类,通过原理讲解和代码示例帮助你快速掌握 JUC 实战。
2. 线程池:从创建到最佳实践
线程池解决了传统 new Thread() 创建线程的资源浪费问题,通过复用线程、控制并发数量实现高性能异步任务执行。
2.1 核心接口与实现类
Executor:顶级接口,定义execute(Runnable)方法ExecutorService:扩展了生命周期管理方法ThreadPoolExecutor:核心实现类,提供高度可配置的线程池Executors:工厂工具类,快速创建常见类型的线程池(不推荐生产环境直接使用)
2.2 ThreadPoolExecutor 构造参数详解
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数,即使空闲也不会被回收(除非
allowCoreThreadTimeOut设为 true) - maximumPoolSize:最大线程数,当队列满时最多可扩展到的线程数
- keepAliveTime:超出核心线程数的空闲线程存活时间
- workQueue:任务队列,常用
LinkedBlockingQueue(无界)、ArrayBlockingQueue(有界)、SynchronousQueue(直接传递) - handler:拒绝策略,当线程数和队列都满时对新任务的拒绝处理
2.3 线程池运行机制
- 任务提交后,若当前线程数 < corePoolSize,立即创建新线程执行任务。
- 若当前线程数 ≥ corePoolSize,任务被放入
workQueue等待。 - 若队列已满且当前线程数 < maximumPoolSize,创建新线程(非核心)执行任务。
- 若队列满且线程数 = maximumPoolSize,触发拒绝策略。
2.4 四种常见拒绝策略
| 策略 | 类名 | 行为 |
|---|---|---|
| AbortPolicy | 默认 | 抛出 RejectedExecutionException 异常 |
| CallerRunsPolicy | 调用者运行 | 直接由提交任务的线程执行该任务,减慢提交速度 |
| DiscardPolicy | 丢弃 | 直接无声丢弃新任务 |
| DiscardOldestPolicy | 丢弃最旧 | 丢弃队列头部最老任务,重新尝试提交当前任务 |
2.5 手动创建线程池的最佳实践
Executors 提供的固定、缓存、单线程等方法存在 OOM 风险(因其队列无界或线程数无上限),强烈建议手动创建 ThreadPoolExecutor。
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
cpuCores + 1, // 核心线程数
cpuCores * 2, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(200), // 有界队列
new ThreadFactoryBuilder().setNameFormat("custom-pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 降级策略
);
使用有界队列防止无限堆积,自定义线程工厂便于监控,选择合适的拒绝策略。
2.6 选择合适的队列
- CPU 密集型任务:
corePoolSize = CPU核心数 + 1,使用SynchronousQueue或较小有界队列,让线程数快速升至最大,避免上下文切换开销。 - IO 密集型任务:
corePoolSize = CPU核心数 * 2(或更多),使用有界队列缓冲任务,防止瞬时流量冲垮服务。 - 混合型:可拆分为两个线程池分别处理。
3. 锁框架:显式锁与读写锁
JUC 提供了比 synchronized 更强大的 Lock 接口,支持尝试获取锁、可中断获取锁、公平锁等特性。
3.1 ReentrantLock:可重入互斥锁
private final Lock lock = new ReentrantLock();
public void safeMethod() {
lock.lock(); // 加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须释放锁!
}
}
高级特性:
- 可中断获取
lockInterruptibly():等待锁时可被其他线程中断。 - 尝试非阻塞获取
tryLock():立即返回是否成功,或带超时参数。 - 公平锁:构造
new ReentrantLock(true)可实现 FIFO 公平性,但性能略低,默认非公平。
3.2 ReadWriteLock 与读写分离
ReentrantReadWriteLock 维护一对锁:读锁(共享) 和 写锁(排他)。读读不互斥,读写/写写互斥,适用于读多写少的场景。
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
public Object getData() {
readLock.lock();
try { return data; }
finally { readLock.unlock(); }
}
public void putData(Object val) {
writeLock.lock();
try { data = val; }
finally { writeLock.unlock(); }
}
注意:写锁可降级为读锁(获取写锁后,再获取读锁,释放写锁),但读锁不能升级为写锁,避免死锁。
3.3 StampedLock:乐观读与性能优化
Java 8 引入的 StampedLock 提供三种模式:
- 乐观读(无锁):通过
tryOptimisticRead()获取一个戳记,读取后通过validate(stamp)验证是否被写过,若被写则升级为悲观读。 - 悲观读:类似传统的读锁。
- 写锁:排他写。
StampedLock stampedLock = new StampedLock();
public int optimisticRead() {
long stamp = stampedLock.tryOptimisticRead(); // 乐观读戳
int result = sharedData;
if (!stampedLock.validate(stamp)) { // 验证期间是否被写
stamp = stampedLock.readLock(); // 升级为悲观读
try {
result = sharedData;
} finally {
stampedLock.unlockRead(stamp);
}
}
return result;
}
乐观读避免了锁开销,适合读极多、写极少的场景,但使用时逻辑较复杂。
3.4 Condition:精确的等待/通知机制
每个 Lock 对象可创建多个 Condition,更精确地控制线程挂起与唤醒,弥补了 synchronized 只有一个等待队列的短板。
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
public void put(Object obj) throws InterruptedException {
lock.lock();
try {
while (queue.isFull()) {
notFull.await();
}
queue.add(obj);
notEmpty.signal();
} finally { lock.unlock(); }
}
3.5 锁的性能与选择
- synchronized:现代 JVM 已大幅优化(偏向锁、轻量级锁),写法简洁,适合大多数简单同步需求。
- ReentrantLock:需要可中断、超时、公平锁等附加功能时使用。
- ReadWriteLock:读多写少场景。
- StampedLock:对读性能极致追求,允许适度代码复杂度增加。
4. 原子操作类:无锁线程安全
原子类利用 CAS(Compare And Swap) 实现无锁同步,避免线程阻塞和上下文切换,适合高频轻量操作。
4.1 基础类型原子类
AtomicInteger、AtomicLong、AtomicBoolean 提供了对基本类型的原子更新。
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 自增并返回
counter.compareAndSet(expect, update);// CAS 操作
counter.getAndUpdate(x -> x * 2); // 原子更新
CAS 底层依赖处理器 cmpxchg 指令,比较原值与期望值,相等才更新,否则重新获取新值重试。
4.2 数组原子类
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 可让数组中的每个元素都能被原子更新。
AtomicIntegerArray arr = new AtomicIntegerArray(5);
arr.set(0, 10);
arr.incrementAndGet(1);
4.3 引用原子类
AtomicReference:原子更新对象引用AtomicStampedReference:解决 CAS 的 ABA 问题(通过版本戳)AtomicMarkableReference:使用布尔标记替代版本号
ABA 问题示例:线程1将 A→B,又 B→A,线程2看到仍是 A 而不知中间变化。AtomicStampedReference 每次更新修改 stamp,即使值相同 stamp 不同也能检测。
4.4 字段更新器
基于反射的原子更新,可减少创建原子类的开销,如 AtomicIntegerFieldUpdater 可让普通 volatile int 字段具备原子操作能力。但要求字段是 volatile,且不能是 static(除非使用对应的 AtomicIntegerFieldUpdater<T>)。
4.5 高性能累加器:LongAdder
JDK 8 引入的 LongAdder 和 DoubleAdder 比 AtomicLong 更适合高并发统计场景,它采用分而治之的热点分离技术,内部维护多个 Cell 累加单元,最终求和时合并,大幅降低 CAS 竞争。
LongAdder adder = new LongAdder();
adder.increment();
adder.increment();
long sum = adder.sum(); // 获取总和(非强一致性快照)
LongAccumulator 则支持自定义累加函数,如求最大值:new LongAccumulator(Math::max, Long.MIN_VALUE)。
5. 总结与最佳实践清单
- 线程池:永远不要使用 Executors,必须手动指定参数;使用有界队列;线程命名有意义;自定义 ThreadFactory 捕获异常。
- 拒绝策略:根据业务选择,
CallerRunsPolicy可提供背压,AbortPolicy适合严格场景需记录异常。 - 锁选择:能用
synchronized解决的问题不要引入Lock;读多写少用ReadWriteLock;更追求性能且代码掌控力强考虑StampedLock。 - 原子类:替代
volatile+synchronized的计数器;大量统计用LongAdder;需要版本控制解决 ABA 用AtomicStampedReference。 - 监控与调优:线程池需暴露核心指标(活跃线程数、队列大小、完成任务数),通过监控及时调整参数。
JUC 是 Java 并发编程的中流砥柱,理解其内部机制并遵循最佳实践是写出高并发、高稳定应用的关键。继续实践和阅读源码,你将对并发有更深体感。