Skip to content

并发工具类

CountDownLatch、CyclicBarrier、Semaphore 区别?

CountDownLatch

CountDownLatch 是一个倒计时门闩。

它的核心特点是:一个或多个线程等待其他线程完成任务。

典型场景:

java
CountDownLatch latch = new CountDownLatch(3);

latch.countDown(); // 计数减 1
latch.await();     // 等待计数归零

特点:

说明
计数方式初始化一个计数值,每调用一次 countDown() 减 1
阻塞方法await()
是否可复用不可复用
典型场景主线程等待多个子任务完成

例子:主线程等 5 个子线程都执行完,再继续执行。

CyclicBarrier

CyclicBarrier 是一个循环屏障。

它的核心特点是:多个线程互相等待,直到所有线程都到达某个屏障点,再一起继续执行。

典型场景:

java
CyclicBarrier barrier = new CyclicBarrier(3);

barrier.await(); // 等待其他线程也到达

特点:

说明
计数方式等待固定数量线程到达屏障
阻塞方法await()
是否可复用可复用
典型场景多个线程分阶段协同执行

例子:3 个线程都完成第一阶段后,再一起进入第二阶段。

Semaphore

Semaphore 是一个信号量。

它的核心特点是:控制同时访问某个资源的线程数量。

典型场景:

java
Semaphore semaphore = new Semaphore(3);

semaphore.acquire(); // 获取许可证
try {
    // 访问资源
} finally {
    semaphore.release(); // 释放许可证
}

特点:

说明
控制对象许可证数量
阻塞方法acquire()
释放方法release()
是否可复用可复用
典型场景限流、连接池、资源访问控制

例子:数据库连接池最多允许 10 个线程同时获取连接。

三者对比总结

工具作用是否可复用典型用途
CountDownLatch一个或多个线程等待其他线程完成主线程等待子线程完成
CyclicBarrier多个线程互相等待,到齐后继续多线程分阶段任务
Semaphore控制并发访问数量限流、资源池

一句话总结:CountDownLatch 是"别人干完我再干";CyclicBarrier 是"大家到齐一起干";Semaphore 是"最多允许 N 个线程干"。

BlockingQueue 有哪些实现?

BlockingQueue 是 Java 并发包中的阻塞队列接口,常用于生产者消费者模型。

常见实现

实现类特点
ArrayBlockingQueue基于数组,有界阻塞队列
LinkedBlockingQueue基于链表,可有界也可近似无界
PriorityBlockingQueue支持优先级排序的无界阻塞队列
DelayQueue延迟队列,元素到期后才能取出
SynchronousQueue不存储元素,每个 put 必须等待 take
LinkedTransferQueue基于链表的无界传输队列
LinkedBlockingDeque双端阻塞队列

重点实现说明

ArrayBlockingQueue

基于数组,必须指定容量。

java
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

特点:

  • 有界
  • FIFO
  • 使用一个 ReentrantLock
  • 适合固定容量的生产者消费者场景

LinkedBlockingQueue

基于链表,可以指定容量,不指定时默认容量非常大。

java
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(100);

特点:

  • 默认近似无界
  • FIFO
  • 通常使用两个锁:putLock 和 takeLock
  • 吞吐量通常比 ArrayBlockingQueue 更高

PriorityBlockingQueue

带优先级的阻塞队列。

java
BlockingQueue<Task> queue = new PriorityBlockingQueue<>();

特点:

  • 无界
  • 不保证 FIFO
  • 元素需要实现 Comparable 或传入 Comparator
  • 适合任务优先级调度

DelayQueue

延迟队列。

特点:

  • 元素必须实现 Delayed
  • 只有延迟时间到期后才能被取出
  • 适合定时任务、缓存过期、订单超时取消

SynchronousQueue

不存储元素的队列。

特点:

  • 容量为 0
  • 每个 put 必须等待一个 take
  • 每个 take 必须等待一个 put
  • 常用于 Executors.newCachedThreadPool()

ThreadLocal 是什么?

ThreadLocal 是 Java 提供的一种线程本地变量机制。

它可以让每个线程都拥有变量的独立副本,线程之间互不影响。

java
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

threadLocal.set(100);
Integer value = threadLocal.get();
threadLocal.remove();

比如多个线程都访问同一个 ThreadLocal 对象:

Thread-1 -> value = A
Thread-2 -> value = B
Thread-3 -> value = C

它们拿到的是各自线程自己的值,不会互相干扰。

ThreadLocal 的底层原理

每个 Thread 对象内部都有一个 ThreadLocalMap。

大致结构可以理解为:

java
Thread {
    ThreadLocalMap threadLocals;
}

ThreadLocalMap 中存储的是:

  • key = ThreadLocal 对象
  • value = 线程本地变量值

也就是说,数据并不是存在 ThreadLocal 里面,而是存在当前线程的 ThreadLocalMap 里面。

常见使用场景

场景说明
用户上下文保存当前登录用户信息
请求上下文保存 traceId、requestId
数据库连接每个线程维护自己的连接
日期格式化解决 SimpleDateFormat 线程不安全问题
事务管理保存当前线程事务状态

核心作用:ThreadLocal 用来在同一个线程内共享变量,在不同线程间隔离变量。

ThreadLocal 为什么可能内存泄漏?

核心原因

ThreadLocalMap 的 key 是弱引用,但 value 是强引用。

结构类似:

java
Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
}

也就是说:

  • key → ThreadLocal,弱引用
  • value → 实际对象,强引用

如果 ThreadLocal 对象没有外部强引用了,GC 时 key 会被回收,变成 null。

但是 value 仍然被 ThreadLocalMap 强引用着。

于是出现这种情况:

key = null
value = 某个大对象

如果线程一直不结束,比如线程池中的核心线程长期存活,那么这个 value 就可能一直无法释放,造成内存泄漏。

为什么线程池更容易出现?

普通线程执行完后,Thread 对象销毁,它内部的 ThreadLocalMap 也会被回收。

但是线程池中的线程会被复用,生命周期很长。

所以如果没有及时调用:

java
threadLocal.remove();

旧数据可能一直留在线程中。

正确使用方式

一定要在 finally 中调用 remove():

java
try {
    threadLocal.set(value);
    // 业务逻辑
} finally {
    threadLocal.remove();
}

ThreadLocal 内存泄漏的根本原因是线程长期存活时,ThreadLocalMap 中 key 被回收但 value 仍被强引用;解决方式是用完后调用 remove。

ConcurrentHashMap JDK 1.7 和 1.8 有什么区别?

JDK 1.7:Segment 分段锁

JDK 1.7 中,ConcurrentHashMap 的核心结构是:

ConcurrentHashMap
    Segment[]
        HashEntry[]
            HashEntry -> HashEntry -> HashEntry

也就是:Segment 数组 + HashEntry 数组 + 链表

其中 Segment 继承了 ReentrantLock。

每个 Segment 相当于一个小的 HashMap。

并发控制靠的是分段锁。

JDK 1.7
数据结构Segment + HashEntry + 链表
锁粒度Segment 级别
锁机制ReentrantLock
并发度默认 16 个 Segment
查询 get通常不加锁,使用 volatile 保证可见性
扩容每个 Segment 单独扩容

JDK 1.8:CAS + synchronized + 红黑树

JDK 1.8 中,ConcurrentHashMap 取消了 Segment 分段锁,结构变成:

Node[] table
    Node -> Node -> Node

链表过长时会转成红黑树:数组 + 链表 + 红黑树

并发控制方式:

  • 插入空桶时使用 CAS
  • 桶中已有元素时使用 synchronized 锁住桶头节点
  • 链表长度过长时转红黑树
  • 扩容时多个线程可以协助迁移数据
JDK 1.8
数据结构Node 数组 + 链表 + 红黑树
锁粒度桶级别
锁机制CAS + synchronized
是否有 Segment保留类定义但不再作为核心结构
查询 get不加锁
扩容多线程协助扩容
性能优化降低锁粒度,提高并发性能

JDK 1.7 和 1.8 对比

对比项JDK 1.7JDK 1.8
底层结构Segment 数组 + HashEntry 数组 + 链表Node 数组 + 链表 + 红黑树
锁粒度Segment桶头节点
锁实现ReentrantLockCAS + synchronized
是否有红黑树没有
扩容方式Segment 内部单独扩容多线程协助扩容
并发度受 Segment 数量影响更细粒度,按桶控制
查询基本无锁无锁
插入锁 SegmentCAS 或锁桶头

为什么 JDK 1.8 改用 synchronized?

因为 JDK 1.6 之后,synchronized 做了大量优化,例如:

  • 偏向锁
  • 轻量级锁
  • 锁膨胀
  • 锁消除
  • 自适应自旋

在低竞争场景下,synchronized 性能已经很好,而且代码更简单。

总结:JDK 1.7 的 ConcurrentHashMap 使用 Segment 分段锁,底层是 Segment 数组加 HashEntry 数组加链表,每个 Segment 继承 ReentrantLock,锁粒度是 Segment。JDK 1.8 取消了 Segment 分段锁,底层变成 Node 数组加链表加红黑树,插入空桶用 CAS,发生冲突时用 synchronized 锁桶头节点,扩容时支持多线程协助迁移,因此锁粒度更细,并发性能更好。