线程是一个重量级的对象,应该避免频繁创建和销毁。 java SDK提供的线程池不同于其他池化资源,没有提供申请线程和释放线程的方法。
1. 线程池是一种生产者-消费者模式
线程池的使用方是生产者,线程池本身是消费者。以下代码简要说明线程池的原理:
//简化的线程池,仅用来说明工作原理
class MyThreadPool {
//利用阻塞队列实现生产者-消费者模式
BlockingQueue<Runnable> workQueue;
//保存内部工作线程
List<WorkerThread> threads = new ArrayList<>();
// 构造方法
MyThreadPool(int poolSize, BlockingQueue<Runnable> workQueue) {
this.workQueue = workQueue;
// 创建工作线程
for (int idx = 0; idx < poolSize; idx++) {
WorkerThread work = new WorkerThread();
work.start();
threads.add(work);
}
}
// 提交任务
void execute(Runnable command) {
workQueue.put(command);
}
// 工作线程负责消费任务,并执行任务
class WorkerThread extends Thread {
public void run() { // (1)
// 循环取任务并执行
while (true) {
Runnable task = workQueue.take();
task.run();
}
}
}
}
/** 下面是使用示例 **/
// 创建有界阻塞队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
// 创建线程池
MyThreadPool pool = new MyThreadPool(10, workQueue);
// 提交任务
pool.execute(()->{
System.out.println("hello");
});
2. 如何使用Java中的线程池
最核心的是ThreadPoolExecutor, 最复杂的构造函数:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize,表示线程池保有的最小线程数;
- maximumPoolSize,表示线程池创建的最大线程数;
- keepAliveTime & unit,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了;
- workQueue,工作队列,用来储存线程;
- threadFactory:自定义如何创建线程,例如给线程指定一个有意义的名字;
- handler:自定义任务的拒绝策略。有四种策略:
1.CallerRunsPolicy:提交任务的线程自己去执行该任务。
2.AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException。
3.DiscardPolicy:直接丢弃任务,没有任何异常抛出。
4.DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
3. 使用线程池要注意些什么
不建议使用线程池的静态工厂Executors的最重要的原因是:Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue,高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。
默认拒绝策略要慎重使用。 如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
虽然线程池提供了很多用于异常处理的方法,但是最稳妥和简单的方案还是捕获所有异常并按需处理,你可以参考下面的示例代码。
try {
//业务逻辑
} catch (RuntimeException x) {
//按需处理
} catch (Throwable x) {
//按需处理
}