本文将全面并且深入地带你全面了解Java线程池,从源码到手写,通俗易懂又硬核,看了你血赚,不看必后悔~~废话不多说,咱们开始
全面分析线程池
壹:初识线程池
一种简单的创建线程的方式:
new Thread(new Runnable() {
@Override
public void run() {
//do something...
}
}).start();
start()启动线程后,调用run()方法,当run()方法执行完毕后,线程就销毁,不能重复使用
那么问题来了
很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的时间的情况,反而会得不偿失。线程本身也是要占用内存空间的,大量的线程会抢占宝贵的内存资源,如果处理不当,可能会导致Out of Memory异常。即便没有,大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间。
那么线程池带着减小消耗的使命来了~
线程的创建和销毁都需要映射到操作系统,因此其代价是比较高昂的。出于避免频繁创建、销毁线程以及方便线程管理的需要,线程池应运而生。
明白几个概念,方便后文手写线程池
线程池就像是一个工厂,当仓库都放不下订单,就需要请临时工了
我们将工厂中的元素映射到线程池中的各个变量上,方便理解
- 核心线程:会处理抢到的任务,当抢到的任务处理完不会立马销毁,而是从workQueue中拿线程继续处理(线程池设计核心)
- 救急线程:会处理抢到的任务,但处理完线程就销毁了
- workQueue:任务队列,底层使用阻塞队列实现
- 任务:runnable,callable或者封装成的collection
- 四种拒绝策略
AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
CallerRunsPolicy 让调用者运行任务
DiscardPolicy 放弃本次任务
DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
什么是阻塞队列
就是经典的生产者,消费者问题
1.当想从队列中拿任务,发现没有任务,那就阻塞等着,等到队列中有任务加入进来,然后唤醒拿走
2.当想向队列中放任务,发现队列满了,那就阻塞等着,等到队列有任务被拿走了,然后唤醒加入到队列中
贰:手写线程池(简化版)
有了对线程池基础的了解后,我们就可以尝试手写线程池,当然是核心简化版,手写一遍很大程度上帮助你对线程池有一个全面的了解,咱们走着:
先实现任务队列(阻塞队列实现)
线程池中阻塞队列有很多实现方式,这里我们使用ArrayDeque
,为了实现线程安全,我们使用ReentrantLock
配合条件变量,如有不懂,先去看看重入锁再来啦~
- 先看看用到那些属性
class BlockingQueue<T>{
// 阻塞队列的底层结构
private Deque<T> queue = new ArrayDeque<T>();
// 锁,线程争夺任务,只能确保一个线程获取成功
private ReentrantLock lock = new ReentrantLock();
// 生产者条件变量,实现阻塞队列核心
private Condition fullWaitSet = lock.newCondition();
// 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 这里使用有容量的阻塞队列
private int capacity;
// 构造方法
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
- 核心的三个方法,拿走任务,加入任务,返回任务个数
class BlockingQueue<T>{
// take拿走任务
public T take(){
lock.lock();
try {
// 别急着拿走,先看看有没有
while(queue.isEmpty()){
// 没有,先等着
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 有元素了,拿走吧
T t = queue.removeFirst();
// 拿走元素了,别忘了通知生产者可以放入元素了
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
// put加入任务
public void put(T element){
lock.lock();
try {
// 满了,别放了,等着消费者拿走通知自己
while(queue.size()==capacity){
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 有位置了,放元素
queue.addLast(element);
// 别忘了告诉消费者有元素了
emptyWaitSet.signal();
}finally {
lock.unlock();
}
}
// 返回当前任务队列的元素个数
public int size(){
lock.lock();
try {
return queue.size();
}finally {
lock.unlock();
}
}
}
- 上述的拿走和放入的方法是类似于CAS操作,while()循环达不到目的,就一直阻塞,这是一种策略,我们提供另一种方式,超过一定时间,别等了,结束吧
代码大致上没有改变,只是调用了超时等待的方法
/**
* 超时拿取
* @param timeout 超时时间,超过就不等了
* @param timeUnit 时间的单位
* @return 返回拿走的任务
*/
public T poll(long timeout, TimeUnit timeUnit){
lock.lock();
try {
// 统一单位
long nanos = timeUnit.toNanos(timeout);
// 别急着拿走,先看看有没有
while(queue.isEmpty()){
// 这里调用另一种方法
try {
//返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,这里防止伪等待
//假如等待过程中没有拿到,在进来的时候又要重新等,不合理,所以更新nanos的值
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 有元素了,拿走吧
T t = queue.removeFirst();
// 拿走元素了,别忘了通知生产者可以放入元素了
fullWaitSet.signal();
return t;
}finally {
lock.unlock();
}
}
/**
* 超时放入
* @param element
* @param timeout
* @param timeUnit
* @return
*/
public boolean offer(T element ,long timeout,TimeUnit timeUnit){
lock.lock();
try{
long nanos = timeUnit.toNanos(timeout);
while (queue.size()==capacity){
try {
// 如果返回值 <= 0 ,则可以认定它已经超时了。
if(nanos<=0){
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(element);
emptyWaitSet.signal();
// 获取成功,返回true
return true;
}finally {
lock.unlock();
}
}
任务队列准备完毕,接下来看看ThreadPool如何实现
- 属性以及构造方法
class ThreadPool{
// 任务队列,这里我们将Runnable当作任务
private BlockingQueue<Runnable> taskQueue;
// 正式员工,线程池的底层结构,
private HashSet<Worker> workers = new HashSet<>();
// 正式员工数,也就是核心线程数
private int coreSize;
private long timeout;
private TimeUnit timeUnit;
//构造方法
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueSize) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
// 初始化阻塞队列,指定阻塞队列的大小
taskQueue = new BlockingQueue<>(queueSize);
}
}
- 核心方法:
execute()
执行方法
这里使用到了worker内部类
public void execute(Runnable task){
// 说明正式工都还没有完全上岗,全部上岗
if(workers.size()<coreSize){
Worker worker = new Worker(task);
workers.add(worker);
worker.start();
}else{
// 全部上岗也不够用了,进入队列等待吧
// 这是一种策略,后文中可以使用不同的拒绝策略
taskQueue.offer(task,timeout,timeUnit);
}
}
- Worker内部类
我们将工作线程包装成Worker,使其拥有更丰富的功能
// 这里Runnable不够用,我们其和一些属性封装到一起
// 内部类的使用在底层源码中十分常见
class Worker extends Thread{
// 工人角色,其实就是一个工作线程
private Runnable task;
// 重写run()方法
@Override
public void run() {
// 1.执行第一次抢到的task
// 2.执行完不销毁,继续去任务队列里拿任务执行
while(task!=null || (task = taskQueue.poll(timeout,timeUnit))!=null){
// 这里防止任务执行异常,我们使用try-catch捕捉一下
try {
// 执行任务
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
// 确保任务执行完,即使抛出异常
task = null;
}
}
// 说明所有的任务都执行完了
// 正式工去休息吧
// 移除操作,确保队列安全
synchronized (workers){
workers.remove(this);
}
}
//构造方法
public Worker(Runnable task){
this.task = task;
}
}
我们来阶段性测试一下写的代码
- 编写main方法的测试代码
public static void main(String[] args) {
ThreadPool pool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 5);
// 我们新建5个线程
for(int i = 0;i<5;i++){
// 供lamd表达式使用
int j = i;
pool.execute(new Thread(()->{
System.out.println("我是线程"+j);
}));
}
}
- 我们在一些关键节点打印信息,帮助我们查看执行状态
- 运行我们看结果
线程输出的顺序是不能保证的,多线程特点
我们从输出的结果清晰的看到,五个任务都被执行了,各个过程清晰明了
看到这,你可能有个小疑问?工人工作完了怎么没有被移除啊?
这是因为,我们还没有编写拒绝策略,在poll()
逻辑中不知道你是否还记得,当队列中没有任务时,我们会让队列一直等待,直到有任务加入队列,所以接下来我们来编写拒绝策略
编写拒绝策略
- 我们将拒绝策略抽象成接口,由调用者自己决定采取哪一种拒绝策略
//函数式接口,接口只有一个方法
@FunctionalInterface
interface RejectPolicy<T>{
// 拒绝策略,我们要知道队列和要拒绝的任务
void reject(BlockingQueue<T> queue, T task);
}
- 这时我们的
poll()
方法就不够用了,我们要在BlockingQueue
类中重新编写一个根据不同策略实现不同放入方式的方法
public void tryPut(RejectPolicy rejectPolicy, T task){
lock.lock();
try {
if(queue.size()==capacity){
// 队列满了,执行相应的拒绝策略
rejectPolicy.reject(this,task);
}else{
queue.addLast(task);
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
- 我们要在
ThreadPool
类中加入RejectPolicy
属性,添加对应的构造方法
- 接下来在加入方法处使用我们新编写的
tryPut()方法
最终测试
这里的拒绝策略写一种即可,我只是在展示不同的拒绝策略,还有许多复杂的拒绝策略我们稍后再源码分析中给你揭晓
叁:全面分析正版线程池
1.继承实现关系
从Java线程池Executor框架体系可以看出:线程池的真正实现类是ThreadPoolExecutor
,因此我们接下来重点研究这个类。
2.线程池状态
ThreadPoolExecutor
使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
为什么用一个非要用一个数来表示两个变量呢?
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
// 对ctl都是CAS操作
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// int32位,COUNT_BITS=29,下文左移29位,将状态移动到高3位
private static final int COUNT_BITS = Integer.SIZE - 3;
// 最大容量,29位能表示的最大数
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;
// 或操作将高三位,低29位合并到一个int中
private static int ctlOf(int rs, int wc) { return rs | wc; }
3.构造方法
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 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 :统一给线程池中的线程设置线程group、统一的线程前缀名。以及统一的优先级。
- handler 拒绝策略
利用Executors创建不同类型线程池
如此多的参数,创建线程就是一个头疼的问题,别担心,Doug Lea为我们考虑到了
Executors
静态类可以为我们创建不同类型的线程池,本质上就是根据上述的构造方法,传递不同的参数,创建不同的线程池,然后将其命名,方便我们去使用
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
从传递的参数可以看出:
1.corePoolSize==maximumPoolSize
表示没有救急线程
2.使用LinkedBlockingQueue
阻塞队列,底层链表实现,没有容量,可以放入任意数量的任务
适用于:适用于任务量已知,相对耗时的任务
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
从传递参数可以看出:
1.核心线程数为0,全部都是救急线程,最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s
2.队列采用了 SynchronousQueue 实现特点是它没有容量,没有线程来取是放不进去的,放之前必须有线程来取,类似于一手交钱,一手交货
适用于:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从传递参数可以看出:
线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程
也不会被释放。
还有许多其他工厂方法,在此就不一一描述
4.submit()提交线程
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
5.关闭线程
ThreadPoolExecutor提供了两个方法,用于线程池的关闭:
- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
6. 整个工作流程
-
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。也就是懒加载模式
-
当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。
-
如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
-
如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。
-
拒绝策略 jdk 提供了 4 种实现
1.AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
2.CallerRunsPolicy 让调用者运行任务
3.DiscardPolicy 放弃本次任务
4.DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之 -
当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制什么时候销毁。