在任务与执行策略之间的隐形耦合
Executor将任务的提交和任务的执行策略解耦开,但实际中并非所有的任务都适用所有的执行策略,因此任务和执行策略之间会有一些隐形的耦合。
- 依赖性任务: 任务如果是独立的,那么可以随意的改变线程池的大小和配置(只会对执行性能产生影响)。如果提交给线程池的任务依赖其他的任务,那么随意改变执行策略,可能会产生活跃性问题。
线程的饥饿死锁: 在线程池中,如果任务依赖于其他任务,那么可能产生死锁。
如:在单线程的Executor,a任务将b任务提交到同一个Executor,并且等待b任务的结果,那么会引发死锁(b任务在队列中等待,而a任务又无法完成,a任务正等待b任务的结果,陷入互相等待)。 即使在更大的线程池中也可能产生这个问题,在执行的所有任务都等待着队列中任务的结果。这种现象被称为线程饥饿死锁。
public class ExecutorTest {
private static ExecutorService executor = Executors.newSingleThreadExecutor();
static class Render implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("开始");
Future<String> head = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "head";
}
});
Future<String> foot = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "foot";
}
});
String headPage = head.get();
String footPage = foot.get();
System.out.println("结束");
return headPage+"body"+footPage;
}
}
public static void main(String[] args){
ExecutorTest.executor.submit(new ExecutorTest.Render());
}
}
- 使用线程封闭机制的任务: 单线程的Executor会放宽代码对于线程安全的要求,当Executor换成线程池的配置可能会产生线程安全问题
- 对响应时间敏感的任务: 一些任务要求响应性高,如果将任务提交单线程的Executor或者提交到只有少量线程的线程池,会降低任务的响应性。
- 使用ThreadLocal的任务: ThreadLocal可以是每个线程拥有某个变量的一个自己的‘版本’。但是线程池的线程是可以复用的,任务结束后线程可能并不会消失。所有ThreadLocal的值应该关联任务的生命周期,而不是线程的生命周期。线程池的线程中也不应该用ThreadLocal在任务间传递值。
设置线程池的大小
线程池过大会导致cpu频繁切换,占用过多的内存,过小会导致空闲的cpu无法执行,降低吞吐率。合理的设置线程池的大小,需要考虑任务的特性。
- CPU密集型:线程池线程数 = Ncpu +1;
- IO密集型:线程池线程数 = Ncpu * 目标cpu利用率 * (1+ 线程等待时间与cpu等待时间之比);
- 获取cpu数量:int N_CPU = Runtime.getRuntime().availableProcessors();
配置ThreadPoolExecutor
Executors提供了一些基本的实现,也可以通过ThreadPoolExecutor的构造函数,构造一些符合自己需求的线程池。
线程的创建与销毁
下面三个参数关乎着线程的创建与销毁:
- 线程池核心线程数量:基本线程池大小,也就是没有任务是,线程池会运行的数量
- 线程池最大线程数量:允许同时运行的线程数量
- 最大空闲时间:线程最大的空闲的时间,超过了这个时间被标记为可回收。如当前线程数大于核心线程数,那么就会被终止。
队列
线程池通过队列来保存等待执行的任务。主要有以下几种队列:
- 无界队列:newFixedThreadPool,newSingleThreadExecutor默认情况下都是使用的无界队列(危险的是队列长度可能无限制增长)。
- 有界队列:有界队列就是指有长度限制的队列,队列填满后,再创建非核心线程,最大线程也满了,就走拒绝策略。
- 同步移交队列:对于非常大或者无界的线程池,可以使用SynchronousQueue来避免任务排队。SynchronousQueue并不是一个真正的队列,要将一个元素放入SynchronousQueue中,必须有一个线程正在等待,如果没有线程等待就创建一个线程(如果已经是最大线程数了,就走拒绝策略)
阿里规范中明确不允许通过Executors来创建线程池
Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
拒绝策略
当队列和最大线程数满了后,任务开始走拒绝策略。有以下四种提供的拒绝策略:
- 中止:抛出RejectedExecutionException(默认的拒绝策略)
- 调用者运行:在调用者的线程中去串行执行
- 抛弃最旧的:抛弃队列中下一个将要执行的任务,将最新的加进来。
- 抛弃:放弃该任务
扩展ThreadPoolExecutor
ThreadPoolExecutor是扩展的,它提供了几个方法可以在子类中重写。
- beforeExecute:在任务执行前调用,如果抛出了一个RuntimeException,那么任务不再执行。
- afterExecute:无论任务是从run中正常返回,还是抛出一个异常返回,都会调用afterExecute。(如果任务在完成后带有一个ERROR,那么不会被调用!这句话实在没理解意思= = !)
- terminated:线程池完成关闭操作时调用terminated。可以用来释放各种资源。