Java线程池作用和设计思想
作用
- 提高线程的可管理性:Java线程池主要的作用就是具有良好的管理线程资源能力,防止无休止的去申请系统的线程资源,因为在我们的hotspot虚拟机中Java的线程模型是1:1的,每一个创建的Java线程都对应的一个OS中的轻量级线程,一般来说,创建几万个线程就对系统内存有非常大的压力
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行
业务背景
线程池在我们的项目中大量的用到,比如在分割大请求,下载资源,或者是异步处理中都会用到这个技术
设计思想
- 线程池中主要利用到的思想就是池化思想,利用这种池化技术,可以达到资源复用的效果。
- 在线程池中,采用了类似生产消费者的模式来对任务进行处理。
大致的运行流程图如下:
Java线程池的参数和生命周期
参数
无论是哪一种方式构造线程池都是依赖ThreadPoolExecutor的构造方法来进行创建的,构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
从上面的构造方法可以看出,一个线程池的构造需要上面七种参数,分别是:
- corePoolSize: 核心线程池大小;
- maximumPoolSize: 最大线程池大小;
- keepAliveTime: 空闲线程的存活时间;
- unit: keepAliveTime的时间单位;
- workQueue: 用来装被阻塞任务的阻塞队列;
- threadFactory: 线程工厂, 用来生成线程池中工作线程的地方;
- handler: 拒绝策略, 当线程池停止或者最大线程和最大队列都满了的 时候,会调用拒绝策略里面的拒绝方法来处理任务。
上面有几个参数需要注意,第一个是keepAliveTime,单独这个参数是只能决定超出核心线程池大小的线程的存活时间,而如果要给我们的核心线程池的线程大小设置存活时间的话,还需要设置一个参数:allowCoreThreadTimeOut,源码如下(getTask()的部分源码):
// 根据allowCoreThreadTimeOut和当前线程数是否超出核心线程池大小决定是否会超时等待
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
......
try {
// 如果允许超时等待,则会在阻塞队列为空的情况下,超时等待一段时间,如果等待一段时间之后依旧阻塞队列为空,则会返回为空
// 如果不允许超市等待,那么线程就会阻塞在阻塞队列里面的Condition队列里面,直到队列中有任务被被唤醒
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
从上面可以看出来,工作线程之间没有任何差别,核心和非核心只针对数目,而不是针对具体线程,就算设置了allowCoreThreadTimeOut为false,在高并发情况下,如果阻塞队列为空,也会销毁没有拿到任务的线程,然后重新添加工作线程,直到工作线程数等于核心线程数,销毁线程源码如下:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
// 使用线程池的锁来锁住,保证删除线程和统计线程执行任务数的逻辑线程安全
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
// 判断线程池的状态是否正常运行
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
// 获取最小线程数目
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
// 如果当前工作线程数大于获取到的最小线程数,就不会再新增加线程
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 新增工作线程
addWorker(null, false);
}
}
第二个是workQueue:在jdk内部的阻塞队列有很多种,但是不管是哪一种阻塞队列,都需要对阻塞队列的大小设置一个合理的大小,设置太大的话可能会导致最大线程数不生效,设置太小的话,可能在高并发情况下会导致最大线程没有来得及开启的情况下,任务打进队列,发现长度不够,直接调用了拒绝策略。
示例代码:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5,
5,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3)
);
for (int i = 0; i < 5; ++i) {
executor.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(900);
} catch (InterruptedException ignored) {
}
}
});
}
Thread.sleep(1000);
System.out.println("After sleep");
for (int i = 0; i < 5; ++i) {
try {
executor.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(900);
System.out.println("执行完成");
} catch (InterruptedException ignored) {
}
}
});
} catch (Exception e) {
System.out.println(e);
}
}
除了这七种熟知的参数外,还有几个很重要的参数:
- ctl
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl这个参数包含两个部分,第一个是高三位,这个里面包含的线程池的运行状态,第二个是低29位,包含的是线程池运行的工作线程数目 2. mainLock
private final ReentrantLock mainLock = new ReentrantLock();
这个是线程池内部的一个可重入锁,用来控制线程池的各种同步
生命周期
ThreadPoolExecutor的生命周期有五种状态,分别是:
状态流转入如下图:
ThreadPoolExecutor源码分析
我们从两个个方向来做ThreadPoolExecutor的源码分析
- 执行任务
- 工作线程
执行任务
在ThreadPoolExecutor中,创建完线程池之后,是通过execute方法来执行任务
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
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的话就会尝试新增工作线程
// 到这里的时候工作线程是0只有两种情况,第一种情况是我们设置的corePoolSize是0,第二种情况是运行到这里时,刚好线程已经过了存活时间被回收了
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果上面队列放不进去就会新增超出核心线程数的线程
else if (!addWorker(command, false))
reject(command);
}
上诉的工作流程可以简化为下图:
这个里面还涉及到添加工作线程的流程,addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
// 通过CAS + 循环来对工作线程的计数增加
retry:
for (;;) {
// 获取当前线程池的状态
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
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;
// else CAS failed due to workerCount change; retry inner loop
}
}
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 {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
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();
}
// 启动工作线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
工作线程
上述过程一直提到了一个东西就是工作线程,实际上工作线程并不仅仅单独指一个线程,工作线程的具体组成有几个东西,第一个是我们真正被运行的线程,第二个是刚刚创建线程时候会放进去的第一个任务,第三个是内部基于AbstractQueuedSynchronizer实现的一套独占锁,这套独占锁是不可重入的。当一个工作线程运行时,会调用工作线程里面的run方法,而run方法里面实际上调用的是ThreadPoolExecute里面的runWorker方法,runWorker方法源码:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 这个是防止在开始执行任务之前,有中断请求,保证可以响应中断
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 首先检查当前的工作线程有没有携带任务,如果没有携带任务就会通过getTask方法去阻塞队列中间拿任务
// 这个地方的循环就是线程池中资源复用的体现
while (task != null || (task = getTask()) != null) {
// 这个地方会锁住当前工作线程,保证线程安全
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
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);
}
}
简化流程如下: