Java并发编程实战:线程池的正确姿势

165 阅读5分钟

在Java的江湖中,并发编程可谓是一门玄之又玄的武功。而线程池,则是这门武功中的一个重要招式。今天,让我们一起来探讨如何正确地使用这招"线程池"。

为什么要用线程池?

想象一下,如果你是一个餐厅老板,每来一个客人就雇佣一个新厨师,客人走了就解雇厨师。这样做不仅成本高昂,而且效率低下。同理,在Java中频繁地创建和销毁线程也是一种资源的浪费。

线程池就像是预先雇佣了一批厨师,有客人来就安排空闲的厨师去工作,没客人时厨师就待命。这样既节省了资源,又提高了效率。妙哉,妙哉!

线程池的基本使用

Java提供了Executors工厂类来创建线程池,但是!且慢!别急着用。正所谓"工欲善其事,必先利其器",我们先来看看基本用法:

ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
    executor.execute(() -> {
        System.out.println("Thread " + Thread.currentThread().getName() + " is running task.");
    });
}
executor.shutdown();

这段代码创建了一个固定大小为5的线程池,然后提交了10个任务。看起来很简单,对吧?但是,这里面暗藏玄机啊!

Executors的陷阱

Executors提供的工厂方法看似方便,实则暗藏杀机。就像武林中的"葵花宝典",练不好就会走火入魔。

  1. newFixedThreadPoolnewSingleThreadExecutor: 允许的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,导致OOM。

  2. newCachedThreadPoolnewScheduledThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能创建大量的线程,导致OOM。

所以,我们需要的是"可控"的线程池。就像是武功要收发自如,线程池也要在我们的掌控之中。

ThreadPoolExecutor:真正的大侠

要想成为并发编程的大侠,就得学会使用ThreadPoolExecutor。这才是真正的"降龙十八掌"!

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                       // 核心线程数
    10,                      // 最大线程数
    60L, TimeUnit.SECONDS,   // 空闲线程存活时间
    new LinkedBlockingQueue<Runnable>(100), // 任务队列
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

这个构造方法就像是武功秘籍,每个参数都有其深意:

  1. 核心线程数: 类似于餐厅的常驻厨师数量。
  2. 最大线程数: 相当于餐厅最多能雇佣的厨师数量。
  3. 空闲线程存活时间: 厨师等了多久没活干就可以下班了。
  4. 任务队列: 存放等待执行的任务,就像餐厅的订单队列。
  5. 拒绝策略: 当厨师都忙不过来,订单队列也满了,新来的订单该怎么处理。

任务队列:案板上的菜

任务队列就像是厨师案板上的菜,决定了厨师的工作节奏。常见的队列有:

  1. ArrayBlockingQueue: 固定大小的队列,就像是固定大小的案板。
  2. LinkedBlockingQueue: 可以设置容量,但如果不设置,默认就是Integer.MAX_VALUE。小心案板无限延伸!
  3. SynchronousQueue: 不存储元素的队列,每个插入操作必须等待另一个线程的移除操作。就像是"即点即做"的快餐店。
  4. PriorityBlockingQueue: 优先级队列,可以根据任务的优先级来执行。某些VIP客人的订单可以插队。

拒绝策略:客满的餐厅如何应对

当线程池忙不过来时,就需要拒绝策略来应对:

  1. AbortPolicy: 直接抛出异常,拒绝新任务。就像是直接跟客人说"不好意思,我们店客满了"。
  2. CallerRunsPolicy: 直接在调用者线程中运行任务。相当于老板自己下场炒菜。
  3. DiscardPolicy: 悄悄丢弃任务,不通知客人。(这可不是什么好习惯!)
  4. DiscardOldestPolicy: 丢弃队列中最旧的任务,然后重新提交被拒绝的任务。就像是把等待最久的客人请出去,让新客人进来。

监控线程池:掌柜要精打细算

作为一个称职的掌柜,你需要时刻关注线程池的状态:

ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);

// 获取线程池的一些统计信息
System.out.println("Pool size: " + executor.getPoolSize());
System.out.println("Active threads: " + executor.getActiveCount());
System.out.println("Completed tasks: " + executor.getCompletedTaskCount());

你还可以继承ThreadPoolExecutor类,重写beforeExecute()afterExecute()terminated()方法来添加自定义的监控逻辑。就像是掌柜亲自盯着每个厨师的工作情况。

合理配置线程池:找到最佳平衡点

配置线程池就像是武林高手调教自己的内力,需要根据实际情况来权衡:

  1. CPU密集型任务: 线程数 = CPU核心数 + 1。让CPU忙起来,别闲着。
  2. I/O密集型任务: 线程数 = CPU核心数 * (1 + I/O耗时/CPU耗时)。在I/O等待时,让CPU去处理其他任务。

记住,线程池不是越大越好。就像厨师不是越多越好,太多了反而会互相干扰。

总结:练好这招"线程池"

线程池就像是武林中的一门绝学,用好了可以让你的程序如虎添翼,用不好则可能会走火入魔。记住以下几点:

  1. 避免使用Executors创建线程池,选择手动创建ThreadPoolExecutor
  2. 根据任务类型选择合适的任务队列。
  3. 选择合适的拒绝策略,优雅地处理过载情况。
  4. 监控线程池状态,及时调整参数。
  5. 根据任务类型合理配置线程池大小。

最后,记住一句话:"工欲善其事,必先利其器"。只有真正理解了线程池的工作原理,才能在并发编程的江湖中游刃有余。现在,去实践吧,年轻的武林高手!

海码面试 小程序

包含最新面试经验分享,面试真题解析,全栈2000+题目库,前后端面试技术手册详解;无论您是校招还是社招面试还是想提升编程能力,都能从容面对~