← 返回首页

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:使用 CompletableFutureAsyncHttpClient 等异步框架。
  • 设置超时:避免无限等待。
// 设置读取超时
socket.setSoTimeout(5000); // 5秒超时

锁竞争阻塞

多个线程竞争同一把锁,未获取到锁的线程进入 BLOCKED 状态。

// 问题:大量线程竞争同一把锁
synchronized (lock) {
    // 临界区代码
}

解决方案

  • 减小锁粒度:将大锁拆分为多个小锁,减少竞争范围。
  • 使用读写锁:读多写少场景用 ReentrantReadWriteLock
  • 使用 CAS 无锁操作:用 AtomicIntegerAtomicReference 等替代锁。
  • 锁分段:类似 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):设置超时时间,避免死等。
  • 使用 ConditionReentrantLock 的条件变量,支持精确唤醒。
  • 使用 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


总结:

 线程池的核心是合理配置参数 + 有界队列 + 拒绝策略,避免资源耗尽。线程阻塞的本质是资源竞争或等待,解决思路是:能用异步就不用同步,能用无锁就不用锁,能超时等待就不无限等待,能减小锁粒度就不锁全局。