使用线程池的好处:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁的开销
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性:使用线程池可以统一分配,调优以及监控,提高稳定度。
线程池的实现
整个线程池的主要流程如下所示。
以ThreadPoolExecutor执行execute方法为例,具体的流程就是
- 如果当前运行的线程少于corePoolSize(自行设定,最大值(1 << Integer.SIZE - 3) - 1),下面maximumPoolSize也一样),则创建新线程来执行任务(会获取全局锁)
- 如果允许的线程等于或多于corePoolSize,则将任务假如BlockingQueue
- 如果BlockingQueue已满,则创建新的线程来执行任务(会获取全局锁)
- 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,抛出异常。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
ctr是workerCount和 runState的或运算的结果
就是涵盖了运行线程数量与运行状态
int c = ctl.get();
如果当前工作的线程数小于核心线程数
if (workerCountOf(c) < corePoolSize) {
尝试增加一个工作者(工作线程)
if (addWorker(command, true))
return;
失败了说明被其他线程提交的任务抢占先机,重写获取状态
c = ctl.get();
}
如果当前状态仍然在运行(就是线程池还未关闭),尝试将任务入队
if (isRunning(c) && workQueue.offer(command)) {
如果成功了,则再次获取状态
int recheck = ctl.get();
如果线程池关闭了,则尝试移除线程
if (! isRunning(recheck) && remove(command))
如果移除成功,拒绝程序运行
reject(command);
如果当前工作线程为0,则新建一个空任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
如果添加失败说明等待队列满了,再次尝试新建一个工作线程。
else if (!addWorker(command, false))
如果失败说明线程池满了,拒绝任务。
reject(command);
}
任务执行:
执行完当前任务后就获取任务队列里的任务,直到线程池关闭或者任务队列为空。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
获取本次任务
Runnable task = w.firstTask;
清空传递进来的本次任务
w.firstTask = null;
解锁,又阔以传递新任务进来啦
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
如果任务不为空或者任务队列还能获取到任务
while (task != null || (task = getTask()) != null) {
再次获取锁
w.lock();
如果线程池被关闭,如果没有中断则中断它
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
执行之前,可用于初始化ThreadLocals或者执行日志之类
beforeExecute(wt, task);
try {
执行任务
task.run();
正常执行完
afterExecute(task, null);
} catch (Throwable ex) {
使得线程异常终止的异常为ex
afterExecute(task, ex);
throw ex;
}
} finally {
将task置为null,来获取下一个任务
task = null;
本线程执行的任务+1
w.completedTasks++;
w.unlock();
}
}
没有任务可以完成了
completedAbruptly = false;
} finally {
退出本线程
processWorkerExit(w, completedAbruptly);
}
}
线程池的使用
初始化
public ThreadPoolExecutor(int corePoolSize,
int imaximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:线程池基本大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即时有空闲线程也会创建,只要当前线程数少于此值。如果大于此值基本就不再创建了(**prestartAllCoreThreads()**可以提前创建并启动所有基本线程)
- workQueue:用于保存待执行工作的阻塞队列。可以选择ArrayBlockingQueue(基于数组的有界阻塞队列,FIFO),LinkedBlockingQueue(基于链表的的阻塞队列FIFO,吞吐高于上一个,Executors.newFixedThreadPool()使用了该队列),SynchronousQueue(不存储元素的阻塞队列,每个插入操作需等待另一个线程调用移除操作,吞吐量高于上一个,Executors.newCechedThreadPool()使用的队列),PriorityBlockingQueue(具有优先级的无限阻塞队列)
- imaximumPoolSize:线程池最大数量,当队列满时,则会创建新的线程执行任务,如果使用无界阻塞队列,则没有什么意义。
- threadFactory:用于设置创建线程的工厂,可通过线程工厂给每个创建出的线程设置更有意义的名字。
- handler:饱和拒绝策略,当任务队列和线程池都满了时,则会使用此策略来处理新任务。JDK1.5(11中还是这4种)中提供了五种策略AbortPolicy(抛异常),CallerRunsPolicy(只用调用者所在线程执行任务),DiscardOldestPolicy(丢弃队列中最近的一个任务,并执行当前任务),DiscardPolicy(丢弃当前任务)
- keepAliveTime:线程活动保持时间,线程空闲时仍保持存活的时间,如果任务都比较短且多,可适当调高该值。
- unit:上一个值的时间单位。
提交任务
execute没有返回值,且立即执行(如果可以的话)
submit:有返回值,如果需要返回值的话,可以采用此方法,而且不会立即执行(毕竟需要存储返回值)
关闭线程池:
有shutdown和shutdownNow两种方式,都是遍历线程池的工作线程,逐个中断(无法响应中断的任务可能永远无法终止)。都是先将状态置为STOP,不过shutdownNow会立即去中断所有线程,而shutdown只中断空闲线程。
所以如果当前任务需要执行完,则使用shutdown,如果不需要则可以使用shutdownNow。
合理配置线程池
首先对任务性质进行分类:
- 任务性质:CPU密集(线程数量尽可能小,如Ncpu+1),IO密集(尽可能多,Ncpu*2),混合型(如果可以拆分且执行时间差异不大为CPU密集和IO密集,用不同规模的线程池处理)
- 任务的优先级(可采用PriorityBlockingQueue):高,中,低
- 任务的执行时间(可以分给不同规模的线程池处理或者将短时间的任务尽量采用较高优先级,不过需要注意如果一直有高优先级任务存在,可能低优先级永远不会执行):长,中,短
- 任务的依赖性(最好不要使用无界队列(任何情况下),防止过久的等待占用过多线程,而导致撑爆内存):是否依赖其他资源,如数据库连接。
线程池的监控
可通过以下几个值才监控线程池,并通过继承现称此来自定义线程池,重写beforExecute()、afterExecute()、terminated()方法。
- public int getLargestPoolSize():执行过程中线程数最大时的数量
- public long getCompletedTaskCount():线程池中已完成的任务
- public int getPoolSize() :线程池的线程数量
- public int getTaskCount():获取需要执行的任务数量
- public int getActiveCount():获取活动线程数