线程池是并发里面最经常使用的工具类,所以想要晋升为中级Java工程师,学好并发的前提就是学好线程池。这里会介绍关于线程池的一些内容,比如线程池的创建过程和原理,如何理解线程池的各种状态以及之间的转换,理解面试中常说的FixedThreadPool,SingleThreadExecutor,CachedThreadPool,ScheduledThreadPool四种池原理以及使用场景等等,在分析原理的过程中会给出具体的使用实例,尽量能够理解到位。
这是开篇,关于常规线程池ThreadPoolExecutor的创建过程以及各参数含义的介绍。
Java语言关于线程池的类图:
ThreadPoolExecutor就是我们口中常说的线程池,ScheduledThreadPoolExecutor是继承自它的子类,江湖上称周期线程池,它的作用跟它的名字一样,可以周期性的执行某一个任务。这里我们重点学习ThreadPoolExecutor类,整个过程我们思考以下几个问题:
在Java世界里,万物皆对象,所以线程池也抽象成一个ThreadPoolExecutor类,每需要一个线程池就可以new一个ThreadPoolExecutor对象;所以创建线程池也是通过new来实现的。JDK通过重载构造函数提供四种不同形式的构造方法:
// 构造函数A 5个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
// 构造函数B 6个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
// 构造函数C 6个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
// 构造函数D 7个参数
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;
}
理解构造函数中每个参数的含义是理解线程池的关键,又因为构造函数A、B和C内部都会调用构造函数D,所以我们首先理解构造函数D中的参数。
以上是创建线程池时需要的7个参数,而回过头来看其他构造函数,你会发现:
构造函数A传递5个构造参数,但是使用默认的线程工厂Executors.defaultThreadFactory和默认的拒绝处理策略defaultHandler;
默认线程工厂构造函数:这里定义了线程所属的线程组以及线程名称的前缀.
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
}
默认拒绝策略defaultHandler:
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
该处理策略表明线程池在拒绝任务时直接抛出运行时RejectedExecutionException异常。
另外其他三种处理策略:
构造函数B传递6个构造参数,相比构造函数A只是使用了自定义的线程工厂;
构造函数C传递6个构造参数,它使用默认的线程工厂,其他7个参数都由外部自定义传递。
在构造函数D中除了7个构造参数外,还会初始化AccessControlContext对象的acc的值。acc对象是指当前调用上下文的快照,其中包括当前线程继承的AccessControlContext和任何有限的特权范围,使得可以在稍后的某个时间点(可能在另一个线程中)检查此上下文。
到这里,线程池创建的过程算是结束了,回过头来看它主要决定了7个对象:
明白线程池的创建过程,能回答开始提出的几个问题:
常规线程池通过提供的四个构造函数new出来,并且根据实际情况即参数个数选择对应的构造函数。
线程池中的线程根据任务的多少由线程工厂创建。如果当前线程数少于核心线程数(corePoolSize),则来一个任务就新创建一个线程;如果当前线程数大于核心线程数且小于最大线程数,新来的任务会优先放到等待队列,等待队列已满则新创建一个线程,线程池内的最大线程不能大于maximumPoolSize。线程池内的存活时间也跟任务数有关;如果线程一直处于工作状态,那么一直存活;如果有池内线程数大于核心线程数且有空闲线程,那么空闲线程等待keepAliveTime之后进行销毁;如果池内线程数小于核心线程数且有空闲线程,线程不会销毁,除非设置了allowCoreThreadTimeOut属性(true/false),其值默认为false,核心线程不会销毁,若设置为true,则空闲核心线程维持keepAliveTime时间后销毁。
判定是不是多余任务,就看池内现有线程数是否等于核心线程数,对于核心线程数处理不及时的任务就是多余任务;池通过等待队列把多余任务存起来,等待队列分为三种:
带着剩下的几个问题继续往下看。
这几个问题会在下一篇继续分析。