一 使用线程池的好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,不能一直创建。线程池还可以进行调优和监控。
二 如何创建线程池
通过ThreadPoolExecutor 来创建线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize,核心线程池的大小(也是线程的基本大小,先吃池维护的线程数)。当一个任务被提交到线程池的时候,线程池会创建一个线程来执行任务。即使当前有线程是空闲状态也会创建线程。直到线程数等于corePoolSize的时候 才不会在创建。如果调用了线程池的 prestarAllCoreThreads()方法,线程池会提前创建并启动所有的核心线程。需要理解的是,线程池中并没有区分核心线程池和一般线程池,corePoolSize只是一个计数的功能。
- maximumPoolSize,线程池中的最大线程数。线程池允许创建的最大线程数。如果队列满了,并且已经创建的先吃小于最大线程数,则线程池会再创建新的线程去执行任务。如果队列是无界的,则此参数无意义
- keepAliveTime,线程池的工作线程空闲后,保持存活的时间。所有如果任务很多,并且执行时间比较短,可以调大时间,提高线程的利用率。
- TimeUnit ,keepAliveTime的单位。可以是天、小时、分钟、秒、毫秒、微秒,纳秒等
- workQueue;任务队列,用于保持等待执行的任务的阻塞队列。可以选择的队列如下:
- ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列,按照FIFO原则对元素进行排序
- LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列,按照FIFO原则对元素进行排序。吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool 使用的这个队列。
- PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列。
- SynchronousQueue 一个不储存元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常高于 LinkedBlockingQueue。Executors.newCahcedThreadPool 使用的这个队列。
- threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。默认才用 Executors.defaultThreadFactory() 来创建的线程工厂。可以通过开源框架guava提供的ThreadFactoryBuilder来创建有意义的名字,代码如下:
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
- RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态了,那么必须采取一种策略处理提交的新任务。默认才用的是new AbortPolicy(),表示无法处理的时候直接抛出异常:
- AbortPolicy:直接抛出异常
- DiscardPolicy:不处理,并且只丢弃
- DiscardOldestPolicy,丢弃队列里最近的一个任务,并执行当前任务
- CallerRunsPolicy 只用调用者所在线程来运行任务
三 使用线程池的实现原理
结合代码 ThreadPoolExecutor 的源代码 分析代码原理。
如上图所示:
- 当执行了 execute()之后的代码流程如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//判断线程数量小于 corePoolSize,则创建一个新的核心线程数,并执行当前command任务,详细见下面的 addWorker方法解析
if (workerCountOf(c) < corePoolSize) {
//true表示核心线程数 创建成功之后直接返回,否则执行下面的
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程数量大于 corePoolSize,则想队列里面添加一个任务,offer返回true表示入队成功,否则入队失败
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果入队失败,说明队列已经满了,则创建普通的线程
else if (!addWorker(command, false))
reject(command);
}
- addWorker方法是如何添加工作线程并启动的呢, firstTask为execute()启动传入的任务。core表示当前worker是不是核心线程类型
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
//... 省略部分代码
for (;;) {
int wc = workerCountOf(c);
//通过core的类型 来判断线程数量为多少的时候 返回false
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//通过case循环操作 来实现原子自增工作线程的数量。 c是一个原子类,记录工作线程的数量
if (compareAndIncrementWorkerCount(c))
break retry;
//... 省略部分代码
}
}
boolean workerStarted = false; boolean workerAdded = false;
Worker w = null;
try {
//通过提交的任务 创建当前的工作线程
w = new Worker(firstTask);
//获取当前更新线程的线程类型,通过Worker的线程可以得知,t.start 会执行 Worker.run方法
final Thread t = w.thread;
if (t != null) {
//创建线程 创建新的线程是需要获取全局锁的
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
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();
}
//创建成功并加入到工作线程集合之后,启动工作线程。t.start()即使执行 Worker.run方法
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
//线程启动失败的话,,需要从工作线程集合中移除调,并把线程数减去1 这样不影响其他任务的提交
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
- Worker线程是怎么启动,并且怎么获取任务的呢?
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
//定义的当前线程
final Thread thread;
//需要执行的任务
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
//... 省略部分代码
}
通过查询代码可以得知,Worker继承类AQS同步器,所以他本身现实了锁的功能,并且实现了 Runnable接口,并通过线程工厂把自己封装到了私有方法 thread中,这就是 thread.start的时候,会启动Worker的run方法的原因。
public void run() {
runWorker(this);
}
//启动工作线程
final void runWorker(Worker w) {
//获取当前的先吃
Thread wt = Thread.currentThread();
//获取当前Worker中需要执行的任务
Runnable task = w.firstTask;
//并把当前worker的工作任务设置为null
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//如果当前取出来的task不为空,则进入循环提,否则从任务队列中取出一个任务并赋值给task
while (task != null || (task = getTask()) != null) {
//获取当前工作线程的线程锁,一个工作线程,同一个时刻只能执行一个任务
w.lock();
//判断当前主线程是否中断???
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//线程启动之前的操作,wt为当前主线程,task为当前需要执行的任务
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
//执行之后的操作
//如果有异常情况 会提前结束循环 并跳出循环体
afterExecute(task, thrown);
}
} finally {
//当task执行完成之后,把该任务赋值为null,然后继续循环的时候 从任务队列中继续读取任务
task = null;
//记录当前线程完成的任务数量
w.completedTasks++;
//释放工作线程锁
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 当推出循环体,说明当前工作队列已经为空了。那把把改工作线程从 工作线程集合中 移除
//并且会保留 corePoolSize个线程 ??是如何保留的呢?
processWorkerExit(w, completedAbruptly);
}
}
四 线程池的原理总结
-
线程池是什么时候创建线程的?
在提交任务的时候,创建的线程。并且创建的线程会先执行当前任务,然后在从队列中读取 -
任务runnable task是先放到core到maxThread之间的线程,还是先放到队列?
由上面的流程图可得知,当达到了 corePoolSize的时候,会先放到队列中,等队列满了,才会去创建 corePoolSize到 maximumPoolSize个线程,并把任务放到新创建的线程里面来。 -
队列中的任务是什么时候取出来的?
从 runWorker()源码分析来看,当线程执行了 创建的任务之后,,循环从队列中读取,直到队列里面为空。 -
什么时候会触发reject策略?
从上面的流程图可以得知,当队列满了,并且工作线程数达到了 maximumPoolSize之后 会触发reject策略。 -
core到maxThread之间的线程什么时候会die?
没有任务的时候,或者 在执行任务有异常的时候。 -
task抛出异常,线程池中这个work thread还能运行其他任务吗?
不能了,如果task抛出异常,当前工作线程会死掉,工作线程数也会减1,后面有新的任务进来,通过策略可能又回重新创建一个线程。 -
如何理解 corePoolSize?
corePoolSize只是一个数字,并不是真的有核心线程池和 普通线程池的区别,只是一个在有任务的时候,线程池里面最小维护的线程数。
五 合理的配置线程池
想要合理的配置线程池,需要对任务进行分析
- 任务的类型:CPU密集型任务、IO密集型任务和混合型任务
- 任务的优先级:高中低
- 任务的执行时间:长中短
- 任务的依赖性:是否依赖其他系统资源
什么是CPU密集任务、什么是IO密集任务?
具体参考
-
CPU密集任务:任务需要进行大量的计算,消耗CPU资源。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
-
IO密集:涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成。 对于IO密集型任务,任务越多,CPU效率越高,同时进行的线程数可以设置在CPU的核心数的2倍。
-
混合型任务:可以拆分成一个CPU密集和一个IO密集的自任务,然后在执行。
-
优先级的任务可以使用优先级队列来处理买。从优先级高的先执行
-
对于执行时间的任务,可以通过优先级队列 ,让执行时间短的任务先执行。
-
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则cpu空闲时间就越长,那么线程数应该设置的越大,这样才能充分利用CPU
-
另外在设置队列的时候 尽量使用有界队列。
六 线程池的监控
- taskCount:线程池需要执行的任务数
- completedTaskCount:线程池在运行过程中已完成的任务数量。
- largestPoolSize:线程池里面曾经创建过的最大线程数量,可以通过这个参数来判断线程池曾经是否满过
- getPoolSize:线程池的线程数量
- getActiveSize:获取活跃的线程数
七 手写一个线程池
demo地址
https://gitee.com/luffyu/manually/tree/master/manually_pool