Java 并发编程 JUC:线程池、锁与原子类

FreeGuideOnline 最新 2026-06-17

Java 并发编程 JUC 核心详解:线程池、锁与原子类

1. JUC 是什么?

JUC 是 java.util.concurrent 包的简称,它是 Java 5 引入的并发编程核心工具集。JUC 提供了比传统 synchronizedwait/notify 更高级、更灵活的并发控制能力,主要包括:

  • 线程池:高效管理和复用线程资源
  • 锁框架:显式锁、读写锁等更精细的锁控制
  • 原子类:无锁的线程安全变量
  • 并发集合:如 ConcurrentHashMapCopyOnWriteArrayList
  • 同步工具CountDownLatchCyclicBarrierSemaphore

本教程聚焦三个最常用的模块:线程池、锁与原子类,通过原理讲解和代码示例帮助你快速掌握 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 线程池运行机制

  1. 任务提交后,若当前线程数 < corePoolSize,立即创建新线程执行任务。
  2. 若当前线程数 ≥ corePoolSize,任务被放入 workQueue 等待。
  3. 若队列已满且当前线程数 < maximumPoolSize,创建新线程(非核心)执行任务。
  4. 若队列满且线程数 = 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 基础类型原子类

AtomicIntegerAtomicLongAtomicBoolean 提供了对基本类型的原子更新。

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();            // 自增并返回
counter.compareAndSet(expect, update);// CAS 操作
counter.getAndUpdate(x -> x * 2);     // 原子更新

CAS 底层依赖处理器 cmpxchg 指令,比较原值与期望值,相等才更新,否则重新获取新值重试。

4.2 数组原子类

AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray 可让数组中的每个元素都能被原子更新。

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 引入的 LongAdderDoubleAdderAtomicLong 更适合高并发统计场景,它采用分而治之的热点分离技术,内部维护多个 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 并发编程的中流砥柱,理解其内部机制并遵循最佳实践是写出高并发、高稳定应用的关键。继续实践和阅读源码,你将对并发有更深体感。