第八章 线程池的使用
1.线程饥饿死锁
在线程池中,如果任务依赖于其他任务,那么可能导致死锁。在单线程的Executor中,第二个任务停留在工作队列中,等待第一个任务的完成,之后到第二个任务开始执行,而第一个任务又需要第二个任务完成的结果,因此这样就导致死锁。只要线程池中的任务需要无限期地等待一些必须由池中其它任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或执行结果,除非线程池足够大,否则将发生线程饥饿死锁。例如:
/**
* 在单线程Executor中任务发生死锁(不要这么做)
* @author cream
*
*/
public class ThreadDeadLock {
ExecutorService exec = Executors.newSingleThreadExecutor();
public class RenderPageTask implements Callable<String>{
public String call() throws Exception {
Future<String> header,footer;//定义页眉,页脚
header = exec.submit(new LoadFileTask("header.heml"));//提交获取页眉的任务
footer = exec.submit(new LoadFileTask("footer.heml"));//提交获取页脚的任务
String page = renderBody();
//将发生死锁,由于任务在等待子任务结束
return header.get()+page+footer.get();
}
}
}
2.配置ThreadPoolExecutor
相当于私人订制的线程池,可以设置核心数和最大线程数等。核心池大小, 最大池大小, 存活时间共同管理着线程的创建与销毁. 即使没有任务执行, 池的大小也等于核心池的大小, 并且直到工作队列充满前, 池都不会创建更多的线程. 最大池大小是可以同时活动的线程的上限, 如果一个线程已经闲置的时间超过了存活时间, 它将成为一个被收回的候选者, 如果当前的池的大小超过了核心池的大小, 线程会终止该候选者。
newFixedThreadPool:工厂为请求的池设置了核心池的大小和最大池的大小, 而且它永远不会超时;
newCachedThreadPool:工厂将最大池的大小设置为Integer.MAX_VALUE, 核心池的大小设置为0, 超时时间设置为1分钟, 这样创建出来的无限扩大的线程池会在需求量减少的情况下减少线程数量.
ThreadPoolExecutor提供一个BlockingQueue来保存等待执行的任务。基本的排队方法有3种,无界队列,有界队列和同步移交(Synchronous)。
newFixedThreadPool和newSingleThreadExecutor:默认使用的是一个无界的LinkedBlockingQueue, 如果所有的工作者线程都处于忙碌状态, 任务将会在队列中等候, 如果任务持续地到达, 超过了它被执行的速度, 队列会无限地增加.
SynchronousQueue:当你需要一个庞大,且线程无限的线程池,可以使用SynchronousQueue,它完全绕开队列, 将任务直接从生产者移交到工作者线程, 因为SynchronousQueue并不是一个真正的队列, 而是一种管理直接在线程间移交信息的机制。
只有当池的大小是无限的, 或者可以接受任务被拒绝, SynchronousQueue才是一个有实际价值的选择, newCachedThreadPool工厂就是用了SynchronousQueue.
3.饱和策略
当有界队列被填满后,饱和策略开始发挥作用。饱和策略包括:
1. 中止策略(AbortPolicy):默认的饱和策略,会抛出RejectedExecutionException: 调用者可以捕获这个隐藏然后编写满足自己需求的处理代码
2. 抛弃策略(DiscardPolicy):当最新提交的任务不能进入队列等待执行时, 遗弃(discard)策略会默认放弃这个任务
3. 遗弃最旧策略(DiscardOldestPolicy):选择丢弃的任务是本应该接下来就应该执行的任务, 该策略还会尝试去重新提交新任务。(该策略最好不要和优先级队列一起使用)
4. 调用者运行策略(CallerRunsPolicy):既不会丢弃哪个任务, 也不会抛出任何异常. 它会把一些任务退回到调用者那里, 从此缓解新任务流. 他不会在池线程中执行最新提交的任务, 但是他会在一个调用了execute的线程中执行。当工作队列充满后, 并没有预置的饱和策略来阻塞execute
4.线程工厂以及定制ThreadPoolExecutor
通过自定义线程工厂可以对其进行扩展加入新的功能实现
当应用需要利用安全策略来控制某些特殊代码库的访问权,可以利用PrivilegedThreadFactory来定制自己的线程工厂,以免出现安全性异常。将与创建privilegedThreadFactory的线程拥有相同的访问权限、AccessControlContext和contextClassLoader
可以在创建线程池后,再通过Setter方法设置其基本属性(将ExecutorService扩展为ThreadPoolExecutor)
在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置
5.扩展ThreadPoolExecutor
执行任务的线程会调用这些方法,用它们去添加日志、时序、监视器或者统计信息的收集。就好像是通过AOP去实现一些切面逻辑一样,ThreadPoolExecutor让我们自己去实现已经提供的这些切面。