本文由作者收集整理所得,作者不保证内容的正确行,转载请标明出处。
作者:关新全
Java多线程 线程池
2.1 ThreadPoolExecutor类
ThreadPoolExecutor类是ExecutorService的一个实现。一般使用线程池可以减少任务调用的开销,对执行大量异步任务时提供增强的性能,并能提供绑定和管理资源的方法。每个线程池还记录了一些线程统计相关的数据。一般很少直接使用ThreadPooleExecutor进行线程的直接配置,在Executors类中提供了很多特殊场合下,已经配置好的ThreadPoolExecutor类型的线程池,例如newCachedThreadPool将提供一个缓冲的线程池,newFixedThreadPool将提供一个指定大小的线程池等。
下面主要探讨如何手动配置ThreadPoolExecutor。
2.1.1 corePoolSize和maximumPoolSize
ThreadPoolExecutor根据corePoolSize和MaximumPoolSize设置边界自动调整池的大小,如果在提交任务时,当前线程池运行任务的线程数目小于corePoolSize,即使已创建的线程有空闲的,ThreadPoolExecutor也会创建一个新的线程来执行新的任务。如果正在运行的线程数目在corePoolSize和MaximumPoolSize之间,除非队列已满,否则不会创建新的线程来执行新的任务。如果corePoolSize和MaximumPoolSize值相等,则此线程池是一个固定大小的线程池。可以将maximumPoolSize设置成大小无界(Integer.MAX_VALUE),则允许池适应任意大小的并发任务。可以通过ThreadPoolExecutor的构造函数或者ThreadPoolExecutor中提供的set方法设置corePoolSize和maximumPoolSize的大小。
2.1.2 创建新线程
ThreadPoolExecutor使用ThreadFactory创建新线程。如果在构造ThreadPoolExecutor实例时提供了ThreadFactory,则通过提供的ThreadFactory构造新的线程,否则通过Executors的defaultThreadFactory方法提供的ThreadFactory来创建新的线程。如果使用defaultThreadFactory提供的ThreadFactory来创建新线程,那么这些线程具有同样的优先级(NORM_PRIORITY)和守护进程状态等。可以通过自制的ThreadFactory改变线程名称、线程组、优先级、守护进程状态等。
2.1.3 keepAliveTime
如果池中的线程数量多于corePoolSize,那么这些多余的线程将会在空闲时间超出keepAliveTime后被终止,释放相应的资源。这样可以使池在处于低活动状态时,减少资源消耗。可以在ThreadPoolExecutor的构造中提供keepAliveTime参数,或者通过设置setKeepAliveTime来动态设置此参数。默认情况下,keepAliveTime适用于非核心线程,使用allowCoreThreadTimeOut方法,可以设置将此策略也应用于核心线程。
2.1.4 排队
ThreadPoolExecutor中使用BlockingQueue进行排队的。新任务与线程池间,可能出现下列交互。
1. 新任务到达时,线程池中活跃的线程数目小于corePoolSize,则添加新线程执行任务,而不考虑排队。
2. 新任务到达时,线程池中活跃的线程数目大于等于corePoolSize,则考虑将新任务放入队列。
3. 新任务到达时,线程池中活跃的线程数目大于corePoolSize,并且队列已经被填满,考虑创建一个新线程,执行任务。
4. 新任务到达时,线程池中活跃线程数等于maximumPoolSize,并且队列已经满了,则拒绝新任务。
队列策略:
1. 直接提交。如果创建ThreadPoolExecutor时提供的是SynchronousQueue,它将任务直接提交给线程而不保存它们。如果不存在可用于立即运行任务的线程,则试图将新任务放入队列将会导致失败(就是队列满了,就是前面说的情况3),因此会构造一个新的线程。这种策略可以避免任务之间因为锁关系,造成的死锁等待问题,为了避免出现任务间死锁现象发生,应当尽量将线程池的maximumPoolSize设置为最大,以避免拒绝服务。(在Executors中的newCachedThreadPool使用这种策略)。
2. 无界队列。如果在创建ThreadPoolExecutor时提供的是像LinkedBlockingQueue这样的无界队列时,交互中的3、4两条根本不会发生。因此maximumPoolSize是一个多余的设置。也就是说线程池中的线程数不会超过corePoolSize。在线程任务互不影响,并且线程池需要有一定的增长性时,可以考虑使用无界队列。(web服务是一个典型的例子,因为在web服务中,每个任务都是相互独立的,不可能有锁和同步等问题)。
3. 有界队列。如果在创建ThreadPoolExecutor时传入的是一个设定大小的ArrayBlockingQueue(这只是其中一种实现),那么线程池可以防止由于过多的请求造成的资源耗尽。在使用有界队列过程中,需要对maximumPoolSize和队列大小进行一定的权衡。使用大队列小线程池,可以降低CPU的使用率、操作系统资源和上下文切换开销,但是可能降低了吞吐量,因为同一时间并发度太低。使用小队列大线程池,CPU使用率较高,但却可能带来不可接受的调度开销,因此也会降低吞吐量。
2.1.5 拒绝任务
如果线程池已经关闭,或者线程池中队列已满并且活动线程到达了maximumPoolSize,此时新提交的任务将会被拒绝。拒绝处理程序将会调用RejectedExecutionHandler接口中的方法,对拒绝任务进行后续处理。ThreadPoolExecutor中提供了一些预定义的处理方法。
1. 默认使用ThreadPoolExecutor.AbortPolicy,新线程提交遭到拒绝,将会抛出RejectedExecutionException异常。
2. 使用ThreadPoolExecutor.CallerRunsPolicy,新线程将会被提交者线程执行(就是直接调用run方法,而不是创建一个新线程,调用start方法),这样很明显会减缓提交线程的提交速度。
3. 使用ThreadPoolExecutor.DiscardPolicy,新线程将会被直接抛弃,而没有任何反馈。
4. 使用ThreadPoolExecutor.DiscardOldestPolicy,将工作队列头部的任务删除,将新任务放入队列。
2.1.6 扩展(回调)
可以重写ThreadPoolExecutor中的beforeExecute和afterExecute方法,以便在线程开启和关闭时插入统计信息,或者进行日志的记录。
2.1.7 终止
对于不再引用的线程池,没有剩余的线程执行时,会自动执行shutdown(这个必须是0核心线程或者是allowCoreThreadTimeOut设置为true的情况)。可以使用shutdown方法的显示调用来确保池的回收,并取消正在执行的任务。建议显示使用关闭。
Graphic 2-1 ThreadPoolExecutor内部的类
![[转载]Java多线程 <wbr> <wbr>线程池 [转载]Java多线程 <wbr> <wbr>线程池](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
Graphic 2-2 ThreadPoolExecutor公有操作
![[转载]Java多线程 <wbr> <wbr>线程池 [转载]Java多线程 <wbr> <wbr>线程池](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
2.2 Executors类
Executors类是一个Executor、ExecutorService、ScheduleExecutorService、ThreadFactory的工厂方法和实用方法。
Graphic2-3给出了Executors的公共方法:
![[转载]Java多线程 <wbr> <wbr>线程池 [转载]Java多线程 <wbr> <wbr>线程池](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
Graphic2-4 Executors内部结构
![[转载]Java多线程 <wbr> <wbr>线程池 [转载]Java多线程 <wbr> <wbr>线程池](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
2.2.1 newCachedThreadPool
Executors类的newCachedThreadPool方法,可以获得一个ExecutorService实例。通过newCachedThreadPool获得的线程池实例具有伸缩的特性,可以根据当前执行任务的多少动态的分配新的线程,在线程执行结束后,并不会别立即释放线程资源,而是等待其超过keepAliveTime后才会释放,如果在等待期间有新的任务到来,那么这个非活动线程将会接受新的任务。
从Demo1-1中可以看出,实际上newCachedThreadPool创建一个ThreadPoolExecutor实例,并且将corePoolSize设置为0,将maximumPoolSize设置为最大,keepAliveTime设置为60s,使用的是SynchronousQueue。当一个新任务到达时,由于corePoolSize为0,所以无论何时线程池都试图向队列中插入元素,由于SynchronousQueue的特点,他不能接受任何类型的插入,因此,队列显示已经满了,因此会选择空闲的线程执行这个新任务,如果没有空闲线程,并且当前启动线程数又少于maximumPoolSize,则线程池选择创建新线程执行这个新任务。
这种类型的线程池,适合提交多个线程之间有关联的情况,以防出现死锁,每次提交的线程任务都会被立即执行。
还有一个与Demo1-1重载版本的newCachedThreadPool,带有一个ThreadFactory类型的参数。
Demo1-1给出了newCachedThreadPool的实现。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
2.2.2 newFixedThreadPool
Executors类的newFixedThreadPool方法,可以创建一个固定大小的线程池。这个线程池中一直维持着一个固定大小的线程数目(在开始执行时,这个线程数目有可能小于这个固定值)。当有新任务到达时,线程池先判断是否有空闲线程,如果有则使用空闲线程执行新任务,如果没有空闲线程,则将新任务放入对列。Demo1-2给出了newFixedThreadPool的实现。
从Demo1-2中可以看出,corePoolSize和maximumPoolSize设置成了相同的值,并且设置等待队列是一个无限阻塞队列。(这里的keepAliveTime设置是无意义的,因为线程数永远不可能超出corePoolSize)。当新任务到来时,线程池先查看是否有空闲的线程可以执行任务,如果存在这样的线程,则直接将任务提交给这个线程执行。如果没有空闲线程,则将任务放入队列中。(注意,即使maximumPoolSize设置大于corePoolSize,那么线程池也不会创建多于corePoolSize个线程,因为阻塞队列是一个无限的队列)。
Demo1-2 newFixedThreadPool的实现
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2.3 ExecutorCompletionService类
CompletionService接口适合于执行几组任务时临时使用。Graphic2-3给出了ExecutorCompletionService类关系。CompletionService的take方法,会阻塞等待这一组提交任务中的某一个执行结束就返回,并且将这个执行结束的线程结果反馈回来。可以想象可能存在这样一种情况,有一组计算,只要其中的一个计算结果执行结束,其他的计算结果就无关紧要时,使用CompletionService接口就方便的多,当我们将任务都提交给线程池,然后调用take方法阻塞等待其中一个任务执行结束,并反馈结果,其他的线程都可以取消。
Demo2-3给出一个使用CompletionService的例子,在这个例子中,分别创建了两组CompletionService(即cs1和cs2),cs1用来计算0-99的和,cs2用来计算0-999的和,这两个CompletionService都使用了同一个线程池。并且两组计算都是各自执行各自的,互不影响。
使用CompletionService执行组任务会十分方便,在这个接口的实现类ExecutorCompletionService中,包装了一个Executor接口的实现,并且在创建ExecutorCompletionService时,可以为这个完成服务设置一个队列。这个队列是用来做完成队列来使用的。也就是说可以用这个队列取消那些未执行结束的线程。
Graphic2-3类关系。
![[转载]Java多线程 <wbr> <wbr>线程池 [转载]Java多线程 <wbr> <wbr>线程池](https://i-blog.csdnimg.cn/blog_migrate/a4c26d1e5885305701be709a3d33442f.gif)
Demo1-3 使用ExecutorCompletionService例子。
package com.upc.upcgrid.guan.SpecialUse.chapter01;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletionExecutorInterface {
private static class OnceComput implements Callable<Integer>{
private int start;
private int end;
public OnceComput(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = start ; i < end ; i++)
sum += i;
return sum;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService pool = Executors.newFixedThreadPool(5);
CompletionService<Integer> cs1 = new ExecutorCompletionService<Integer>(pool);
CompletionService<Integer> cs2 = new ExecutorCompletionService<Integer>(pool);
for(int i = 0 ;i < 10 ; i++)
{
cs1.submit(new OnceComput(i*10, (i+1)*10));
}
for(int i = 0 ;i < 10 ;i++)
{
cs2.submit(new OnceComput(i*100,(i+1)*100));
}
int sum1 = 0;
for(int i = 0 ; i < 10 ; i++)
{
sum1 += cs1.take().get();
}
System.err.println(sum1);
int sum2 = 0;
for(int i = 0 ; i < 10 ;i++)
{
sum2 += cs2.take().get();
}
System.err.println(sum2);
}
}