ThreadPoolExecutor源码分析(上编)
文章目录
一. ThreadPoolExecutor应用方式
-
基本应用方式举例:
//创建线程池 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor( //1. 核心线程数 2, //2. 最大线程数 3, //3. 最大线程数的存活时间 500, //4. 时间单位 TimeUnit.SECONDS, //5. 阻塞队列 new LinkedBlockingDeque<>(), //6. 线程工厂 new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("线程名字1"); return t; } }, //7. 拒绝策略 new ThreadPoolExecutor.AbortPolicy() ); //调用线程池线程执行的方法execute()和submit() //execute执行线程没有返回结果,只能走Runnable接口 //submit执行线程可以有返回结果,可用callable接口,通过返回的对象,调用get()获取返回的值(注意这个位置会阻塞住) poolExecutor.execute(() -> System.out.println("执行测试1----线程")); }
-
思考:
- 核心线程应用写多大合适?
- 最大线程怎么设置?
- 阻塞队列怎么设置比较合适?
- 拒绝策略怎么选择合适?
- 如何将当前系统的硬件CPU,发挥出它的最大的特性?
-
带着以上的几个问题我们一起来找下答案…
二. ThreadPoolExecutor核心参数
-
进入ThreadPoolExecutor类中查看源码,可以看到包含以下7个参数:
-
public ThreadPoolExecutor(int corePoolSize, //1.核心线程数 int maximumPoolSize, //2.最大线程数 long keepAliveTime, //3.最大线程数的存活时间 TimeUnit unit, //4.时间单位 BlockingQueue<Runnable> workQueue,//5.阻塞队列 ThreadFactory threadFactory,//6.线程工厂 RejectedExecutionHandler handler)//7.拒绝策略 { 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 最大线程数的存活时间
非核心线程的存活时间
-
TimeUnit 时间单位
keepAliveTime 的时间单位
-
BlockingQueue 阻塞/工作队列
-
核心线程没有空闲的时候就会扔到这个队列中进行排队
-
BlockingQueue 是一个接口,常用实现类是:
ArrayBlockingQueue 可以指定长度的
LinkedBlockingQueue 没有指定长度的
-
-
ThreadFactory 线程工厂
- 线程池默认是空的,只有第一次提交任务的时候才会创建线程
- 就是通过这个线程工厂将线程创建出来的
- 只不过Thread对象被封装成了一个Work对象
-
RejectedExecutionHandler 拒绝策略
-
当核心线程数,阻塞队列,最大线程数均执行满时,才会执行这个拒绝策略
-
RejectedExecutionHandler 是一个接口,因此可以引用它的实现类
AbortPolicy 直接抛出异常
CallerRunsPolicy 当线程池中的线程没有空闲时,调用的主线程自己执行线程
DiscardOldestPolicy 把阻塞队列中排在最前面的任务弹出去,再执行线程
DiscardPolicy 当线程池线程没有空闲时,直接忽略。当然也可以重写它的方法执行一定的业务
-
可以根据实际的业务需求,去实现其拒绝的策略
-
三. ThreadPoolExecutor执行流程
-
业务线程提交任务到线程池之后,任务的处理流程
-
可通过查看 execute( ) 的源码,进行分析:
-
public void execute(Runnable command) { //判断传递进来的线程是否为空值 if (command == null) throw new NullPointerException(); //获得AtomicInteger对象 int c = ctl.get(); //通过workerCountOf(c) 获取“工作线程个数”来与“核心线程数”进行判断 if (workerCountOf(c) < corePoolSize) { // 如果小于核心线程数,addWorker(command, true)则添加当前线程到工作线程中执行线程 if (addWorker(command, true)) //添加核心线程成功则返回true return; //如果在并发的情况下,核心线程数添加失败,则会重写获取ctl属性,继续往下执行 c = ctl.get(); } //如果上面的条件不满足则会继续进行下面的判断 //isRunning(c)判断线程是否RUNNING状态,并且通过offer()将线程放入队列中,会判断工作队列是否排满,成功返回true,失败返回false if (isRunning(c) && workQueue.offer(command)) { //将任务放到工作队列,等待工作线程执行 //这里会再次获取ctl,重写检查线程的状态 int recheck = ctl.get(); //判断线程是否RUNNING状态取反,表示如何不是RUNNING状态,就会将任务从工作队列中移除 if (! isRunning(recheck) && remove(command)) // 移除成功则会执行拒绝策略 reject(command); //如果不为上面的情况则会再次判断,工作线程是否为0 else if (workerCountOf(recheck) == 0) //这一步是为了解决,工作线程数为0,但是工作队列有任务在排队的情况 //因此会添加一个“空任务非核心线程” addWorker(null, false); } //任务添加到工作队列失败,则会执行这个 //addWorker(command, false)判断工作线程数是否大于最大线程数.创建非核心线程并执行当前任务 //其中false表示添加非核心线程,true表示核心线程 else if (!addWorker(command, false)) // 取反,表示添加非核心线程失败,开始执行拒绝策略 reject(command); }
- 执行流程图:
图片类源于:【马士兵教育】多线程视频
- 执行流程图:
四. ThreadPoolExecutor状态
- 了解其状态,就要先了解线程池中的核心属性 ctl ,线程的状态是通过其计算得出来的
//其中ctl本质就是一个int类型的数值
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//Integer.SIZE = 32,则COUNT_BIT = 29 (表示标识工作线程个数)
//为什么这样表示-----主要是因为ctl表述了两个状态:
//1.(高三位)表示线程池当前的状态
//2.(低29位)标识线程池当前的工作线程个数
private static final int COUNT_BITS = Integer.SIZE - 3;
//CAPACITY表示工作线程的最大数量(包括:核心线程及非核心线程)=(1右移29位)- 1;
//二进制表示是:00011111 11111111 11111111 11111111 = 4160486911 (转十进制)
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 以下5种就是(高三位)表示线程池的状态(通过右移二进制转换得到以下二进制值)
private static final int RUNNING = -1 << COUNT_BITS; //111
private static final int SHUTDOWN = 0 << COUNT_BITS; //000
private static final int STOP = 1 << COUNT_BITS; //001
private static final int TIDYING = 2 << COUNT_BITS; //010
private static final int TERMINATED = 3 << COUNT_BITS; //011
//计算当前线程池的状态;
//c表示当前ctl对象,~CAPACITY 工作线程数取反,& 二进制运算符
//比如:当前c为000,~CAPACITY取反为111,因此 000 & 111 = 000(&运算都为1才为1)
private static int runStateOf(int c) { return c & ~CAPACITY; }
//计算当前线程池中的工作线程个数
private static int workerCountOf(int c) { return c & CAPACITY; }
- 线程池的状态变换
图片类源于:【马士兵教育】多线程视频
- 从RUNNING状态到SHUTDOWN状态是通过调用shutdown( )进入的,并加锁停止接收任务
- 从RUNNING状态到STOP状态是通过调用shutdownNow( ),直接中断任务(暴力停止)
- 而TIDYING是一个过渡状态是shutdown或shutdownNow方法内部的调用,是通过执行tryTerminate( )进行调用的,会判断工作线程数是否为0
- TERMINATED销毁状态。是执行完tryTerminate( ),会自动调用terminated(){}进行销毁线程,其中这个方法业务逻辑是空的,可以根据业务需要重写这个方法,线程销毁之后需要做处理的业务逻辑
- 注意:每次提交任务都会判断当前线程的状态,是因为往线程池提交任务是有并发操作的,这种并发操作可能会造成任务刚提交,忽然之间调用shutdown( ),因此需要每次都重写判断当前任务线程的状态。以此来避免并发的一些问题。
以上是对ThreadPoolExecutor类基本的一个解析,并对execute( ) 的源码进行初步的分析。相信大家对创建线程池的的这类,已经有基本的了解啦!
其中涉及到的addWorker方法, Worker对象,runWorker方法,getTask方法,processWorkerExit方法 ,并没有深度分析,将会在ThreadPoolExecutor源码分析(下编)中继续阐述。