什么是线程池
在Java中,如果每一个请求到达就创建一个新线程,那么创建和销毁线程所花费的时间和消耗系统资源都相当大,甚至可能要比在处理实际请求的时间和资源还要多。如果要在JVM中创建非常多的线程,可能会是系统由于过度消耗内存而导致系统资源不足。为了解决这个问题,就有了线程池的概念。线程池的核心逻辑就是提前创建好若干个线程放在一个容器中,如果有任务需要处理,则将任务直接分配给线程池中的线程来执行处理,任务处理完之后这个线程不会被销毁,而是等待后续分配任务。
在开发过程中,合理使用线程池可以带来以下三个好处:
- 降低资源消耗:通过重复利用已经创建的线程来降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达后,可以不需要等待线程的创建就能立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统稳定性,使用线程池可以进行统一分配、调优和监控。
线程池的实现原理
当向线程池提交一个任务后,线程池是如何处理这个任务的呢?线程池的主要处理流程如下图所示:
由上图可以看到,当一个新任务提交到线程池的时候,线程池的处理流程如下:
- 线程池判断核心线程池里的线程是否都在执行任务,如果不是,则创建一个新的线程来执行任务;如果核心线程池中的线程都在执行任务,则进入下一个流程
- 线程池判断工作队列是否已满,如果没满,则将新的任务提交到工作队列中;如果工作队列满了,则进入下一个流程
- 线程池判断线程池中的线程是否都处于工作状态,如果没有,则创建一个新的线程来执行任务;如果满了,则交给饱和策略来处理这个任务
ThreadPoolExecutor是线程池的核心,提供了线程池的实现。ThreadPoolExecutor.execute方法的示意图如下:
ThreadPoolExecutor执行execute方法分下面4中情况:
- 如果当前运行的线程少于corePoolSize,则创建新的线程来执行任务(这一步需要获取全局锁)
- 如果运行的线程数超过corePoolSize,则将当前任务加入到BlockingQueue
- 如果当前任务无法加入到BlockingQueue(队列已满),则创建新的线程来执行任务(这一步需要获取全局锁)
- 如果创建新线程将使当前运行的线程超过maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法
ThreadPoolExecutor采取上述步骤的总体思路是为了在执行execute()方法时,尽可能的避免获取全局锁;ThreadPoolExecutor在完成预热之后(当前运行的线程大于corePoolSize),几乎所有的execute方法都会执行第二步,而第二步不需要获取全局锁。
源码分析
上面的流程分析让我们很直观的了解了线程池的工作原理,接下来我们通过源码看看线程池是如何实现的:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { // 当前线程池中的线程比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);
else if (workerCountOf(recheck) == 0) // 如果之前的线程已被销毁完,则新建一个线程
addWorker(null, false);
}
else if (!addWorker(command, false)) // 核心线程已满,队列已满,试着创建一个新的线程
reject(command); // 如果创建新线程失败了,说明线程池关闭了或者线程池完全满了,拒绝该任务
}
ctl的作用
在线程池中,ctl这个变量贯穿在线程池的整个生命周期中:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
它是一个原子类,主要作用是用来保存线程数量和线程池的状态;我们分析一下上面这段代码,它用到了位运算,一个int数值是32个bit位,这里采用高3位来保存运行状态,低29位来保存线程数量。
上面的方法中调用了ctlOf(int rs, int wc)方法:
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int RUNNING = -1 << COUNT_BITS;
private static int ctlOf(int rs, int wc) { return rs | wc; }
其中RUNNING = -1 << count_BITS;也就是-1左移29位,-1的二进制是32个1;那么-1 左移29位也就是[111],所以上面代码中 rs | ws 也就是 111 | 000,得到的结果仍然是111。
同理可以得到其他状态的bit位表示:
private static final int COUNT_BITS = Integer.SIZE - 3; // 32 - 3 = 29
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 将1 的二进制左移29位,再减一表示最大容量 0001 1111 1111 1111 1111 1111 1111 1111
// 运行状态保存在int值的高3位(所有数值左移29位)
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; // 所有任务都已结束,线程数量为0, 处于该状态的线程池即将调用ternamited()方法
private static final int TERMINATED = 3 << COUNT_BITS; // terminated()方法执行完成
addWorker方法
如果工作线程数量小于核心线程数的时候,会调用addWorker方法创建一个工作线程;通过源码可以发现,这个方法其实只做了两件事:
1). 通过循环CAS操作将线程数加1
2). 新建一个线程并启动
private boolean addWorker(Runnable firstTask, boolean core) {
retry: // goto语句,避免死循环
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 1. 线程池已经shutdown后(rs >= shutdown),还要添加新任务,拒绝
// 2. SHUTDOWN状态不接收新任务,但仍然会执行已经添加到队列中的任务;所以当线程池是SHUTDOWN的时候,
// 而传进来的任务为空,并且任务队列不为空的时候,是允许添加新线程的,再把这个条件取反,就表示不能添加新的worker了
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) { // 自旋
int wc = workerCountOf(c); // 获得worker工作线程数
// 如果工作线程数大于默认容量大小,或者大于核心线程数大小,则直接返回false表示不能添加新的worker
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c)) // 通过CAS来增加工作线程数,如果失败则直接重试
break retry;
c = ctl.get(); // 再次获得ctl的值
if (runStateOf(c) != rs) // 如果状态不相等, 说明线程池的状态发生了变化,继续重试
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 上面这段代码主要是对worker的数量做原子+1的操作,下面的代码才是真正的创建一个新的worker
boolean workerStarted = false; // 工作线程是否启动的标识
boolean workerAdded = false; // 工作线程是否已经添加成功的标识
Worker w = null;
try {
w = new Worker(firstTask); // 创建一个worker,把当前任务传递给worker
final Thread t = w.thread; // 从worker对象中取出线程
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());
// 只有当前线程池是正在运行的状态,或者是SHUTDOWN且firsttask为空,才能添加到workers集合中
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // 任务刚封装到work里面,还未启动,线程不可能是alive状态,如果是alive状态就直接抛异常
throw new IllegalThreadStateException();
workers.add(w); // 将新创建的worker添加到workers集合中
int s = workers.size(); // 获取工作线程的数量
// 如果集合中工作线程的数量大于最大线程数,则更新最大线程数的值;
// 这里的最大线程数是指线程池中曾经出现过的最大线程数
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true; // 表示工作线程创建成功
}
} finally {
mainLock.unlock();
}
if (workerAdded) { // worker添加成功
t.start(); // 启动线程
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w); // 如果添加失败,递减实际工作线程数
}
return workerStarted;
}
工作线程
我们发现addWorker方法只是构造了一个Worker,并且把firstTask封装到worker中,它的作用是什么呢?
Worker类集成了AQS并且实现了Runnable接口;其中包含了firstTask和thread属性,firstTask用来保存传入的任务;thread是在调用构造方法时通过ThreadFactory来创建的线程,是用来处理任务的线程。
在调用构造方法时,需要传入任务,这里通过getThreadFactory().newThread(this)来新建一个线程,newThread方法传入的参数是this,因为worker本身继承了Runnable接口,也就是一个线程,所以一个worker对象在启动的时候会调用Worker类中的run方法。
Worker继承了AQS,使用AQS来实现独占锁的功能,这里问什么不适用ReentrantLock来实现呢?从代码中可以看到tryAcquire方法是不允许重入的,而ReentrantLock是允许重入的。
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);
}
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) {
}
}
}
}
runWorker方法
前面已经了解到ThreadPoolExecutor的核心方法是addWorker方法,主要作用是增加工作线程。而Worker简单理解就是是一个线程,里面重写了run方法,线程池中真正的处理逻辑也就是调用Worker的run方法,Worker中的run方法又调用ThreadPoolExecutor类中的runWorker方法,这个runWorker方法主要做了下面几件事:
- 如果task不为空,则执行task
- 如果task为空,则通过getTask()去获取任务,并赋值给task;如果去到的task不为空,则执行
- 执行完毕后通过while循环继续调用getTask()取任务
- 如果getTask()取到的任务依然为空,那么整个runWorker方法执行完毕
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // unlock表示当前worker允许中断,在new worker的时候默认的state=-1,这里通过tryRelease方法将state设置成0
boolean completedAbruptly = true;
try {
// 在这个while循环中实现了线程的复用,如果task为空,则通过getTask()来获取任务
while (task != null || (task = getTask()) != null) {
w.lock(); // 这里加锁不是为了防止并发执行任务,而是为了在shutdown的时候不终止正在运行的任务
// 线程池尾stop状态时,不接收新的任务,不执行已经添加到队列中的任务,并且要中断正在执行的任务。
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
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;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
线程池的创建
我们可以通过ThreadPoolExecutor来创建一个线程池:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
创建线程池时需要传入几个参数,如下:
- corePoolSize(线程池的基本大小): 当提交一个任务到线程池的时候,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程;等到需要执行的任务数大于线程池基本大小的时候就不再创建。如果调用了线程池的prestartAllCoreThreads()方法, 线程池会提前创建并启动所有线程
- maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。需要注意的是如果使用了无界的任务队列,这个参数就没什么效果了
- keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间;所以如果任务很多,并且每个任务的执行时间很短,则可以调大时间,提高线程利用率
- unit(线程活动保持时间的单位):可选的单位有天DAYS、HOURS、MINUTES、MILLISECONDS、MICROSECONDS,NANOSECONDS。
- workQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按照FIFO对元素进行排序
LinkedBlockingQueue:基于链表结构的阻塞队列,按照FIFO对元素进行排序;吞吐量要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了此队列
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工 厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。 - threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设 置更有意义的名字
- handler:当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法 处理新任务时抛出异常。Java线程池框架提供了以下4种策略:
AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。 通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方 法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线
程一段时间后立即返回,这时候有可能任务没有执行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch(Exception e) {
// TODO
} finally {
executor.shutdown();
}
关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线 程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务 可能永远无法终止。
但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务 都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪 一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭 线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
合理地配置线程池
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
CPU密集型任务应配置尽可能小的 线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配 置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务 和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越 长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。