-
线程池到底是如何运作的?线程是怎么保证不被销毁的呢?
- 1、线程池是什么
- 2、JDK默认提供了哪些线程池
- 3、线程池的实现原理
- 4、任务提交的两种方式
- 5、如何合理地配置线程数
- 6、总结
1、线程池是什么
线程池就是存放线程的一个池子,应用池化技术把创建线程的工作统一交给线程池来管理,就可以避免创建大量的线程带来的开销,以提高响应速度。
那么,JDK给我们提供的默认线程池有哪些呢?
2、JDK默认提供了哪些线程池
2-1、newFixedThreadPool
大小固定,无可用线程时,任务需等待,直到有可用线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
适用场景:负载比较重的服务器,为了资源的合理利用,需要限制当前线程数量
2-2、newCachedThreadPool
大小不受限,当线程释放时,可重用该线程
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
适用场景:并发执行大量短期的小任务,或者是负载较轻的服务器.
2-3、newSingleThreadExecutor
创建一个单线程,任务会按顺序依次执行
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
适用场景:串行执行任务,每个任务必须按顺序执行,不需要并发执行
2-4、newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
适用场景:需要多个后台线程执行周期任务,同时需要限制线程数量
3、线程池的实现原理
3-1、线程池的运行流程
通过JDK默认提供的线程池我们可以知道,他们都是通过ThreadPoolExecutor
来创建的。ThreadPoolExecutor
的全参数的构造方法如下:
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //空闲线程等待工作的超时时间
TimeUnit unit, //超时时间的时间单位
BlockingQueue<Runnable> workQueue, //保存待执行任务的队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler //拒绝策略
) {
//do
}
其中 BlockingQueue
常用的实现有四种
ArrayBlockingQueue
先进先出的基于数组的有界阻塞队列LinkedBlockingDeque
先进先出的基于链表的阻塞队列,其内部维持一个基于链表的数据队列SynchronousQueue
一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作PriorityBlockingQueue
支持优先级的无界阻塞队列:默认情况下元素采取自然顺序升序排列,可以以自定义compareTo()方法来指定元素的排序规则
其中 handler
默认有四种拒绝策略。也可以自定义拒绝策略,实现 RejectedExecutionHandler
接口即可
CallerRunsPolicy
:直接用调用者所在线程来运行任务AbortPolicy
:直接抛出 RejectedExecutionException 异常DiscardPolicy
:直接丢弃任务DiscardOldestPolicy
:丢弃队列中最久的任务,然后再调用execute()
ThreadPoolExecutor
的核心是execute()
方法:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//当前池中线程数比核心线程数少,创建新线程执行任务
if (workerCountOf(c) < corePoolSize) {
//未超过核心线程数,则新增 Worker 对象,true表示核心线程
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);
//线程池为空就新创建一个线程并执行
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//达到核心线程数,队列已满,试着创建新线程,失败则拒绝任务
else if (!addWorker(command, false))
reject(command);
}
执行流程如下图:
3-2、核心线程如何保证不被销毁的
上述 execute()
方法中,addWorker(command, true)
方法部分源码如下:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
...
// 省略代码
...
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//CAS累加
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
}
}
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 {
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 线程添加到workers中
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;
}
其中workers
对象是个Worker
的HashSet集合。
private final HashSet<Worker> workers = new HashSet<Worker>();
Worker
对象部分源码如下:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
...
// 省略代码
...
}
通过源码可知,初始化Worker
对象时,通过Worker
对象创建了一个线程放在成员变量thread
的位置,所以启动线程t.start()
等价于调用Worker
对象的run()
方法。继续看runWorker(this)
方法:
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();
// 省略代码
...
task.run();
...
// 省略代码
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
最终是调用的task.run()
方法,此处为什么不用task.start()
方法呢?或者说可以用这个方法吗?
仔细思考一下,就可以得到答案,不可以。因为如果用的是task.start()
方法,那么会再启动一个线程去执行这个任务,就达不到线程池的效果了。打个比方就是:一个核心线程数为2个线程池,如果有10个任务,会重新创建10个线程来执行这些任务。
上面的runWorker(this)
方法,我们可以看到有个while
循环一直用getTask()
方法取任务,这里是线程池不被销毁的核心,继续看一下getTask()
方法:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 仅在必要时检查队列是否为空
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// workers会被淘汰吗
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 线程池中核心线程不被销毁的重点,阻塞当前线程
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
可以看到在for循环中,通过不断的检查线程池状态和队列容量,来获取可执行任务。
在 Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
代码中,分为两种情况
timed
为true
,允许淘汰Worker
,即实际运行的线程,则通过workQueue.poll
的方式定时等待拉取任务,如果在指定keepAliveTime
时间内获取任务则返回,如果没有任务则继续for循环并直到timed等于false
;timed
为false
,则会调用workQueue.take()
方法,队列中take()
方法的含义是当队列有任务时,立即返回队首任务,没有任务时则一直阻塞当前线程,直到有新任务才返回。这里就可以保持线程一直存活。
3-3、线程池的状态流转
// runState is stored in the high-order bits 状态的定义
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
RUNNING
是运行状态,指可以接受任务执行队列里的任务SHUTDOWN
指调用了shutdown()
方法,不再接受新任务了,但是需要等待队列里的任务执行完毕STOP
指调用了shutdownNow()
方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务TIDYING
所有任务都执行完毕,在调用shutdown()/shutdownNow()
中都会尝试更新为这个状态TERMINATED
终止状态,当执行完terminated()
方法后会更新为这个状态
4、任务提交的两种方式
- execute():提交不需要返回值的任务,不能抛出异常
void execute(Runnable command);
- submit():提交需要返回值的任务,可以把异常继续往外抛
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
5、如何合理地配置线程数
设置线程池的线程数需要针对不同的任务类型而定,任务类型可以分为cpu密集型、IO密集型和混合型。(CPU核心数用Ncpu表示)
- CPU密集型。线程数 = Ncpu + 1
- IO密集型。
cpu所占用时间不多的情况下:线程数 = Ncpu * 2
cpu所占用时间较多的情况下:线程数 = 线 程 等 待 时 间 + 线 程 c p u 时 间 线 程 c p u 时 间 \frac{线程等待时间+线程cpu时间}{线程cpu时间}线程cpu时间线程等待时间+线程cpu时间 * Ncpu - 混合型。根据具体情况来判断,可拆分为IO密集型和CPU密集型
6、总结
全篇介绍了线程池的基本原理,实际使用过程中,需要根据实际情况决定使用何种线程池。