线程池知识梳理
1.线程饥饿死锁
在线程池中,如果任务依赖于其他任务,那么可能导致死锁。在单线程的Executor中,第二个任务停留在工作队列中,等待第一个任务的完成,之后到第二个任务开始执行,而第一个任务又需要第二个任务完成的结果,因此这样就导致死锁。只要线程池中的任务需要无限期地等待一些必须由池中其它任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或执行结果,除非线程池足够大,否则将发生线程饥饿死锁。例如:
//在单线程Executor中任务发生死锁(不要这么做)
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();
}
}
}
饥饿死锁图如下:
除了在线程池大小上的显示限制外,还可能由于其他资源上的约束而存在一些隐式限制。如果应用程序使用一个包含10个连接的JDBC连接池,并且每个任务需要一个数据库连接,那么线程池就好像只有10个线程,因为当超过10个任务时,新的任务需要等待其他任务释放连接。
当运行时间较长的任务时同样存在问题:
如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。执行时间较长的任务不仅会造成线程池堵塞,甚至还会增加执行时间较短任务的服务时间。如果线程池中线程数量远小于稳定状态下执行时间较长任务的数量,那么最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。
2.设置线程池的大小
线程池的理想大小取决于被提交任务的类型和所部署系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或者根据Runtime.availableProcessors来动态计算。
如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源。如果线程池过小,那么将导致许多空闲的处理器无法执行工作,从而降低吞吐率。
对于计算密集型任务,在拥有NCPU个处理器的系统上,当线程池的大小为NCPU+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被浪费。)
对于I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。
下面给出估算线程池最优大小的定义和公式:
- NCPU=CPU的数量
- UCPU=CPU的利用率,0≤UCPU≤1
- W/C=等待时间与计算时间的比率
线程池的最优大小等于:
Nthread=NCPUUCPU(1+W/C)
线程池大小的上限等于:
线程池大小的上限=可用资源总量 / 每个任务的需求量
3.配置ThreadPoolExecutor
可以使用ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己的需求来定制。构造函数形式如下:
//ThreadPoolExecutor的通用构造函数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecytionHandler handler) {
...
}
3.1线程的创建与销毁
线程池的基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。
基本大小就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超过这个数量的线程。
线程池的最大大小表示可同时活动的线程数量上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过基本大小时,这个线程将被终止。
newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。
newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为零,并将超时设置为1分钟,这种方法创建出来的线程池可以被无线拓展,并且当需求降低时会自动收缩。
3.2管理队列任务
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。
基本的任务排队方法有3种:
- 无界队列
- 有界队列
- 同步移交
newFixedThreadPool
和newSingleThreadExecutor
在默认情况下将使用一个无界的LinkedBlockingQueue
。
但是使用无界队列有可能导致队列大小无法控制。一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue
、有界的LinkedBlockingQueue
、PriorityBlockingQueue
。有界队列有助于避免资源耗尽情况的发生,但是在使用时需要指定相应的饱和策略。
对于非常大的或者无界的线程池,可以通过使用SynchronousQueue
来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue
不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。
只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。
3.3饱和策略
当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略。)
JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略分别为:
- AbortPolicy
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
“中止(Abort)”策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionHandler。
“调用者运行(Caller-Runs)”策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
当新提交的任务无法保存到队列中等待执行时,“抛弃(Discard)”策略会悄悄抛弃该任务。
“抛弃最旧的(Discard-Oldest)”策略则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。如果工作队列时一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将“抛弃最旧的”饱和策略和优先队列放在一起使用。
3.4线程工厂
每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息。通过指定一个线程工厂方法,可以定制线程池的配置信息。在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用这个方法。
//ThreadFactory接口
public interface ThreadFactoy {
Thread newThread(Runnable r);
}
可以通过实现接口来自定义线程工厂,如下所示:
//自定义的线程工厂
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
super();
this.poolName = poolName;
}
@Override
public Thread newThread(Runnable r) {
return new MyAppThread(r);
}
}
//定制Thread基类
public class MyAppThread extends Thread {
public static final String DEFAULT_NAME="MyAppThread";
private static volatile boolean debugLifecycle = false;
private static final AtomicInteger created = new AtomicInteger();
private static final AtomicInteger alive = new AtomicInteger();
private static final Logger log = Logger.getAnonymousLogger();
public MyAppThread(Runnable r) {
this(r, DEFAULT_NAME);
}
public MyAppThread(Runnable r, String name) {
super(r, name+ "-" + created.incrementAndGet());
setUncaughtExceptionHandler( //设置未捕获的异常发生时的处理器
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
log.log(Level.SEVERE, "UNCAUGHT in thread " + t.getName(), e);
}
});
}
@Override
public void run() {
boolean debug = debugLifecycle;
if (debug)
log.log(Level.FINE, "running thread " + getName());
try {
alive.incrementAndGet();
super.run();
} finally {
alive.decrementAndGet();
if (debug)
log.log(Level.FINE, "existing thread " + getName());
}
}
}
如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限,那么可以通过Executors中的privilegedThreadFactory工厂来定制自己的线程工厂。
3.5在调用构造函数后再定制ThreadPoolExecutor
- 可以在创建线程池后,再通过Setter方法设置其基本属性(将ExecutorService扩展为ThreadPoolExecutor)
- 在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置
4.扩展ThreadPoolExecutor
ThreadPoolExecutor使用了模板方法模式,提供了beforeExecute、afterExecute和terminated扩展方法
- 线程执行前调用beforeExecute(如果beforeExecute抛出了一个RuntimeException,那么任务将不会被执行)
- 线程执行后调用afterExecute(抛出异常也会调用,如果任务在完成后带有一个Error,那么就不会调用afterExecute)
- 在线程池完成关闭操作时调用terminated,也就是所有任务都已经完成并且所有工作者线程也已经关闭后