线程池简介
线程池技术一是用以控制系统中线程的数量,使得线程数不因太少而浪费资源,不因太多而导致效率不高(线程的创建和销毁都需要占用时间、资源)。
jdk1.4之前的版本中,线程池都极其简陋,jdk1.5之后引入了java.util.current之后,情况开始大幅改观。为我们解决线程问题提供了很大的便利。
java中线程池的接口
最顶级的接口是Excutors,但严格意义上说,Excutors并不是线程池,只是线程执行的工具,真正的线程池接口是ExcutorService。官方文档并不推荐使用构造方法创建线程池,因此我们一般使用Excutors
的一些方法来创建线程池:
1. newSingleThreadExecutor()
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. newFixedThreadPool()
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. newCachedThreadPool()
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4. newScheduledThreadPool()
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
下面看下各类关于它的具体实现
/**
*创建一个可重用固定数量线程的线程池
*在任何时候至多有n个线程处于活动状态
*如果在所有线程处于活动状态时提交其他任务,则它们将在队列中等待,
*直到线程可用。 如果任何线程在关闭之前的执行期间由于失败而终止,
*如果需要执行后续任务,则一个新的线程将取代它。池中的线程将一直存在
*知道调用shutdown方法
* @param nThreads 线程池中的线程数
* @param threadFactory 创建新线程时使用的factory
* @return 新创建的线程池
* @throws NullPointerException 如果threadFactory为null
* @throws IllegalArgumentException if {@code nThreads <= 0}
** /
public static ExcutorService newFixedThreadPool(int nThreads){
return new ThreadPoolExcutor(nThreads,nThreads,
0L,TimeUnit.MILLIONSECONDS,
new LinkedBlockingQueue<Runnable>);
}
在这里,corePoolSize和maximumPoolSize相等,keepAliveTime为0,LinkedBlockingQueue是无界的。
而单线程池
/**
*创建使用单个worker线程运行无界队列的Executor
*并使用提供的ThreadFactory在需要时创建新线程
*
* @param threadFactory 创建新线程时使用的factory
*
* @return 新创建的单线程Executor
* @throws NullPointerException 如果ThreadFactory为空
*/
public static ExcutorService newSingleThreadExcutor(){
return new ThreadPoolExcutor(1,1,
0L,TimeUnit.MILLIONSECONDS,
new LinkedBlockingQueue<Runnable>);
}
newCachedThreadPool的实现
//CachedThreadPool的corePoolSize被设置为空(0),maximumPoolSize被设置为Integer.MAX.VALUE,即它是无界的,
//这也就意味着如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线
//程。极端情况下,这样会导致耗尽cpu和内存资源。
public static ExcutorService newCachedThreadPool(){
return new ThreadPoolExcutor(0,Integer.MAX_VALUE,
60L,TimeUnit.MILLIONSECONDS,
new SynchronousQueue<Runnable>)
}
ThreadPoolExcutor详解
ThreadPoolExcutor的构造方法的参数列表
我们所说的是java.util.current.ThreadPoolExecutor
//构造函数有4个
public class ThreadPoolExecutor extends AbstractExecutorService {
.....
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
...
}
/**
*corePoolSize 线程池中保留的线程,包括闲置线程
*maximumPoolSize 线程池中允许的最大线程数
*keepAliveTime 当线程数大于核心数时,这是空闲线程等待新任务的最大时间
*unit -keepAliveTime的时间单位
*workingQueue 执行前用于保持任务的队列,此队列仅保持由excute()提交的Runnable任务
*threadFactory 执行程序创建新线程的工厂
*handler 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
*/
ThreadPoolExcutor
继承自AbstractExecutorService
,后者又实现了ExecutorService
接口,最后ExecutorService
接口又是Executor
的子接口,Executor
是一个顶层实现,其中只有一个execute(Runnable command)
方法,返回值为void
,我们可以轻易地理解,该方法是负责执行传入的任务的。
ThreadPoolExcutor中的重要方法
execute();
submit();
shutdown();
shutdownNow();
execute()
是Exuecutor
接口中声明的方法,在ThreadPoolExcutor
中进行了具体的实现,这个方法是ThreadPoolExcutor
的核心方法,通过这个方法可以向线程池提交一个任务,并由线程池去执行。
submit()
是在ExecutorService
中声明的方法,在AbstractExecutorService
中就有了具体的实现,ThreadPoolExcutor
中并没有对该方法进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()
有所不同,它能够返回任务执行的具体结果,通过查看submit()
的源码可以看到,事实上它依然需要调用execute()
方法,只不过,它利用了Future
来获取执行结果。
shutdown()
和shutdownNow()
是用来关闭线程池的。
还有一些可以获取线程池相关属性的方法例如
poolExecutor.allowsCoreThreadTimeOut();
poolExecutor.getActiveCount();
poolExecutor.getCorePoolSize();
poolExecutor.prestartAllCoreThreads();
有兴趣的可以自行查阅。
深入剖析线程池的实现原理
线程池的状态
ThreadPoolExcutor
中有一个volatile型的字段来表示线程池的状态,此外还规定了几个静态变量整数对状态进行了规定。
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
runstate
用volatile
修饰来保证线程之间的可见性。当创建了线程池后,初始时,线程池处于RUNNING
状态;调用了shutdown()
方法,则线程池处于相应的SHUTDOWN
状态,此时线程池不能接受新的任务,但是会等待所有任务执行完毕;如果调用了shutdownNow()
方法,那么线程池处于STOP
状态,此时线程池不再接受新的任务,并且尝试终止正在执行的任务,当线程池处于SHUTDOWN
或者STOP
状态,并且所有任务都被销毁,任务缓存队列清空或者执行完毕,线程池被置为TERMINATED
状态。
任务的执行
ThreadPoolExecutor
一些重要的成员变量
private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
//线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
private final ReentrantLock mainLock = new ReentrantLock();
private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
private volatile long keepAliveTime; //线程存活时间
private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
//核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int corePoolSize;
private volatile int maximumPoolSize; //线程池最大能容忍的线程数
private volatile int poolSize; //线程池中当前的线程数
private volatile RejectedExecutionHandler handler; //任务拒绝策略
private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
private long completedTaskCount; //用来记录已经执行完毕的任务个数
这里其实从变量的名称上就可以大致地推测出用处,这里我们需要注意的是largestPoolSize
并不是一个可以设置的线程池的规模,它只是一个记录数。
下面我们进入重点,从任务的提交到最终执行完毕到底经历了哪些过程。ThreadPoolExecutor
最核心的方法是execute()
,即使是submit()
方法,最终还是需要调用execute()
,所以我们有必要研究一下execute()
方法的实现。
//jdk8的情况
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
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 (workerCountOf(c) < corePoolSize)
,要判断当前的线程数是否不大于核心池的规模,如果不大于,那么就将用该任务创建一个Worker
并且将Worker
放入ThreadPoolExecutor
的workers
属性(一个HashSet)中,结束execute()
; - 进入下一个判断条件,如果线程池在运行中且任务成功地加入到了缓存任务队列中,继续进行double-check判断,前面我们已经检查过一次
isRunning(c)
,这里我们做二次检查,这是因为这是多线程操作,没有进行同步,所以需要再次check操作,如果此时不处于运行状态并且从缓存队列中删除成功,那么拒绝执行该任务,结束execute()
,否则判断当前线程数是否为0,是则添加进任务队列,否则结束execute()
; - 如果在条件3中失败了,则直接添加线程,添加失败则拒绝该任务。
关于BlockingQueue
所有的BlockingQueue都可以用于传输和保持提交的任务,可以使用此队列和池大小进行交互:
- 如果运行的线程少于线程池的corePoolSize,那么Excutor始终优先选择添加新的线程,而不进行排队;
- 如果线程数大于或者等于线程池的corePoolSize,那么优先选择进入队列,而不添加线程;
- 如果无法请求加入队列,则创建新的线程,除非线程数超过maximumPoolSize,这种情况下,任务会被拒绝。
排队的策略
- 直接提交:任务队列的默认选项是SynchronousQueue,它将任务直接提交给线程而不是保持它们。因此,如果不存在直接运行任务的线程,则试图将任务加入队列失效,因此会创建一个新的线程。此策略可以避免在处理具有内部依赖性的请求集时出现锁。直接提交应该要求无界maximumPoolSize以免拒绝任务提交。当命令以超过队列所能处理的平均数到达时,此策略允许无界线程线程具有增长的可能性。
- 无界队列:使用无界队列(例如不具有预定义容量的LinkedBlockingQueue)将导致所有corePoolSize线程都忙时新任务处在等待状态。这样创建的线程就不会超过corePoolSize,那么maximumPoolSize也就没有意义了。当每个任务之间相互独立时,适合于使用无界队列。当命令以超过队列处理的平均数到达时,此策略允许无界线程具有增长的可能性。
- 有界队列:当时用有界的maximumPoolSize时,使用有界队列(ArrayBlockingQueue)有助于避免资源的耗尽,但是较难调整和控制,队列大小和最大池大小可能需要相互折中:使用最大队列和小型池可以最大限度的降低CPU使用率,操作系统资源以及上下文切换的开销。但是可能也人工的造成了吞吐量的下降,使用小型池和大队列,则有可能增大了CPU的使用,但是又造成了不可接受的调度开销,同样会降低吞吐量。
参考文献
1.Java 线程池ThreadPoolExecutor(基于jdk1.8)(一)https://blog.csdn.net/youxitongyongming/article/details/77751874;
深入理解Java之线程池