Java线程池
导语:
在Java中,大家都知道,多线程开发往往都面试中以及工作中经常处理的一类技术难题,而线程池则是我们在开发中,必不可少的并发框架,因为它可以有效的利用线程,使其发挥最大的作用。想要做到这一点,我们应该对于它的原理以及是如何进行工作的,掌握清楚。
当我们在线程池中新提交一个任务时,线程池会进行如下的操作:
(1)检查线程池中的corePoolSize:如果当前运行线程小于corePoolSize,就进行线程创建来执行任务(需要获得全局锁)
(2)当核心线程池中正在运行的线程大于或者等于corePoolSize时,则将该任务放入任务队列BlockingQueue中。
(3)如果任务队列已满,则在线程池中创建线程进行任务执行。
(4)当创建线程之后,会导致线程池中的线程数大于maximumPoolSize的话,则会将任务交给饱和策略来进行处理。
下面我么能通过JDK - 8源码看看,execute()的执行过程:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 如果运行的线程少于corePoolSize线程,请尝试使用给定命令作为其第一个任务来启动新线程。
* 对addWorker的调用从原子上检查runState和workerCount,从而通过返回false来防止在不应该添加线程的情况下发出虚假警报。
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 如果一个任务可以成功排队,那么我们仍然需要仔细检查是否应该添加一个线程(因为现有线程自上次检查后就死掉了)或该池自进入此方法后就关闭了。
* 因此,我们重新检查状态,并在必要时回滚排队(如果已停止),或者在没有线程的情况下启动新线程。
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*
* 如果我们无法将任务排队,则尝试添加一个新线程。如果失败,则表明我们已关闭或已饱和,因此拒绝该任务
*/
// 原子类实例变量 ctl 是一个主池控制常量,用于进行workCount和runState字段的更改
// workCount 表示线程数
// runState 表示线程的生命周期,包括运行,关闭等
int c = ctl.get();
// 检查核心线程池中线程的数量是否小于corePoolSize
if (workerCountOf(c) < corePoolSize) {
// 当满足上面的条件时,我们通过调用addWorker方法,来进行任务的线程分配
if (addWorker(command, true))
return;
c = ctl.get();
}
// 若corePoolSize小于workerCount,
// 则说明,核心线程池中没有空闲的工作线程
// 检查当前线程是否存活,并且能否添加到workQueue中
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);
}
这里简单提一下工作线程的概念,就是说,在执行execute()方法时,通过调用
private boolean addWorker(Runnable firstTask, boolean core)
来将我们的任务封装为一个工作线程,使其能在线程池中工作。如果该任务被放到了BlockingQueue中时,线程会不断的从中获取任务来执行。
那我们再来看看线程池的一些基本属性和线程池的一些创建方法
1、maximumPoolSize:最大池大小。请注意,实际的最大值在内部受CAPACITY限制。
而这里的CAPACITY我们简单介绍一下:
// 为了将它们打包为一个int,我们将workerCount限制为(2 ^ 29)-1(约5亿个)线程,而不是(2 ^ 31)-1(20亿个)可表示的线程。
//如果将来有问题,可以将该变量更改为AtomicLong,并调整以下移位掩码常量。
//但是在需要之前,使用int可以使此代码更快,更简单。
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
2、corePoolSize:核心线程数的大小,通过源码中的注释,我么你可以获悉,这个参数是动态的,它是保持活动状态线程的最低数量,需要注意的一点是,即使线程池中有空闲线程,当工作线程数量没有达到此参数时,依旧会创建新的线程,而不是空闲的线程
3、workQueue:用于保存等待执行任务的阻塞队列,常见的队列有下面几种:
1)ArrayBlockingQueue:FIFO,基于有界数组实现的阻塞队列
2)LinkedBlockingQueue:FIFO,基于链表实现的阻塞队列
3)SynchronousQueue:不存储任务的阻塞队列,插入操作必须等到其他线程执行完移除操作,否则以一直会处于阻塞状态。
4)PriorityBlockingQueue:基于优先级的无限阻塞队列。吞吐量: SynchronousQueue > LinkedBlockingQueue > ArrayBlockingQueue
4、饱和策略:是当线程池和任务对流都以达到峰值,来进行这种饱和状态下的一些策略模式,具体有下面几种:
1)AbortPolicy:直接抛出异常
2)CallerRunsPolicy:只用调用者所在线程来运行线程
3)DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
4)DiscradPolicy:不进行处理,直接丢弃掉
execute()与submit()方法
execute():方法调用没有返回值
submit():方法调用有返回值,而我们截至目前,一直使用Future接口来接收处理结果,这一点到下一篇文章中讲解。