六、线程池
1、使用优点
-
降低资源消耗
可以重复利用已创建的线程,降低线程创建和销毁造成的消耗
-
提高响应速度
当任务到达时,可以不用等到线程创建就能立即执行
-
提高线程的可管理性
线程是稀缺资源,无限地创建,不仅会消耗系统资源,还会降低系统的稳定性。而使用线程池可以进行统一分配、调优和监控
2、线程池的创建
通过java.util.concurrent.ThreadPoolExecutor
来创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
corePoolSize:核心线程数
maximumPoolSize:最大线程数(包括核心线程数)
keepAliveTime:非核心线程的存活时间(数值)。非核心线程(空闲状态)等待执行新的任务的终结最大时长(wait for new tasks before terminating)java.lang.Thread.State: WAITING (parking)
unit:非核心线程的存活时间(单位)
workQueue:线程任务队列
threadFactory:生成线程的方法工厂。比如使用默认的静态方法创建的线程工厂java.util.concurrent.Executors#defaultThreadFactory
handler:线程任务拒绝策略。比如使用默认的拒绝执行策略java.util.concurrent.ThreadPoolExecutor.AbortPolicy
/* 会直接抛出异常 */
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
3、工作方式
- 当执行线程任务时,先判断是否达到核心线程数(corePoolSize),如果没有达到,则会创建新的线程来执行线程任务,否则就将线程任务加入到阻塞队列中(BlockingQueue)
- 当阻塞队列已满时,并且还未达到最大线程数(maximumPoolSize)时,会创建新的线程来执行线程任务
- 当达到最大线程数时,如果还需要执行线程任务,则会执行拒绝策略(handler)
- 当额外的线程(非核心线程)空闲时,且达到了最大等待时间,那么它们就会被终结(TERMINATED)
注意:当创建新的线程时,需要获取全局锁
/**
* Lock held on access to workers set and related bookkeeping.
* While we could use a concurrent set of some sort, it turns out
* to be generally preferable to use a lock. Among the reasons is
* that this serializes interruptIdleWorkers, which avoids
* unnecessary interrupt storms, especially during shutdown.
* Otherwise exiting threads would concurrently interrupt those
* that have not yet interrupted. It also simplifies some of the
* associated statistics bookkeeping of largestPoolSize etc. We
* also hold mainLock on shutdown and shutdownNow, for the sake of
* ensuring workers set is stable while separately checking
* permission to interrupt and actually interrupting.
*/
private final ReentrantLock mainLock = new ReentrantLock();
private boolean addWorker(Runnable firstTask, boolean core) {
// ... ...
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
虽然可以使用并发的手段,但是最好还是使用锁。因为一方面可以确保线程安全,使得线程池运行稳定;另一方面也简化了一些操作(线程池源码实现)
4、最佳实践
- 合理配置最大线程数
- CPU密集型任务:CPU核数 + 1【减少线程的切换】
- IO密集型任务 [计算方式一]:CPU核数 × 2
- IO密集型任务 [计算方式二]:CPU核数 / (1 - 阻塞系数),阻塞系数在 0.8 ~ 0.9之间【提高吞吐量】
- 合理配置非核心线程的存活时间(keepAliveTime),假如需要执行的任务有很多,且任务执行时间较短,则可以调大这个时间,以提高线程的利用率
- 合理配置阻塞队列的大小,如果太大,不仅无法执行到任务,还有可能耗尽内存资源
- 关闭线程池有2种方法,
shutdown
是只关闭线程池,但未完成的任务还是会让其执行完;shutdownNow
是关闭线程池,但未完成的也要终结