线程池原理
核心的思想就是把宝贵的资源放到一个池子中;每次使用都从里面获取,用完之后又放回池子供其他人使用。
为什么要使用线程池
- 线程是稀缺资源,不能频繁的创建。
- 解耦作用;线程的创建与执行完全分开,方便维护。
- 应当将其放入一个池子中,可以给其他任务进行复用。
如何创建线程池
- 案例代码演示
public class ThreadPoolDeom {
ExecutorService pool2 = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
pool3.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}
- 线程池结构
在 JDK 1.5 之后推出了相关的 api,常见的创建线程池方式有以下几种:
//Executors是一个辅助工具类,帮助创建线程
ExecutorService pool1 = Executors.newCachedThreadPool(); //可扩容的线程
ExecutorService pool2 = Executors.newFixedThreadPool(5);//自定义最大线程数
ExecutorService pool3 = Executors.newSingleThreadExecutor();//单个线程
线程池源码分析
我们以newCachedThreadPool()为入口,进行源码级别的分析。
查看代码会发现,其实看这三种方式创建的源码就会发现,以上三种都是利用 ThreadPoolExecutor 类实现的。
经过构造方法之间的调用,最终会通过该构造方法来实例化 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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- ThreadPoolExecutor中的核心属性
- int corePoolSize:初始线程数量,是否手动设置了初始线程数量
- int maximumPoolSize:可增加的最大线程的数量
- ong keepAliveTime:增加的线程保持时间
- TimeUnit unit:保持时间的单位 s/ms/min
- BlockingQueue workQueue:阻塞队列,无边界,只要内存空间还有就能一直存储队列。
- ThreadFactory threadFactory:用来创建线程的工厂
- RejectedExecutionHandler handler:如果超了阻塞队列的拒绝策略,多种接口实现类
线程池中线程的状态
// 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: 线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
- ShutDown: 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
- Stop: 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
- Tidying:当所有的任务已终止,任务数量为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
- Terminated: 线程池彻底终止,就变成TERMINATED状态。
- 如何改变线程池的状态
最重要的就是 execute() 方法
- execute() 方法会根据线程池的状态,添加进不同的队列中然后执行方法。
public void execute(Runnable command) {
//如果传入的对象(具体的执行代码)为 null,直接抛出异常
if (command == null)
throw new NullPointerException();
//从 ctl (AtomicInteger原子类型的类) 属性中拿到 value 值(正在运行的线程数)
int c = ctl.get();
//判断正在运行的线程数是否小于核心数量,是则直接新增 work 成功后直接退出
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
//增加失败后重新获取数量
c = ctl.get();
}
//线程池是否处于 Running 状态 && workQueue(阻塞队列)成功添加进任务对象
if (isRunning(c) && workQueue.offer(command)) {
//再次获取线程池的运行状态和有效线程数
int recheck = ctl.get();
//当前线程池不处于 RUNNING 状态 && 从阻塞队列中移除任务对象
if (! isRunning(recheck) && remove(command))
//执行拒绝策略
reject(command);
//当前线程池中的worker数为0,则直接创建一个(非核心)线程(不超过最大线程数),task为空的线程在执行时,会直接到任务队列中去获取任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果在之前的前提下加入任务队列也失败了(比如任务队列已满),则会在不超过线程池最大线程数量的前提下建立一个工作线程来处理。
else if (!addWorker(command, false))
//如果还添加不进去只能执行拒绝策略了
reject(command);
}
- addWorker(Runnable firstTask, boolean core),ThreadPoolExecutor中方法,添加有效线程
private boolean addWorker(Runnable firstTask, boolean core) {
//标记了一个循环 break 位置
retry:
//自旋操作
for (;;) {
//获取有效线程数量和线程池状态
int c = ctl.get();
//获取线程池的运行状态
int rs = runStateOf(c);
//线程池不能执行任务对象的情况,直接返回false
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();
if (runStateOf(c) != rs)
continue retry;
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//创建一个worker对象封装任务对象
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())
throw new IllegalThreadStateException();
//添加进workers中,表示运行状态
workers.add(w);
//拿到workers的大小
int s = workers.size();
//如果s大于largestPoolSize,则需要将s赋值给largestPoolSize
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
//释放锁
mainLock.unlock();
}
if (workerAdded) {
//启动线程,执行run方法
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
//将线程添加到workers中失败,需要回滚
addWorkerFailed(w);
}
//返回执行状态
return workerStarted;
}
在之前的过程中我们建立了工作线程Worker()类,那么我们现在看看worker类的内部实现,也可以说是线程池的核心部分。Worker类作为线程池的内部类,接下来是Worker()类的成员。
- thread作为worker的工作线程空间,由线程池中所设置的线程工厂生成。
- firstTask则是worker在构造方法中所接受到的所要执行的任务。
- completedTasks作为该worker类所执行完毕的任务总数。
然后调用 start 方法 执行 Wokrer的runWorker方法
如果这个worker还没有执行过在构造方法就传入的任务,那么在这个方法中,会直接执行这一任务,如果没有,则会尝试去从任务队列当中去取的新的任务。
但是在真正调用任务之前,仍旧会判断线程池的状态,如果已经不是running亦或是shutdwon,则会直接确保线程被中断。如果没有,将会继续执行并确保不被中断。
接下来可见,我们所需要的任务,直接在工作线程中直接以run()方式以非线程的方式所调用,这里也就是我们所需要的任务真正执行的地方。
在执行完毕后,工作线程的使命并没有真正宣告段落。在while部分worker仍旧会通过getTask()方法试图取得新的任务。
在runWorker()方法的最后,调用了processWorkerExist()方法来执行工作线程的回收。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
//任务不为空或者从任务队列中获取的任务不为空,则进入while循环
while (task != null || (task = getTask()) != null) {
w.lock();
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);
}
}
- 整体流程
拒绝策略
- AbortPolicy:抛出异常,默认
- CallerRunsPolicy:不使用线程池执行
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:丢弃队列中最旧的任务
如何配置线程池
肯定不是线程池越大越好,需要根据执行的任务性质来配置。
- I.O密集型(与磁盘或者内存进行交互):因为线程大部分时间处于阻塞状态,所以可以多开启线程比如说 CPU * 2
- CPU密集型(进行大量运算):因为线程会一直运行,所以需要尽可能少线程数
如何更优雅的关闭线程
有运行任务自然也有关闭任务,从上文提到的 5 个状态就能看出如何来关闭线程池。 其实无非就是两个方法 shutdown()/shutdownNow()。
但他们有着重要的区别:
- shutdown() 执行后停止接受新任务,会把队列的任务执行完毕。
- shutdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop