目录
7、RejectedExecutionHandler handler
2、newFixedThreadPool(int nThreads)
4、newSingleThreadScheduledExecutor()和计划任务线程池
5、newScheduledThreadPool(int nThreads)
2、线程池的扩展:beforeExecute/afterExecute/terminated
3、线程池的关闭:shutdown/shutdownNow/isShutdown
4、线程池终止状态查询:isTerminating/isTerminated/awaitTermination
一、概览
1、背景
对于服务器应用,经常需要处理时间短但是数据庞大的请求。由于线程的创建和销毁会产生一定的开销,如果每次都为每个请求创建一个线程,执行完任务再去销毁,那么花在线程创建和销毁上的开销就会很大,导致系统的性能降低。
JDK 1.5中增加了并发包java.util.concurrent,其中包含接口Executor及一系列的子接口和实现类,提供了对线程池的支持,用于在高并发的情况下对线程进行合理的复用,以降低线程创建和销毁带来的开销。
2、继承关系
查看Executor的继承关系图谱可以看到:
这里我们讨论的重心是ThreadPoolExecutor,这也就是我们平时说的“线程池”。
二、线程池的内部实现
1、构造函数解析
ThreadPoolExecutor重载了多个构造函数,但是实际上其他构造函数都是在调用参数最多的构造函数。所以这里只是介绍一下参数最多的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
以下会逐一介绍构造函数的参数,通过理解参数的作用来了解线程池的原理。
1、int corePoolSize
核心线程池大小,即线程池中保存的线程数。
2、int maximumPoolSize
最大线程池大小,即线程池中允许的最大线程数。
3、long keepAliveTime
多余线程的存活时长。当线程数量超过corePoolSize的时候,多余的线程如果连续空闲时间超过keepAliveTime指示的时长(时长的单位由下一个参数指定),那么该多余空闲线程就会被销毁,直到线程池中的线程数量达到corePoolSize为止。
4、TimeUnit unit
时间的单位,和上面的参数keepAliveTime结合使用,来指示多余线程的存活时长。
5、BlockingQueue<Runnable> workQueue
1、概念
等待队列,当线程池中的线程数量达到或者多于corePoolSize且无空闲线程的时候,如果这时候有新的任务需要执行,那么这个任务会放入到workQueue中,等待有空闲线程的时候再去执行。
2、BlockingQueue的实现类
BlockingQueue接口有如下常见的实现类,可以用在ThreadPoolSize的实现类中:
1)直接提交的队列:SynchronousQueue。SynchronousQueue没有容量,每一个插入操作都要等待一个删除操作,每一个删除操作同样要等待一个插入操作。
如果在这里使用SynchronousQueue,那么提交的任务实际上并不会保存,而是直接提交给线程池去执行。如果这时候没有空闲的线程,那么就会创建新的线程;如果已经达到最大线程数,那么就会触发拒绝策略。
2)有界的任务队列:ArrayBlockingQueue。ArrayBlockingQueue的构造函数必须带一个int型的容量参数,表示该队列的最大容量。
当使用有界队列时,如果线程池的线程数已经达到了corePoolSize,那么任务就会提交到任务队列ArrayBlockingQueue中;当任务队列也满了之后,才会提交给线程池去创建新的线程。
3)无界的任务队列:LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的队列不存在任务入队失败的情况。
当使用无界队列时,如果线程池的线程数达到了corePoolSize,那么任务就会提交到任务队列LinkedBlockingQueue;但是因为LinkedBlockingQueue是无界的,永远不会满,所以线程池的线程数永远不会超过corePoolSize。
4)优先任务队列:PriorityBlockingQueue。PriorityBlockingQueue是带有执行优先级的队列,可以控制任务执行的先后。这是一个特殊的无界队列,跟其他队列最大的区别在于,其他队列ArrayBlockingQueue或者LinkedBlockingQueue都是按先进先出算法执行的,但是PriorityBlockingQueue可以根据任务自身的优先级顺序先后执行的。
6、ThreadFactory threadFactory
线程工厂,用于创建线程,可以定制化线程属性。一般使用默认的即可。
线程池中的线程是通过ThreadFactory来创建的。ThreadFactory接口中只有一个方法,是用来创建新线程的:
Thread newThread(Runnable r);
用户可以通过实现ThreadFactory并复写newThread方法,来自定义创建线程的逻辑,例如可以将线程设置为守护线程等。
7、RejectedExecutionHandler handler
拒绝策略。当等待队列已满,并且线程池中正在执行的线程数量已经达到maximumPoolSize的时候,如果有新的任务传入,这时候等待队列已经无法再存储新的任务,线程池也无法再创建新的线程去执行任务,那么这时候就会触发拒绝策略。
RejectedExecutionHandler接口有4个实现类,作为4中不同的拒绝策略:
1、AbortPolicy:直接抛出异常,阻止系统正常工作。
2、CallerRunsPolicy:只要线程池没有关闭,该策略就会直接在调用者的线程中运行当前被丢弃的任务。这样虽然不会丢弃任务,但是任务提交线程的性能可能会受到很大影响。
3、DiscardOldestPolicy:丢弃等待队列中时间最久的一个任务,也就是即将被执行的一个任务,并尝试再次提交当前任务。
4、DiscardPolicy:丢弃当前任务,不做处理。
如果上述JDK内置的策略无法满足需要,可以通过实现RejectedExecutionHandler接口来自定义拒绝策略。实现RejectedExecutionHandler接口需要实现接口方法:
/**
* Method that may be invoked by a {@link ThreadPoolExecutor} when
* {@link ThreadPoolExecutor#execute execute} cannot accept a
* task. This may occur when no more threads or queue slots are
* available because their bounds would be exceeded, or upon
* shutdown of the Executor.
*
* <p>In the absence of other alternatives, the method may throw
* an unchecked {@link RejectedExecutionException}, which will be
* propagated to the caller of {@code execute}.
*
* @param r the runnable task requested to be executed
* @param executor the executor attempting to execute this task
* @throws RejectedExecutionException if there is no remedy
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
2、线程池的任务调度逻辑
线程池ThreadPoolExecutor的任务调度逻辑如下:
三、常用的线程池
JDK的java.util.concurrent包提供了一个工厂类Executors,可以用来创建几种常见的线程池:
// 1.
public static ExecutorService newCachedThreadPool()
// 2.
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
// 3.
public static ExecutorService newSingleThreadExecutor()
// 4.
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
// 5.
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
1、newCachedThreadPool()
无界线程池。这种线程池实际上就是corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,workQueue的类型是直接提交的线程队列SynchronousQueue。它的特点是线程池的大小没有上线,并且任务不会保存在任务队列,会直接提交给线程池去执行。
当有新的任务提交的时候,如果有空闲的线程可以复用,就去复用空闲线程;如果没有空闲线程会立即创建线程去执行,并且不会有上限。缺点是当并发量特别高的时候,会导致开销很大。
2、newFixedThreadPool(int nThreads)
固定大小线程池。这种线程池需要传入整型参数作为线程池中线程的数量,这种线程池的特点是corePoolSize = maximumPoolSize,所以线程池中的线程数是固定的,数量固定为函数的输入参数nThreads;同时它使用无界队列存储任务。
当有新任务提交的时候,如果线程池中有空闲的线程,那么立即执行;如果没有,那么新的任务就会被暂存在一个任务队列中,等到有空闲线程再去按先入先出的顺序处理任务队列中的任务。
3、newSingleThreadExecutor()
单一线程池,相当于nThreads=1的newFixedThreadPool,线程池中固定只有一个线程,实际上就是一条线程以队列的形式来执行任务。
4、newSingleThreadScheduledExecutor()和计划任务线程池
方法返回一个ScheduledExecutorService的对象,线程池大小为1.ScheduledExecutorService接口扩展了在给定时间执行某项任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
计划任务:
ScheduledExecutorService接口的实现类对象,并不会立即执行任务,而是在一定的延时后再去执行。接口定义了4个方法,用于延时执行任务:
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
1)schedule(Runnable command, long delay, TimeUnit unit):在延时delay指定的时长后执行command任务,任务只会执行一次;
2)schedule(Callable<V> callable, long delay, TimeUnit unit):在延时delay指定的时长后执行callable任务,任务只会执行一次;和上面方法不同的地方是,这里执行的任务是有返回值的Callable任务,而上面的Runnable任务没有返回值。
3)scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):以固定频率(FixedRate)周期性执行任务command,第一次执行任务是在initDelay指定的时延后,然后每隔period时长就会执行一次任务。
4)scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):以固定时延(FixedDelay)周期性执行任务command,第一次执行任务是在initDelay指定的时延后,然后每次执行完任务后间隔时长period就会重复执行一次。
FixedRate和FixedDelay的区别:
1)FixedRate:指的是每次执行任务的开始时间是周期性的。例如周期是10s,任务执行耗时2s,第一次执行任务的时间点是10:00:00,那么10:00:02执行完第一次任务,第二次任务的开始时间是10:00:10。
2)FixedDelay:指的是下一次任务开始的时间,是上一层任务结束之后的特定时间之后。还是上面的例子,周期是10s,任务执行耗时2s,第一次执行任务的时间点是10:00:00,10:00:02执行完第一次任务,那么第二次任务的开始时间就变成了10:00:12。
对于FixedRate有个需要注意的事情,如果任务执行的耗时比周期长,例如周期是10s,但是任务花费了12s才执行完,这时候并不会出现两个任务并行执行的情况,第二个任务会在第一个任务执行完成之后才会去执行。在上面的例子中,周期是10s,任务执行耗时12s,第一次执行任务的时间点是10:00:00,10:00:12执行完第一次任务,那么第二次任务的开始时间是10:00:12。
5、newScheduledThreadPool(int nThreads)
方法返回一个ScheduledExecutorService的对象,线程池大小可以设置。
四、常用方法介绍
1、任务提交:submit/execute
// ExecutorService接口中定义的方法:
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);
// ThreadPoolExecutor中定义的方法:
public void execute(Runnable command)
submit方法和execute方法都可以用来提交任务给线程池去执行,但是两者有一些区别,如下:
1、定义方法的类不同:submit是在ExecutorService接口中定义的,而execute方法是在ThreadPoolExecutor类中定义的。
2、返回值类型不同:execute方法返回值为空,submit方法会返回线程的直接结果。
3、对异常的处理方式不同:
如果执行的任务中产生了异常,execute方法会直接打印产生的异常的堆栈,由于该异常是在子线程中产生的,主线程中包围在execute方法周围的try-catch语句并不能捕获该异常。
而submit方法提交的子线程如果产生了异常,当调用submit方法返回的Future实例的get方法时,可以在主线程中通过try-catch捕获该异常。这里需要注意的是,如果不调用Future示例的get方法,是不能捕获到异常的。
2、线程池的扩展:beforeExecute/afterExecute/terminated
protected void beforeExecute(Thread t, Runnable r) { }
protected void afterExecute(Runnable r, Throwable t) { }
protected void terminated() { }
线程池中提供了三个空函数体的方法,用于线程池的扩展。顾名思义,beforeExecute方法用来在执行一个任务之前执行,afterExecute方法在执行一个任务之后执行,而terminated方法在线程池关闭执行执行。
Runnable实现类:
class MyRunnable implements Runnable {
private String name = "running_" + new Random().nextInt(100);
public void run() {
System.out.println("run");
}
public String getName() {
return name;
}
}
主函数:
public class TestMain {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(5)) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("before execute: " + t.getName());
System.out.println("before execute: " + ((MyRunnable) r).getName());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("before execute: " + ((MyRunnable) r).getName());
}
@Override
protected void terminated() {
System.out.println("terminated");
}
};
for (int i = 0; i < 5; i++) {
Runnable runnable = new MyRunnable();
pool.execute(runnable);
}
pool.shutdown();
}
}
执行结果:
before execute: pool-1-thread-1
before execute: pool-1-thread-3
run
before execute: pool-1-thread-2
run
before execute: running_23
run
before execute: pool-1-thread-3
run
before execute: running_66
before execute: running_1
before execute: running_72
before execute: pool-1-thread-2
run
before execute: running_41
terminated
3、线程池的关闭:shutdown/shutdownNow/isShutdown
线程池使用完后,必须要进行关闭,否则无用的线程池会一直占据系统资源,导致内存泄漏的问题。
public void shutdown()
public List<Runnable> shutdownNow()
public boolean isShutdown()
shutdown():使当前未执行完的线程继续执行,而不再添加新的任务。shutdown()方法不会阻塞线程,调用shutdown()方法后主线程中线程池的使命就马上结束了,而线程池会继续运行直到所有线程执行完才会停止。
shutdownNow():使当前线程池中不再添加新任务,同时会给线程池中正在执行的线程打上中断标志,即让正在执行的线程调用isInterrupted()方法的返回值是true。
isShutdown():判断当前线程池是否已经关闭。执行过shutdown或者shutdownNow方法的线程池就是已经关闭的。
4、线程池终止状态查询:isTerminating/isTerminated/awaitTermination
public boolean isTerminating()
public boolean isTerminated()
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException
isTerminating():线程池是否正在正在关闭。当线程池执行shutdown()或者shutdownNow()方法后,但是还有任务在执行、还没有彻底关闭的时候,方法返回true。
isTerminated():线程池是否已经关闭。当线程池shutdown()或者shutdownNow()方法后,并且所有任务都已经执行完成、线程池已经彻底关闭的时候,方法返回true。
实际上,isShutdown()相当于(isTerminating() || isTerminated)。
awaitTermination(long timeout, TimeUnit unit):等待timeout指示的时间长度之后,去查看线程池是否已经关闭。
5、get方法:
几个构造函数参数的get方法就不用说了,这里介绍几个其他的get函数:
public int getActiveCount()
public long getCompletedTaskCount()
public int getLargestPoolSize()
public int getPoolSize()
public long getTaskCount()
getActiveCount():获取线程池中正在执行的任务的约数。这里有两点需要注意,一是“正在执行的任务”并不包括等待队列中的任务;二是这里得到的结果是个约数,因为可能会已经统计过的任务在统计过程中执行完的情况。
getCompletedTaskCount():获取线程池中已经执行完成的任务的总数。这也是个约数。
getLargestPoolSize():获取线程池中出现过的、并行执行任务最多的时候,并行执行的任务数。
getPoolSize():获取当前线程池中的线程数。
getTaskCount():获取线程池中提交的任务数量的总数,包括已经执行完成的、正在执行的和等待执行的任务。这也是个约数。