首先,既然可以直接创建线程,为什么要用线程池,其实一个方案的出现,要么为了增加功能,要么为了解决一些缺陷,而线程池就是为了优化系统性能而出现,由于线程的生命周期中包括创建、就绪、运行、阻塞、销毁阶段,当我们待处理的任务数目较小时,我们可以自己创建几个线程来处理相应的任务,但当有大量的任务时,由于创建、销毁线程需要很大的开销,运用线程池这些问题就大大的缓解了。
但是使用线程池时需要注意,要根据任务量来制定线程池,否则适得其反。因此有必要了解线程池的工作原理及参数含义。对于理解新的东西,我比较喜欢先从大的角度去总揽一下大概的过程,再根据需要深入理解细节步骤,这样,建立在理解大局的基础上去跟进细节,理解的效率会高一些。好,下面先来看一下线程池涉及的几个类以及他们的关系。
这篇文章简单介绍使用方法,主要解析关键方法execute()方法的执行过程。
首先来看看线程池涉及到的相关类
了解线程池相关类后,我们再通过一个图来理解线程池的执行过程。
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。
- 构造方法
public ThreadPoolExecutor(int corePoolSize,//线程核心数
int maximumPoolSize,//最多工人数量
long keepAliveTime,//保存存活时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//线程池队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler)//拒绝处理对象
- 参数含义
- corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
ArrayBlockingQueue;//基于数组的先进先出队列,此队列创建时必须指定大小;
LinkedBlockingQueue;
SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
- threadFactory:线程工厂,主要用来创建线程;
- handler:表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
- 关键方法
execute()
submit()
shutdown()
shutdownNow()
- execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
- submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果(Future相关内容将在下一篇讲述)。
- shutdown()和shutdownNow()是用来关闭线程池的。
任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
使用实例
-使用Executors来创建线程池
private static void creatByExecutors() {
// 使用Executors来创建线程池,最大核心数位3,任务队列容量为int的最大值
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
MyTast myTast = new MyTast(i);
pool.execute(myTast);
}
pool.shutdown();
}
- 使用ThreadPoolExecutor来创建线程池
private static void creatByThreadPoolExecutor() { // 创建一个固定大小的线程池,核心线程数为3个线程,任务队列容量为5,超过了就会抛出异常 ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(5));
for (int i = 0; i < 15; i++) {
MyTast task = new MyTast(i);
executor.execute(task);
System.out.println("线程池中的线程数量=" + executor.getMaximumPoolSize() + ",等待执行" + "的线程数="
+ executor.getQueue().size() + ",已经执行的线的任务="
+ executor.getCompletedTaskCount());
}
// 执行完毕,终止线程池
executor.shutdown();
对于execute()部分源码解析
public void execute(Runnable command) {
if (command == null)//如果传进来的对象为空,则抛出异常
throw new NullPointerException();
int c = ctl.get();//获取ctl,即当前线程池中运行的线程数量,ctl控制的意思,
//由两部分含义组成,包含了线程的状态和当前线程数量,由一个32位的int数据类型表示,
//高3位表示运行状态,后面29位表示线程数量,即最大的核心数量为5亿多(2^29)
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);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
下面一步一步解析
- 如果传进来的任务为空,则抛出异常
if (command == null)//如果传进来的对象为空,则抛出异常
throw new NullPointerException();
- 在执行下一步之前,先来看一下ThreadPoolExecutor类中定义的一些相关变量,理解了这些变量,才能往下理解执行过程
//ctl是AtomicInteger的类对象,是封装了线程状态和线程数量的对象,
//实际是由int类型拆分来表示,int有32位,用高3位来表示线程状态,用后面29位来表示线程数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//后29位用于表示最大的线程数
private static final int COUNT_BITS = Integer.SIZE - 3;//实际就是32-3=29
//移位之后得到的容量:536870911
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits,由int的高3位表示线程运行的不同状态
//正在运行,这种状态下可接受新任务和处理队列任务
private static final int RUNNING = -1 << COUNT_BITS;
//关闭状态,不接受新的任务,但如果列队中还有任务,列队中的任务还是要处理完成才结束线程
private static final int SHUTDOWN = 0 << COUNT_BITS;
//停止状态,不接受新任务,不处理任务队列中的任务,中断正在执行的任务
private static final int STOP = 1 << COUNT_BITS;
//正在结束状态,所有任务已经结束,工作线程数为0,此时线程转为正在结束的状态,将会调用terminated()方法
private static final int TIDYING = 2 << COUNT_BITS;
//结束状态,方法terminated()方法执行完毕。
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl,相当于装箱和拆箱
//运行状态的计算方法
private static int runStateOf(int c) { return c & ~CAPACITY; }
//工作线程数量的计算方法
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
- 好了,现在来看下一条语句,获取当前的线程数
int c = ctl.get();//是由当前线程状态和线程数量通过与运算得到
//获取ctl,即当前线程池中运行的线程数量,ctl控制的意思,
//由两部分含义组成,包含了线程的状态和当前线程数量,由一个32位的int数据类型表示,
//高3位表示运行状态,后面29位表示线程数量,即最大的核心数量为5亿多(2^29)
//ctl.get()这个方法调用了AtomicInteger类中的方法,返回一个原子数,
private volatile int value;
public final int get() {
return value;
}
//这个value可以通过方法设置,也可以是构造函数中赋值
在开始执行execute()的时候,ctl需要被初始化
就在ThreadPoolExecutor类中作为成员变量初始化
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
这个ctlOf()的方法定义
private static int ctlOf(int rs, int wc) { return rs | wc; }
紧跟着,到AtomicInteger的构造函数去看看,首先调用这个方法ctlOf(RUNNING, 0)得到的值还是RUNNING,就是当前状态为RUNNING
public AtomicInteger(int initialValue) {
value = initialValue;//在这里看到value被赋值了,也就是后面ctl.get()取到的值
}
//还有提供设置方法设置这个值
public final void set(int newValue) {
value = newValue;
}
//由上一步的解析可知,这里调用ctl.get()得到的结果为RUNNING状态,即-536870912;
- 判断当前线程数是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {//以当前线程数作为参数传递
//以当前线程数获取当前工作线程数
//这里调用了ThreadPoolExecutor内部方法,返回的就是当前工作的线程数量
private static int workerCountOf(int c) { return c & CAPACITY; }
由上一步返回的c带进去计算,结果workerCountOf(c)等于0,即当前工作线程数量为0,小于我们创建线程池时传进来的corePoolSize,所以这个条件符合
- 如果当前工作的线程数量小于核心线程数,这个条件满足,就继续执行下一个语句,第一次执行是符合条件的
if (addWorker(command, true))
//看看这个方法的实现
//这个方法还是在当前的ThreadPoolExecutor类中声明
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//先说明一下这个retry,其实这只是一个标志,二话不说先写Demo测试
private static void testRetry() {
// 标识retry的使用,类似于goto,但java中没有goto,就用这个来解决
// retry只是个标记,可以使用其他任何非java关键字,一般配合continue来使用,或者嵌套多个for循环时用于跳出某个循环的标记
示例:
tag: for (
int i = 0; i < 5; i++) {
flag: for (int j = 0; j < 5; j++) {
if (i == 2) {
continue flag;// 相当于跳过i=2
}
if (i == 4) {
break tag;// i=4,直接退出最外层
}
System.out.print(j + " ");
}
}
}
好了,弄明白这个retry之后回到之前的代码
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();//再次获取当前线程数
int rs = runStateOf(c);//获取当前线程运行状态,第一次运行为RUNNING
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&//当前线程状态大于等于SHUTDOWN,就是SHUTDOWN,STOP,TIDYING,TERMINATED几种状态中的某一种
! (rs == SHUTDOWN &&//SHUTDOWN状态,这时候不接受新的任务,可能队列中还有任务需要执行,那些已经添加到列队的任务是需要执行的
firstTask == null &&//任务为空
! workQueue.isEmpty()))//队列不为空
return false;//只有上面的条件符合才返回false,显然第一次进来不走这个逻辑
接着继续执行
for (;;) {
int wc = workerCountOf(c);//获取工作线程数
if (wc >= CAPACITY ||//大于等于容量
//开始传进来的core为true,下面这里执行wc>=corePoolSize,第一次进来不成立
wc >= (core ? corePoolSize : maximumPoolSize))
return false;//开始进来也不应该返回false
if (compareAndIncrementWorkerCount(c))
break retry;//比较增加的线程数和工作线程数是否一致,一致就跳出循环,
//这里的意思应该是线程池中可以开启新的工作线程来执行添加进来的任务,可以的话就返回true,然后就跳出循环了,
//这里第一次进来明显是符合要求的,所以就跳出循环了
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)//如果不是运行状态,跳到外层循环继续,这里先不关心
continue retry;
跳出循环后,继续执行
Worker w = new Worker(firstTask);
Thread t = w.thread;
//第一句创建了一个Worker对象,看构造函数
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);//创建一条线程
}
- 好,接着枷锁再判断一次
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 c = ctl.get();
int rs = runStateOf(c);
if (t == null ||
(rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null))) {
decrementWorkerCount();
tryTerminate();
return false;//如果在获得 锁 之后发现有人调用shutdown了,就尝试去结束,目前这个条件不符合,所以不执行这个条件
}
- 下一步添加到工作队列
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;//工作队列容量大于最大容量,就置为最大容量
} finally {
mainLock.unlock();
}
接着就执行这个工作的线程了
t.start();//这里会调用了本地的native方法去实现,这里涉及到jin的调用,
//由于没有C语言源码,暂时不用关心怎么实现,反正就是底层帮我们执行了这个线程。
if (runStateOf(ctl.get()) == STOP && ! t.isInterrupted())
t.interrupt();//如果中途有人中断线程,则尝试去中断,也涉及到本地方法
return true;//由上面的逻辑可知,这个addWorker()方法会返回true
- 回到execute()方法中的addWorker(),如果返回true,说明执行完了, 直接返回了。
if (addWorker(command, true))
return;
- 后面的逻辑就是当添加新的任务,如果当前线程数大于核心数,且任务队列不接受新的任务,就尝试增加新的线程,如果实在不行,就只能执行拒绝策略了。
if (isRunning(c) && workQueue.offer(command)) {//当前正在执行
//且往工作队列中添加成功,就再次获取当前工作线程数
int recheck = ctl.get();
- 继续,如果当前不是工作状态,并且移除成功,执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
- 如果当前工作线程为0,添加到工作队列中
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
- 最后做一次判断
else if (!addWorker(command, false))
reject(command);//添加不成功,执行拒绝策略
小结:
代码中的注释非常详细, 这里再简要概括一下. execute()方法主要分为以下四种情况:
- 情况1: 如果线程池内的有效线程数少于核心线程数 corePoolSize, 那么就创建并启动一个线程来执行新提交的任务.
- 情况2: 如果线程池内的有效线程数达到了核心线程数 corePoolSize, 并且线程池内的阻塞队列未满, 那么就将新提交的任务加入到该阻塞队列中.
- 情况3: 如果线程池内的有效线程数达到了核心线程数 corePoolSize 但却小于最大线程数 maximumPoolSize, 并且线程池内的阻塞队列已满, 那么就创建并启动一个线程来执行新提交的任务.
- 情况4: 如果线程池内的有效线程数达到了最大线程数 maximumPoolSize, 并且线程池内的阻塞队列已满, 那么就让 RejectedExecutionHandler 根据它的拒绝策略来处理该任务, 默认的处理方式是直接抛异常.