线程池简介:
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(WorkerThreads):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(FutureTask):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
线程池原理:
(1)线程池的状态
线程池一共有五种状态:
- RUNNING:接收新任务和处理队列任务 。
- SHUTDOWN:不接收新任务,但处理队列里的任务 。
- STOP:不接收新任务,也不处理队列任务且中断正在执行的任务 。
- TIDYING:所有任务都终止,工作线程数为0,将执行terminated()方法。
- TERMINATED: terminated() 执行完成。
以下是线程池状态的转换过程:
这里看出SHUTDOWN和STOP的区别:
当线程池处于SHUTDOWN状态,那么线程池拒绝接受新的任务,但会继续执行阻塞队列中的任务。
当线程池处于STOP状态,不接收新任务,也不处理队列任务且中断正在执行的任务 。
(2)线程池处理任务流程
当向线程池提交一个任务之后,线程池是如何处理这个任务的呢?本节来看一下线程池
的主要处理流程,处理流程图所示。
从上图可以看出,线程池对于任务的处理流程:
- 核心线程池未满,则创建线程加入核心线程池执行任务。
- 核心线程池满了,但是阻塞队列未满,就将任务加入阻塞队列,等待工作线程执行。
- 阻塞队列满了,但是线程池还没有满,就创建线程加入线程池执行任务。
- 如果线程池满了,就按照策略执行拒绝执行的任务。
(3)线程回收的情况
- 线程池状态为STOP
- 线程池状态为SHUTDOWN且任务阻塞队列为空。
- 工作线程超过了最大线程数maximumPoolsize
- 获取超时,超过了 线程池所维护的线程的活动时间。
第四点单独说一下:keepAliveTime : 线程池所维护的线程的活动时间。如果超过了该时间范围,而线程还是空闲的,那么该线程将会被回收。不再由线程池所维护。以下是官方的一些说明: 如果当前线程池数大于核心线程池数,且这些线程是空闲的超过所设定的存活时间keepAliveTime,那么这些多出来的线程则会被终止,可以降低线程资源消耗。通过设置尽可能大的过期时间来阻止空闲线程被销毁,可手动调用setKeepAliveTime来设置。默认情况下,该策略只能适用于大于核心线程数的线程,但是可以通过设置ThreadPoolExecutor.allowCoreThreadTimeOut(boolean),则该策略也适用于核心线程数。
继承关系:
ThreadPoolExecutor 源码分析:
内功心法:字段分析
public class ThreadPoolExecutor extends AbstractExecutorService {
//共享变量 32位整数,左边3位是线程池状态,后面29位是线程池中的工作线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//工作线程位数29位
private static final int COUNT_BITS = Integer.SIZE - 3;
//00011111111111111111111111111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 五种线程池状态,前面分析过了
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;
//获取线程池状态 c&11100000000000000000000000000000 就是获取前三位的值
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
}
//任务阻塞队列,生产消费者模式
private final BlockingQueue<Runnable> workQueue;
//同步锁,通过锁来实现多线程同步。
private final ReentrantLock mainLock = new ReentrantLock();
//工作线程池
private final HashSet<Worker> workers = new HashSet<Worker>();
private final Condition termination = mainLock.newCondition();
//曾经最大线程池大小
private int largestPoolSize;
//完成任务总数
private long completedTaskCount;
//线程工厂,生产线程
private volatile ThreadFactory threadFactory;
//拒绝策略
private volatile RejectedExecutionHandler handler;
//线程空闲情况存活时间
private volatile long keepAliveTime;
//允许回收核心线程
private volatile boolean allowCoreThreadTimeOut;
//核心线程数
private volatile int corePoolSize;
//最大线程数
private volatile int maximumPoolSize;
以下是参数说明:
corePoolSize : 线程池的核心线程数。该参数并不是初始化时候写死了,线程池对象构造完成以后也能通过它提供的一些方法动态修改,ThreadPoolExecutor.setCorePoolSize。
maximumPoolSize : 线程池的最大线程数,只有当任务队列满了才会创建新的线程。该参数并不是初始化时候写死了,线程池对象构造完成以后也能通过它提供的一些方法动态修改,ThreadPoolExecutor.setMaximumPoolSize。
keepAliveTime : 线程池所维护的线程的活动时间。如果超过了该时间范围,而线程还是空闲的,那么该线程将会被回收。不再由线程池所维护。以下是官方的一些说明: 如果当前线程池数大于核心线程池数,且这些线程是空闲的超过所设定的存活时间keepAliveTime,那么这些多出来的线程则会被终止,可以降低线程资源消耗。通过设置尽可能大的过期时间来阻止空闲线程被销毁,可手动调用setKeepAliveTime来设置。默认情况下,该策略只能适用于大于核心线程数的线程,但是可以通过设置ThreadPoolExecutor.allowCoreThreadTimeOut(boolean),则该策略也适用于核心线程数。
threadFactory : 传入一个线程工厂。通过该线程工厂可以创建线程。它创建的所有线程都是通过同样的ThreadGroup和同样的NORM_PRIORITY和non-daemon状态。通过提供的不同ThreadFactory,可以掌握修改线程名字,线程组,优先级,守护状态等。如果线程池调用线程工厂创建一个线程失败时,则返回一个null。且executor会继续,但是可能不会执行任何任务。 如果我们自己重写封装了一遍线程工厂,还有个好处就是可以通过该线程工厂实例维护所有由它创建的线程。
人剑合一:源码函数分析
submit(Callable or Runnable)
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
//采用适配器模式将任务封装
RunnableFuture<T> ftask = newTaskFor(task);
//调用子类ThreadPoolExecutor的实现
execute(ftask);
//返回任务句柄,该句柄可以获取任务执行情况和结果,也可以取消任务。
return ftask;
}
execute(Runnable command)
//execute()对应了线程池执行任务的四步流程,前面已经说过
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//1.核心线程池不满
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//2.核心线程池满了,任务阻塞队列不满
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);
}
//3.任务阻塞队列满了,线程池不满
else if (!addWorker(command, false))
//4.线程池满了,按照策略处理无法执行的任务。
reject(command);
}
boolean addWorker(Runnable firstTask, boolean core)
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 1.如果线程池状态为SHUTDOWN,firstTask 为空(没有新任务)且任务阻塞队列不为空,才创建Worker去执行阻塞队列任务,否则返回false
// 2.如果线程池状态>=STOP,返回false
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
//3.如果工作线程数大于(是核心? 核心线程数 : 最大线程数),返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
4.如果逃过了上面三个鬼门关,就来到了这里,创建新的Worker,并启动这个Worker的线程
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
final ReentrantLock mainLock = this.mainLock;
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int c = ctl.get();
int rs = runStateOf(c);
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;
}
当执行execute()添加任务时,拒绝创建新线程的情况:
- 如果线程池状态为SHUTDOWN,firstTask 为空(没有新任务)且任务阻塞队列不为空,才创建Worker去执行阻塞队列任务,否则返回false。
- 如果线程池状态>=STOP,返回false。
- 如果工作线程数大于(是核心? 核心线程数 : 最大线程数),返回false。
如果线程数大于核心线程数,execute()就会让任务进阻塞队列,如果大于最大线程数,或者线程池状态>=SHUTDOWN那么会按照策略处理无法执行的任务。
Worker类
/**
Worker类里封装了Thread和任务Runnable,还有completedTasks。可以注意到创建一个Thread时候把this传入,这样的话如果我调用Worker.thread.start()就相当于该线程会执行Worker里的run方法了。completedTasks是用来记录该线程完成了多少个任务(非整个线程池)。
注意该Worker继承了AQS同步基础器,它主要实现了互斥锁的功能,但是这个互斥锁和ReentrantLock有点不同,该实现是不允许线程重入获取锁的。下面说说为什么要实现锁功能和非重入:
1.lock方法主要用在标明当前线程正在执行任务中,而private interruptIdleWorkers 方法需要使用tryLock来判断当前线程是否正在执行任务,如果非执行任务状态则表明可能是正在获取任务,那么该线程属于空闲状态,可以被中断。
2.看回答1可以知道这通过ReentrantLock也能实现,但是如果我们在提交一个任务给线程池(实现一个Runnable),如果该
任务里面调用了和interruptIdleWorkers相关的,需要中断当前可获取锁(代表空闲)的线程。如果我们不使用非重入锁,这个任务线程就会给中断,从而导致一些奇怪的问题。
**/
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
private static final long serialVersionUID = 6138294804551838833L;
//保存的线程
final Thread thread;
//初始任务
Runnable firstTask;
//完成任务数
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/**
主要看这里,其他的都是关于AQS的实现。
**/
public void run() {
runWorker(this);
}
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
final void runWorker(Worker w)
final void runWorker(Worker w) {
//1.这里获取当前执行线程,就是Worker所封装的Thread(因为是通过该Thread启动的,然后执行自身的run方法)
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//2.如果存在第一个任务则直接执行该任务,否则从任务队列里阻塞获取任务
while (task != null || (task = getTask()) != null) {
w.lock();
//2.如果线程池已经STOP,确保当前工作线程被中断,否则,确保线程没被中断。
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//3.执行任务前的钩子方法
beforeExecute(wt, task);
Throwable thrown = null;
try {
//4.这里的run()方法,会将当前线程传递给任务,参见我的关于FutureTask文章
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 {
//5.执行任务后的钩子方法
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
/* 6.执行到这里代表该线程已被终止,将被回收(从线程池的workers里删除该线程)。
这个方法同时也代表了当线程超出了空闲时间后,将不再由线程池维护,而是被GC回收。
具体可以看getTask。由于getTask是以阻塞方式从阻塞队列获取任务,可以通过阻塞获取时候
设定一个阻塞时间来达到 keepAliveTime空闲功能*/
processWorkerExit(w, completedAbruptly);
}
}
Runnable getTask()
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
retry:
for (;;) {
int c = ctl.get();
//1.获取当前线程池的状态
int rs = runStateOf(c);
// 2。如果线程池已被shutdown或者由于其他原因关闭,那么则终止该线程,返回null,
//最后就会走processWorkerExit方法 了
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
boolean timed; // Are workers subject to culling?
for (;;) {
//3.获取线程池当前的线程数(worker数量则代表线程数)
int wc = workerCountOf(c);
//4.判断是否需要采取设置 阻塞时间的方式获取任务.如果核心线程也需要空闲回收
//或者当前线程数量已经超越了核心线程数,那么都需要采取阻塞时间获取任务方式。
timed = allowCoreThreadTimeOut || wc > corePoolSize;
//5.判断是否需要跳出循环,循环仅仅只是为了cas修改减少线程池的线程数。
if (wc <= maximumPoolSize && ! (timedOut && timed))
break;
// 6.执行到这里代表阻塞获取任务超时,keepAlivetime时间到了。该线程将被回收
if (compareAndDecrementWorkerCount(c))
return null;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
try {
//7.如果需要采用阻塞形式获取,那么就poll设定阻塞时间,否则take无限期等待。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
getTask()有四种情况会返回null:
- 线程池终止:state>=STOP。
- 线程池状态为SHUTDOWN,而且任务阻塞队列为空。
- 工作线程数超过了最大线程数maximumPoolSize。
- 获取任务超时,也就是该工作线程空闲时间超时。
注意第四种情况:
当工作线程超过keepAliveTime空闲时间,getTask()就会返回null,runWoker()就会执行
finally{ processWorkerExit(w, completedAbruptly);}执行到这里代表该线程已被终止,将被回收(从线程池的workers里删除该线程)。 这个方法同时也代表了当线程超出了空闲时间后,将不再由线程池维护,而是被GC回收。我们可以设置keepAliveTime来控制回收工作线程,注意这里是回收的不是核心线程,而是核心线程之外的工作线程。
timed = allowCoreThreadTimeOut || wc > corePoolSize;这条语句是getTask()函数中的关键语句。
Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();
可以看出如果我们设置了allowCoreThreadTimeOut 为true,那么线程池也会回收核心线程。
到此我们就把线程池讲完了。整个函数分析流程就是一条调用的路线,思路很清晰吧!
合理地配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
·任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
·任务的优先级:高、中和低。
·任务的执行时间:长、中和短。
·任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的
线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配
置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务
和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量
将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过
Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高
的任务先执行。
注意 如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能
执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让
执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越
长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
建议使用有界队列。
有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点
儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任
务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线
程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻
塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,
有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所
有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是
出现这样问题时也会影响到其他任务。
线程池的监控
如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根
据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的
时候可以使用以下属性。
·taskCount:线程池需要执行的任务数量。
·completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
·largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是
否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
·getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销
毁,所以这个大小只增不减。
·getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的
beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执
行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。
这几个方法在线程池里是空方法。
protected void beforeExecute(Thread t, Runnable r) { }
参考文章