思维导图:
引言:
本章主要介绍如何选择,配置和扩展线程池。所以,本文的所有部分都归属于使用部分。
- 使用部分:介绍如何选择合适的线程池,配置合适的线程池参数,并扩展线程池的功能
一.选择线程池
在前文中提到过,Executors可以产生不同类型的线程池,每种线程池的特性和功能不同。当我们需要执行一个或者多个任务的时候,需要选择恰当的线程池提交并执行任务,如果选择了不恰当的线程池可能会带来不好的结果。
在上篇文章中也提到过,不同的任务选择的执行策略是不同的。某些任务则和执行策略有隐性的耦合条件,致使我们只能选择特定的线程池。以下几个小节则分别介绍了其中一些种类的有耦合的任务。
1.1 依赖性任务
某些任务的执行需要依赖其他任务执行的结果,而如果这些返回结果的任务又被阻塞导致不能执行的话,这种情况称之为线程饥饿死锁。
如果我们选择了newSingleThreadExecutor来创建线程池又想其中提交具有依赖性的任务的话,就很有可能导致线程饥饿死锁。如下例:RenderPageTask的执行完成需要两个LoadFileTask的执行结果,但是由于我们选择了单线程Executor,导致只有RenderPageTask执行完毕后才会执行LoadFileTask任务,所以,结果就是发生了线程饥饿死锁。
public class ThreadDeadlock {
ExecutorService exec = Executors.newSingleThreadExecutor();
public class LoadFileTask implements Callable<String> {
private final String fileName;
public LoadFileTask(String fileName) {
this.fileName = fileName;
}
public String call() throws Exception {
// Here's where we would actually read the file
return "";
}
}
public class RenderPageTask implements Callable<String> {
public String call() throws Exception {
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
// Will deadlock -- task waiting for result of subtask
return header.get() + page + footer.get();
}
private String renderBody() {
// Here's where we would actually render the page
return "";
}
}
}
1.2 时间敏感性任务
对于某些需要较长时间才能执行完毕的任务,则需要选择没有线程数量上限的线程池。否则,当执行任务的线程数量少于需要大量时间才能执行完毕的任务的数量时,很有可能会发生所有线程都执行长时间任务的情况,这就导致线程池会在相当长的一段时间内没有响应。这时就因该选择使用newCachedThreadPool而不是newFixedThreadPool。
1.3 线程封闭性任务
当需要执行使用了线程封闭的任务时,最好选择使用newSingleThreadExecutor线程池,而不是多线程线程池,以防止对象泄露并确保任务不会并发的执行以丧失线程安全性。
1.4 使用ThreadLocal的任务
如果任务使用了ThreadLocal,则不要讲ThreadLocal对象用于线程间的参数传递。
二.配置线程池
线程池有一些通用的配置参数,比如线程池的基本大小corePollSize,最大线程数量maximumPoolSize,存活时间keepAliveTime,工作队列workQueue,线程工厂,和饱和策略处理器。本节将介绍如何配置合适的线程池参数。
2.1 设置线程池的大小
当线程池是计算密集型即处理大量运算的时候,CPU数量+1是个不错的选择,而当线程池是I/O密集型的时候,则需要根据任务的等待时间,执行时间等参数进行设置,总之是一句废话,具体情况,具体分析。
2.2 线程的创建与销毁
线程的创建与销毁有线程池的基本大小,最大大小和存活时间这三个参数来决定。基本大小就是在没有任务执行时所维护的线程的数量,最大大小则表示可以同时活动的线程数量的上限。如果线程空闲时间超过了存活时间,那么线程的资源就会被回收。在我们常用的线程池中,newFixedThreadPool的基本大小和最大大小都是指定的值,而且创建的线程不会超时。newCashedThreadPool的基本大小是0,最大大小是Integer.MAX_VALUE,超时时间为1分钟。
2.3 管理队列任务
在有限的线程池中会限制可并发执行的任务的数量,那么为执行的任务将被保存在一个队列中。任务的基本排队方式有三种
- 无界队列 : 无限的提交任务,可能会导致内存溢出。
- 有界队列 : 当待执行任务的数量达到上线后,将执行饱和策略。
- 同步移交: 通过使用SynchromousQueue直接将任务从生产者交给工作者线程以避免任务排队。
newFixedThreadPool和newSingleThreadPool默认情况下使用的时一个无界的LinkedBlockingQueue。
2.4 饱和策略
如果我们将newFixedThreadPool和newSingleThreadPool的工作队列修改为有界队列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue或者PriorityBlockingQueue的话,可以更好的管理线程池所使用的资源,但是这将会导致一个问题:如果待执行任务数量达到了上限怎么办,此时,线程池将执行饱和策略用于处理此类问题。一般的我们有以下几种饱和策略:
- 终止Abort : 默认的饱和策略,将会抛出RejectedExecutionExeption,我们可以捕获并进行相应的处理。
- 抛弃Discard : 悄悄的抛弃该任务。
- 抛弃最旧的Discard-Oldest :抛弃下一个将被执行的任务,当任务队列是PriorityBlockingQueue的时候将抛弃优先级最高的任务。
- 调用者运行Caller-Runs : 将任务回退给调用者,即在调用了executor的线程中执行该任务。
2.5 线程工厂
每单线程池需要创建线程的时候,都会通过默认的线程工厂进行创建,此时创建的线程都是新的,非守护的线程。我们可以定制自己的线程工厂以让线程池创建独特的线程。如下:
- 线程工厂类
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable runnable) {
return new MyAppThread(runnable, poolName);
}
}
- 自定义线程类
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 runnable, String name) {
super(runnable, name + "-" + created.incrementAndGet());
setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
public void uncaughtException(Thread t,
Throwable e) {
log.log(Level.SEVERE,
"UNCAUGHT in thread " + t.getName(), e);
}
});
}
@Override
public void run() {
// Copy debug flag to ensure consistent value throughout.
boolean debug = debugLifecycle;
if (debug) {
log.log(Level.FINE, "Created " + getName());
}
try {
alive.incrementAndGet();
super.run();
} finally {
alive.decrementAndGet();
if (debug) {
log.log(Level.FINE, "Exiting " + getName());
}
}
}
public static int getThreadsCreated() {
return created.get();
}
public static int getThreadsAlive() {
return alive.get();
}
public static boolean getDebug() {
return debugLifecycle;
}
public static void setDebug(boolean b) {
debugLifecycle = b;
}
}
三.扩展线程池
我们可以对线程池做少量的扩展工作,例如添加前后处理或者进行使用完毕后的类似资源回收的终止处理。
我们可以重写以下方法:
- beforeExecute:每个线程执行前调用此方法
- aferExecute : 每个线程执行后调用此方法
- terminated : 线程池完成关闭操作时调用此方法
在这个例子中,我们扩展了线程池以实现日志和计时功能,并在线程池关闭后进行打印。
public class TimingThreadPool extends ThreadPoolExecutor {
public TimingThreadPool() {
super(1, 1, 0L, TimeUnit.SECONDS, null);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private final Logger log = Logger.getLogger("TimingThreadPool");
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.fine(String.format("Thread %s: start %s", t, r));
startTime.set(System.nanoTime());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
try {
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
log.fine(String.format("Thread %s: end %s, time=%dns",
t, r, taskTime));
} finally {
super.afterExecute(r, t);
}
}
@Override
protected void terminated() {
try {
log.info(String.format("Terminated: avg time=%dns",
totalTime.get() / numTasks.get()));
} finally {
super.terminated();
}
}
}