JavaJUC 并发工具箱:常见类、线程安全集合与死锁
JUC并发编程核心工具摘要 Java并发工具包(JUC)提供了高效的多线程开发组件,主要包括: Callable+FutureTask - 替代传统wait/notify模式,实现带返回值的异步任务处理 ReentrantLock - 比synchronized更灵活的互斥锁,支持超时、公平锁等特性 原子类 - 基于CAS实现无锁线程安全操作,如AtomicInteger的原子增减 线程池 - 通
JUC 并发工具箱:常见类、线程安全集合与死锁
java.util.concurrent(JUC)可以理解成:多线程开发里“别手搓了,直接用标准件”的工具箱。来看三块最常用的内容:常见类、线程安全集合、死锁。
1. JUC 的常见类:从“手动挡多线程”升级到“自动挡多线程”
1.1 Callable + FutureTask:把“返回值 + 等结果”这件事做正确
先看一个“传统手法”:子线程算完结果,主线程用 wait/notify 等结果。代码能写,但同步细节多、容易出错。
static class Result {
public int sum = 0;
public Object lock = new Object();
}
public static void main(String[] args) throws InterruptedException {
Result result = new Result();
Thread t = new Thread(() -> {
int sum = 0;
for (int i = 1; i <= 1000; i++) sum += i;
synchronized (result.lock) {
result.sum = sum;
result.lock.notify();
}
});
t.start();
synchronized (result.lock) {
while (result.sum == 0) {
result.lock.wait();
}
System.out.println(result.sum);
}
}
这里的关键点是:主线程要在 while 里 wait(),避免“被唤醒后条件其实仍不成立”的情况;而且还需要额外的 Result 辅助对象来承载共享数据和锁,整体复杂度偏高。
再看 JUC 的“标准答案”:Callable 负责“能返回结果的任务”,FutureTask 负责“存结果 + 等结果”。
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() {
int sum = 0;
for (int i = 1; i <= 1000; i++) sum += i;
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get(); // 阻塞等待计算完成,并拿到返回值
System.out.println(result);
FutureTask 就像“取餐小票”:任务什么时候做完不确定,但小票在手,随时 get() 去等结果/拿结果。
1.2 ReentrantLock:更灵活的互斥锁(但也更考验手法)
ReentrantLock 和 synchronized 都是为了互斥(同一时刻只让一个线程进入临界区)。它的典型用法是:lock 之后必须 finally unlock,否则极容易漏掉解锁造成“锁永久不释放”。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区:访问共享资源
} finally {
lock.unlock();
}
它比 synchronized 更“可控”的点主要有这些:
synchronized是 JVM 内部实现的关键字;ReentrantLock是标准库类(JVM 外、Java 实现)。synchronized获取不到锁会“死等”;ReentrantLock可以tryLock(超时),等一会拿不到就放弃。synchronized是非公平锁;ReentrantLock默认也非公平,但构造时传true可以开公平锁。- 等待/唤醒方面:
synchronized用wait/notify,唤醒的是“随机等待线程”;ReentrantLock + Condition可以更精确地控制唤醒哪个等待线程。
再给一个 tryLock 的味道(“死等” vs “等一会儿不行就撤”):
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
// 拿到锁了再干活
} finally {
lock.unlock();
}
} else {
// 拿不到锁:选择降级、重试、记录日志等
}
什么时候选哪个?一句话:竞争不激烈图省心 → synchronized;竞争激烈或需要超时/公平/精确唤醒 → ReentrantLock。
1.3 原子类 AtomicX:用 CAS 做“无锁原子更新”
原子类内部依赖 CAS(Compare-And-Swap)实现,通常比“加锁做 i++”更高效。常见的有:
AtomicBooleanAtomicIntegerAtomicIntegerArrayAtomicLongAtomicReferenceAtomicStampedReference(名字就暗示:它会带“戳”,常用来对付 ABA)
以 AtomicInteger 为例,常见方法和语义:
addAndGet(delta):i += deltaincrementAndGet():++igetAndIncrement():i++decrementAndGet():--igetAndDecrement():i--
来看一个最常用的例子:并发计数。
AtomicInteger cnt = new AtomicInteger(0);
Runnable r = () -> {
for (int i = 0; i < 100000; i++) {
cnt.getAndIncrement();
}
};
new Thread(r).start();
new Thread(r).start();
它背后的核心逻辑可以理解成一个 CAS 自旋(一直尝试,直到更新成功):
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while (CAS(value, oldValue, oldValue + 1) != true) {
oldValue = value;
}
return oldValue;
}
}
注意这里的 CAS 体现的是“三元比较交换”:内存位置/当前值、期望旧值、新值,不匹配就重试。
1.4 线程池:ExecutorService / Executors / ThreadPoolExecutor
线程频繁创建销毁不划算,所以线程池的思路是:线程不用了先放“池子”里,下次直接复用。
最常见入口是:
ExecutorService:线程池实例Executors:工厂类,快速创建不同风格线程池submit(...):提交任务
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(() -> {
System.out.println("hello");
});
Executors 常见创建方式包括:
newFixedThreadPool:固定线程数newCachedThreadPool:线程数动态增长newSingleThreadExecutor:单线程newScheduledThreadPool:延迟/定时执行(Timer 的进阶版)
更可控的底层是 ThreadPoolExecutor。理解它的参数,可以用“开公司招人”类比:
corePoolSize:正式员工数maximumPoolSize:正式员工 + 临时工上限keepAliveTime+unit:临时工空闲多久就辞退workQueue:任务队列(阻塞队列)threadFactory:线程工厂RejectedExecutionHandler:忙不过来时的拒绝策略
拒绝策略常见四种:
AbortPolicy():直接抛异常CallerRunsPolicy():调用者自己执行DiscardOldestPolicy():丢队列里最老的任务DiscardPolicy():丢新来的任务
来看一个带拒绝策略的 ThreadPoolExecutor 示例(这里用 SynchronousQueue 配合 AbortPolicy):
ExecutorService pool = new ThreadPoolExecutor(
1, 2,
1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 3; i++) {
pool.submit(() -> System.out.println("hello"));
}
任务超过负荷时,会按策略处理(这里是直接异常)。
1.5 Semaphore:为什么能像“锁”一样用?
Semaphore 是“可用资源个数”的计数器:acquire() 申请资源(P 操作),release() 释放资源(V 操作)。计数器减到 0 还要申请就会阻塞等待;更关键的是 PV 加减计数是原子的,所以可以直接多线程使用。
它能起到“类似锁”的作用,本质是:把“能同时进入临界区的人数”限制住。互斥锁是“最多 1 个”;信号量可以是“最多 N 个”(共享锁)。
Semaphore semaphore = new Semaphore(4); // 4 个“名额”
Runnable r = () -> {
try {
System.out.println("申请资源");
semaphore.acquire(); // P:名额 -1(没有名额就阻塞)
System.out.println("获取到资源");
Thread.sleep(1000);
System.out.println("释放资源");
semaphore.release(); // V:名额 +1
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int i = 0; i < 20; i++) {
new Thread(r).start();
}
前 4 个线程能直接进入“临界区”,后面的线程会在 acquire() 阻塞,直到有人 release();这就是“共享锁”的味道。
1.6 CountDownLatch:等一堆线程干完活再继续
CountDownLatch 用来“同时等待 N 个任务结束”:构造时给一个计数 N;每个任务结束 countDown();主线程 await() 等计数归零。
CountDownLatch latch = new CountDownLatch(10);
Runnable r = () -> {
try {
Thread.sleep((long) (Math.random() * 10000));
latch.countDown(); // 一个任务完成,计数 -1
} catch (Exception e) {
e.printStackTrace();
}
};
for (int i = 0; i < 10; i++) {
new Thread(r).start();
}
// 必须等到 10 个都完成
latch.await();
System.out.println("比赛结束");
这段代码的语义非常直白:“不到 10 人全回来,不公布成绩”。
2. 线程安全的集合类:别拿 HashMap 去硬刚并发
2.1 先定个基调:哪些“天生线程安全”,哪些不是
很多集合默认不是线程安全的;但 Vector / Stack / Hashtable 是线程安全的(不过不太建议用),其他大多数集合类都不是线程安全的。
2.2 多线程环境下怎么用 ArrayList:三条路
路 1:自己加锁(synchronized 或 ReentrantLock):
List<Integer> list = new ArrayList<>();
Object lock = new Object();
Runnable r = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
list.add(i);
}
}
};
路 2:Collections.synchronizedList(标准库给的“加了 synchronized 的 List 包装器”):
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
Runnable r = () -> {
for (int i = 0; i < 1000; i++) {
list.add(i); // 内部关键方法都带 synchronized
}
};
路 3:CopyOnWriteArrayList(写时复制:读写分离)
它的核心思想是:写的时候不在原容器上改,而是 copy 出新容器,写完再把引用指向新容器;读则读旧容器,因此读不需要加锁竞争,适合“读多写少”。代价也很明显:更吃内存,而且新写入的数据不会第一时间被读到。
List<Integer> list = new CopyOnWriteArrayList<>();
// 读多写少的场景更合适
list.add(1);
System.out.println(list.get(0));
2.3 多线程环境下用队列:BlockingQueue 家族直接上
多线程里最常见的模式之一是“生产者-消费者”,这时候用阻塞队列非常省心。常见阻塞队列包括:
ArrayBlockingQueue:数组实现LinkedBlockingQueue:链表实现PriorityBlockingQueue:堆实现(带优先级)TransferQueue:交接型队列(用来做更强的“线程间移交”)
来看一个最经典的生产者-消费者:
BlockingQueue<String> q = new ArrayBlockingQueue<>(3);
// 生产者:放
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
q.put("msg-" + i); // 满了会阻塞
System.out.println("put " + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 消费者:取
new Thread(() -> {
try {
while (true) {
String v = q.take(); // 空了会阻塞
System.out.println("take " + v);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
这类代码的好处是:阻塞/唤醒由队列内部完成,业务线程不用手搓 wait/notify。
2.4 多线程环境下用哈希表:Hashtable vs ConcurrentHashMap
HashMap 不是线程安全的。并发场景下常用两种:
-
Hashtable:给关键方法加了synchronized,锁住的是整个Hashtable对象,效率偏低,key 不允许为 null。 -
ConcurrentHashMap:线程安全,并且为了降低锁竞争做了不少优化:- JDK 1.7 用“分段锁”(Segment)降低冲突概率(同段才竞争)。
- JDK 1.8 取消分段锁,改为“每个桶/链表一把锁”(以链表头结点作为锁对象);结构从“数组+链表”升级为“数组+链表/红黑树”,链表长到一定程度(≥8)会转红黑树;并提到会充分利用 CAS、优化扩容方式,key 不允许为 null。
看一个并发 map 的典型用法:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 并发下“如果没有就放一个默认值”常用 computeIfAbsent
map.computeIfAbsent("k", key -> 0);
// 原子式更新(避免 read-modify-write 的竞态)
map.compute("k", (key, oldVal) -> oldVal + 1);
3. 死锁:线程界的“互相礼让到世界毁灭”
3.1 死锁是什么:线程都卡住了,程序不可能正常结束
死锁就是:多个线程同时被阻塞,一个或全部都在等待某个资源被释放,结果谁也不撒手,线程无限期阻塞,程序无法正常终止。
理解死锁最形象的例子:吃饺子要酱油和醋,两个人一人拿一个,都要求对方先给自己——互不相让,直接卡死。酱油/醋是两把锁,两个人是两个线程。
进一步还有经典“哲学家就餐问题”:如果大家同一时刻都先拿左边筷子,再拿右边筷子,就会发现右边都被占了,于是全员等待,全员死锁。
3.2 死锁产生的四个必要条件(面试必背但更要会用)
死锁成立需要四个条件同时满足:
- 互斥使用:资源被一个线程占有时,其他线程不能用
- 不可抢占:资源只能由占有者主动释放,不能强抢
- 请求并保持:拿着已有资源,还要继续请求新的资源
- 循环等待:形成环路:P1 等 P2 的资源,P2 等 P3 的资源,…,Pn 又等回 P1
只要打破任意一个条件,死锁就消失。最容易下手的是:破坏循环等待。
3.3 如何避免死锁:锁排序(Lock Ordering)
最常用的办法:给锁编号(1…M),所有线程必须按编号从小到大加锁,这样就不会形成等待环路。
来看一段“容易死锁”的代码:两个线程加锁顺序相反。
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
synchronized (lock1) {
// do something...
}
}
});
t1.start();
t2.start();
如果 t1 拿到 lock1、t2 拿到 lock2,然后双方都去等对方的第二把锁,就卡死。
修复方式就是“约定顺序”:都先 lock1 再 lock2。
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
});
t1.start();
t2.start();
锁顺序一致,就不会出现环路等待。
总结
- 需要“有返回值的任务 + 等结果” →
Callable + FutureTask(同步细节少很多)。 - 读多写少的共享 List →
CopyOnWriteArrayList;生产者消费者 →BlockingQueue家族;并发 Map →ConcurrentHashMap。 - 看到“多把锁 + 加锁顺序不一致”就要警觉:锁排序是最常用的死锁规避手段。
更多推荐

所有评论(0)