← 返回首页
Java 线程池了解过吗,线程阻塞怎么解决
Java 线程池
线程池是 Java 并发编程中最核心的组件之一,核心类是 ThreadPoolExecutor,通过复用线程避免频繁创建和销毁线程的开销。
线程池的核心参数
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻线程,即使空闲也不回收)
int maximumPoolSize, // 最大线程数(核心线程 + 临时线程的上限)
long keepAliveTime, // 非核心线程的空闲存活时间
TimeUnit unit, // 存活时间的单位
BlockingQueue<Runnable> workQueue, // 任务等待队列
ThreadFactory threadFactory, // 线程工厂(可自定义线程名)
RejectedExecutionHandler handler // 拒绝策略(队列满且线程满时触发)
)
任务提交流程(核心逻辑)
提交任务
│
▼
核心线程数是否已满?
├─ 否 → 创建核心线程执行任务
└─ 是 → 任务放入阻塞队列
│
▼
队列是否已满?
├─ 否 → 任务在队列中等待
└─ 是 → 线程数是否达到最大值?
├─ 否 → 创建非核心线程执行任务
└─ 是 → 执行拒绝策略
四种拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) |
直接抛出 RejectedExecutionException |
CallerRunsPolicy |
由提交任务的线程自己执行该任务(降级处理) |
DiscardPolicy |
静默丢弃任务,不抛异常 |
DiscardOldestPolicy |
丢弃队列中最旧的任务,重新提交当前任务 |
常用内置线程池(了解但不推荐直接使用)
| 类型 | 特点 | 风险 |
|---|---|---|
FixedThreadPool |
固定线程数,无界队列 | 队列无限,任务堆积可能 OOM |
CachedThreadPool |
线程数无上限,60s 回收 | 可能创建大量线程导致 OOM |
SingleThreadExecutor |
单线程串行执行 | 队列无限,可能 OOM |
ScheduledThreadPool |
定时/周期任务 | 队列无限,可能 OOM |
《阿里巴巴 Java 开发手册》明确要求:线程池不允许使用 Executors 创建,而是通过 ThreadPoolExecutor 手动创建,规避资源耗尽风险。
线程阻塞的原因与解决方案
线程阻塞通常有以下几种场景:
I/O 阻塞
线程在执行网络请求、文件读写时等待 I/O 完成。
// 问题:线程在等待网络响应时被阻塞
InputStream in = socket.getInputStream();
int data = in.read(); // 阻塞,直到有数据可读
解决方案:
- NIO(非阻塞 I/O):使用
Selector+Channel,一个线程可以管理多个连接。 - 异步 I/O:使用
CompletableFuture、AsyncHttpClient等异步框架。 - 设置超时:避免无限等待。
// 设置读取超时
socket.setSoTimeout(5000); // 5秒超时
锁竞争阻塞
多个线程竞争同一把锁,未获取到锁的线程进入 BLOCKED 状态。
// 问题:大量线程竞争同一把锁
synchronized (lock) {
// 临界区代码
}
解决方案:
- 减小锁粒度:将大锁拆分为多个小锁,减少竞争范围。
- 使用读写锁:读多写少场景用
ReentrantReadWriteLock。 - 使用 CAS 无锁操作:用
AtomicInteger、AtomicReference等替代锁。 - 锁分段:类似
ConcurrentHashMap的分段锁思想(JDK 7),或 JDK 8 的 CAS + synchronized 细粒度锁。
// CAS 无锁方案
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 无锁自增
等待/通知阻塞
线程调用 wait()、join()、LockSupport.park() 等方法主动等待。
// 问题:线程无限期等待
synchronized (obj) {
obj.wait(); // 阻塞,直到被 notify
}
解决方案:
- 使用
wait(timeout):设置超时时间,避免死等。 - 使用
Condition:ReentrantLock的条件变量,支持精确唤醒。 - 使用
CountDownLatch/CyclicBarrier:替代原始的 wait/notify,更安全。
// 带超时的等待
synchronized (obj) {
obj.wait(3000); // 最多等3秒
}
// CountDownLatch 替代 wait/notify
CountDownLatch latch = new CountDownLatch(3);
latch.await(5, TimeUnit.SECONDS); // 最多等5秒
线程池队列堆积阻塞
任务提交速度远大于处理速度,队列堆积导致线程饥饿。
解决方案:
- 合理配置队列大小:根据业务评估 QPS 和处理耗时,设置合理的队列容量。
- 有界队列 + 拒绝策略:使用
ArrayBlockingQueue而非LinkedBlockingQueue。 - 动态扩缩容:监控队列积压情况,动态调整线程池大小。
// 有界队列,防止无限堆积
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
死锁
两个或多个线程互相持有对方需要的锁,形成循环等待。
解决方案:
- 固定加锁顺序:所有线程按相同顺序获取锁。
- 使用
tryLock(timeout):尝试获取锁,超时则放弃,打破死锁。 - 死锁检测工具:使用
jstack、JVisualVM 等工具排查。
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
// 超时获取锁,避免死锁
if (lock1.tryLock(3, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(3, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
排查线程阻塞的常用工具
| 工具 | 用途 |
|---|---|
jstack <pid> |
导出线程快照,查看线程状态和锁信息 |
jvisualvm |
可视化监控线程状态 |
Arthas(阿里开源) |
在线诊断,thread -b 直接定位阻塞线程 |
jconsole |
JDK 自带,监控线程和内存 |
# 使用 jstack 排查
jstack <pid> | grep -A 20 "BLOCKED"
使用 Arthas 定位死锁
thread -b