Java 并发控制从入门到精通:实现方式、适用场景与优缺点

目录


1. 并发问题的本质:可见性、原子性、有序性

  • 可见性:一个线程对共享变量的写,何时对另一个线程可见?(CPU 缓存、多级缓存)
  • 原子性:复合操作是否不可中断?如 count++ 非原子。
  • 有序性:编译器/CPU 指令重排可能改变执行顺序。

解决思路:利用 JMM 的 happens-before 关系建立“先行发生”,或使用锁/原子操作/volatile 保证。


2. Java 内存模型(JMM)与 happens-before 规则

常见规则(高频考点):

  1. 程序次序规则:单线程内,按程序顺序。
  2. 监视器锁规则:对同一锁的 unlock 先行发生于随后对该锁的 lock
  3. volatile 规则:对某个 volatile 变量的写,先行发生于后续读。
  4. 线程启动Thread.start() 先行发生于新线程中的任何操作。
  5. 线程终止:线程中的所有操作先行发生于 Thread.join() 成功返回。
  6. 中断规则:对线程 interrupt() 先行发生于被中断线程检测到中断事件。
  7. 对象发布与最终字段:构造函数中对 final 字段的写,对其他线程是安全可见的(对象正确发布)。

3. 互斥与同步的实现方式总览

按粒度分层理解:

  • 语言级:synchronizedvolatilefinal 发布。
  • 类库级(java.util.concurrent):LockCondition、AQS 同步器(SemaphoreCountDownLatch…),原子类与并发容器,阻塞队列,线程池,CompletableFuture 等。
  • 设计层:无锁/低锁、复制写(COW)、分段锁、读写分离、基于消息/队列的并发(异步解耦)。

4. synchronized 关键字

用法

  • 实例/类方法修饰;同步代码块 synchronized(obj) { ... }
  • 内置监视器(Monitor),进入时独占,退出时释放。

优点

  • 语义简单、异常安全、可重入;与 JMM 深度集成,释放锁→刷新写、获取锁→读取最新
  • 代码可读性强,适合临界区较短的互斥。

缺点

  • 无法中断等待,无法设置超时;
  • 仅支持单条件队列(需 wait/notify 搭配,API 较原始)。

示例:

class Counter {
  private int c;
  public synchronized void inc() { c++; }
  public synchronized int get() { return c; }
}

何时优先:同步块短、结构简单、无需超时/中断、无需多条件队列。


5. Lock 家族:ReentrantLock / ReadWriteLock / StampedLock

5.1 ReentrantLock

  • 特性:可重入、可选择公平/非公平、支持可中断获取可超时获取、支持多个 Condition
  • 优点:比 synchronized 更灵活;支持 tryLock() 避免死锁;可实现复杂的等待/通知。
  • 缺点:需要手动 unlock();滥用会导致复杂度上升。

示例(避免死锁):

if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
  try {
    if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
      try { /* ... */ }
      finally { lock2.unlock(); }
    }
  } finally { lock1.unlock(); }
}

5.2 ReadWriteLock(ReentrantReadWriteLock)

  • 读多写少场景:多个读并行,写互斥。
  • 优点:读吞吐提升明显。
  • 缺点:可能写饥饿(可用公平锁缓解);读期间升级为写不支持(需释放后再获取写)。

5.3 StampedLock

  • 三种模式:写锁、悲观读锁、乐观读tryOptimisticRead)。
  • 优点:读多写少下,乐观读无需加锁,性能更好。
  • 缺点不可重入、不与 Condition 协作、API 更复杂。

示例(乐观读):

long stamp = lock.tryOptimisticRead();
int x = this.x, y = this.y;
if (!lock.validate(stamp)) { // 有写入发生,回退到悲观读
  stamp = lock.readLock();
  try { x = this.x; y = this.y; } finally { lock.unlockRead(stamp); }
}

6. 条件队列与线程协作:wait/notify、Condition、LockSupport

wait/notify(配合 synchronized)

  • 优点:JDK 早期机制,直接作用于对象监视器。
  • 缺点:易错(丢通知/虚假唤醒),写法繁琐;建议总是用 while 检查条件
synchronized (lock) {
  while (!ready) lock.wait();
  // do work
}

Condition(配合 Lock)

  • 优点:多个条件队列、精确唤醒(signal/signalAll),与 ReentrantLock 配合强大。
  • 缺点:需要管理锁与条件的匹配关系。

LockSupport

  • 低层 API(park/unpark),AQS 的基石;不要直接与 synchronized 混用

7. AQS 同步器与并发工具:Semaphore / CountDownLatch / CyclicBarrier / Phaser / Exchanger

AQS(AbstractQueuedSynchronizer) 是 JUC 多数同步器的基础。

  • Semaphore:计数信号量,限流/限并发。

    • 优点:实现连接池/限速简单有效;
    • 缺点:只能控制许可数量,无法表达复杂条件;
    Semaphore sem = new Semaphore(10);
    sem.acquire();
    try { /* 临界区 */ } finally { sem.release(); }
    
  • CountDownLatch:一次性闭锁,等待若干事件完成。

    • 优点:线程安全版“等 N 件事”;
    • 缺点:一次性,不可重用。
  • CyclicBarrier:可重用栅栏,N 个线程相互等待;可附加 barrierAction。

    • 优点:阶段性并行算法(如分片计算 → 汇总);
    • 缺点:某线程超时/异常会导致 BrokenBarrierException
  • Phaser:分阶段栅栏,动态注册/注销,适合复杂的多阶段工作流。

    • 优点:更灵活的多轮次协作;
    • 缺点:API 相对复杂。
  • Exchanger:成对线程交换数据。

    • 优点:双缓冲、生产-消费配对;
    • 缺点:仅适用于两两配对的场景。

8. 原子类与 CAS:AtomicXXX、LongAdder/LongAccumulator、ABA 问题

CAS(Compare-And-Set)

  • 优点:无需加锁,冲突低时性能好。
  • 缺点:自旋重试消耗 CPU;ABA 问题;只适合简单状态更新。

AtomicXXX

  • AtomicInteger/Long/Boolean/ReferenceAtomicStampedReference(带版本解决 ABA)、AtomicMarkableReference
  • 优点:简单无锁,适合计数器/标志位/单引用更新。
  • 缺点:难以表达复合不变量(需要 AtomicReference 持有不可变对象)。

LongAdder / LongAccumulator

  • 思想:热点分散(分片 Cell),高并发累加更快。
  • 优点:高并发下优于 AtomicLong
  • 缺点读取是近似值(在没有并发写时才准确)。

示例:

LongAdder adder = new LongAdder();
adder.increment();
long v = adder.sum(); // 近似累计值

9. 并发容器与阻塞队列

并发容器

  • ConcurrentHashMap:高并发哈希表,支持 computeIfAbsent/merge

    • 优点:扩容、分段/桶级并发(JDK8 之后基于 CAS + 红黑树化优化);
    • 缺点:复合操作仍需外部同步(如“检查后执行”)。
  • CopyOnWriteArrayList/Set:写时复制,读无锁。

    • 优点:读多写少非常快;迭代无锁且弱一致;
    • 缺点:写开销大、内存占用高,不适合大对象/频繁写。

阻塞队列(生产者-消费者)

  • ArrayBlockingQueue:有界数组;
  • LinkedBlockingQueue:链表,默认近乎无界(不指定容量会很大);
  • SynchronousQueue:无容量,交接式;适配“直接移交”模型;
  • DelayQueue/PriorityBlockingQueue/LinkedTransferQueue:定时/优先级/传递式等特殊场景。

建议:生产者-消费者优先选有界队列,并在饱和时给出降级/拒绝策略。


10. ThreadLocal 的使用与陷阱

  • 用途:为每个线程提供独立副本(如格式化器、上下文、TraceId)。
  • 陷阱:在线程池中,线程复用 → 不清理会内存泄漏/脏数据
  • 建议:使用 try { set(...) } finally { remove(); };必要时使用 TransmittableThreadLocal 传播上下文。

11. 线程池与任务调度:ThreadPoolExecutor / ScheduledThreadPoolExecutor / ForkJoinPool / CompletableFuture

11.1 ThreadPoolExecutor 关键参数

  • corePoolSize / maximumPoolSize / keepAliveTime / workQueue / threadFactory / handler
  • 拒绝策略AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
  • 创建建议不要直接用 Executors 的快捷方法(如 newFixedThreadPool 默认无界队列),改用 ThreadPoolExecutor 明确边界。

尺寸估算(经验):

  • CPU 密集:core ≈ CPU核数
  • I/O 密集:core ≈ CPU核数 × (1 + 平均等待时间/平均计算时间)

11.2 ScheduledThreadPoolExecutor

  • 定时/周期任务;注意任务执行时间超过周期时的堆积与并发策略。

11.3 ForkJoinPool 与并行计算

  • 任务分治(work-stealing);适合大任务拆分小任务的 CPU 密集型场景。

11.4 CompletableFuture(异步编排)

  • 优点:链式编排、组合计算、异常处理(exceptionally/handle),可指定自定义执行器;
  • 陷阱:默认使用公共 ForkJoinPool,易与框架抢资源;阻塞 join/get 可能导致线程饥饿

示例:自定义执行器 + 组合

ExecutorService ioPool = new ThreadPoolExecutor(
  8, 16, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200));

CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> loadA(), ioPool);
CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> loadB(), ioPool);
String result = a.thenCombine(b, (x, y) -> x + y)
                 .orTimeout(2, TimeUnit.SECONDS)
                 .exceptionally(ex -> "fallback")
                 .join();

12. 无锁/低锁思路与性能优化

  • 分段与拆分:将热点拆成多个分片(如 LongAdder)。
  • 局部性优化:减少共享、增加不可变对象;
  • 读写分离ReadWriteLock快照+增量
  • 批量化:降低锁获取频率(批处理插入/聚合);
  • 避免伪共享:必要时使用填充(@sun.misc.Contended 在某些版本需 JVM 选项)或结构化内存布局。

13. 典型并发 Bug 与排错方法

13.1 常见问题

  • 死锁:循环等待;

    • 防治:约定加锁顺序、tryLock + 超时、细化锁粒度、避免在持锁区做 I/O。
  • 活锁/饥饿:线程过度礼让/优先级不当;

  • 可见性缺陷:缺 volatile 或发布不安全;

  • 并发容器误用computeIfAbsent 里做阻塞操作导致扩散阻塞。

13.2 观测与工具

  • 线程转储jstack/jcmd Thread.print → 定位死锁(会直接打印 “Found one Java-level deadlock”)。
  • JFR/Profiler:Java Flight Recorder、Async-profiler 观察锁竞争、阻塞点、CPU 画像。
  • 日志与指标:关键路径埋点、超时重试日志、队列长度/拒绝次数/耗时直方图。

14. 选型与实践清单(速查表)

需求首选替代/补充注意点
简单互斥synchronizedReentrantLock临界区短、无需超时/中断 → 用 synchronized
需要可中断/超时/多条件ReentrantLock+Conditiontry{ lock } finally { unlock }
读多写少ReentrantReadWriteLockStampedLockStampedLock 不可重入;乐观读需校验
限流/限并发Semaphore线程池 + 有界队列许可数量与降级策略
等待多事件完成CountDownLatchPhaserLatch 一次性,Phaser 可复用
阶段性协作CyclicBarrierPhaser超时/异常会破栅栏
双线程数据交换Exchanger队列只适用于成对线程
高并发计数LongAdderAtomicLong读取近似值;统计结束再读
生产者-消费者BlockingQueueDisruptor(第三方)使用有界队列与拒绝策略
异步编排CompletableFutureReactor/RxJava指定自定义执行器,避免公共池争用

工程化建议

  • 统一线程池创建工厂(命名、守护线程、异常处理、MDC/Trace 传递)。
  • 所有阻塞操作设定超时;关键路径设熔断/限流/降级策略。
  • 监控:线程池活跃数、队列长度、任务拒绝、等待与执行耗时分布。

15. 面试要点与手撕小题

  • volatile 的作用与局限:只保证可见性与有序性,不保证复合操作原子性。
  • synchronized vs ReentrantLock:是否可中断/超时、公平性、条件队列、手动释放。
  • AQS 的核心:基于 CLH 队列,state 表示同步状态,独占/共享两种获取与释放语义。
  • Semaphore 与 CountDownLatch 区别:许可 vs 计数;多次获取/释放 vs 一次性倒数。
  • CyclicBarrier 与 Phaser 区别:Barrier 固定参与者;Phaser 动态注册/多阶段。
  • LongAdder 适用场景:高并发计数;为何优于 AtomicLong(热点分散)。
  • CompletableFuture 常见坑:公共池、阻塞 join 造成饥饿、异常传播;如何指定线程池与做超时/降级。

手撕示例:有界生产-消费 + 超时退出

class BoundedQueue<T> {
  private final BlockingQueue<T> q = new ArrayBlockingQueue<>(1000);
  public boolean put(T t, long timeout, TimeUnit unit) throws InterruptedException {
    return q.offer(t, timeout, unit); // 失败可触发降级
  }
  public T take(long timeout, TimeUnit unit) throws InterruptedException {
    return q.poll(timeout, unit);     // 超时可做空闲处理
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值