开发过程中,合理地使用线程池可以带来3个好处:
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
1 线程池的创建
ThreadPoolExecutor有以下四个构造方法
- ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
- ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
- ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
- ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
这里面需要几个参数
1 corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
2 workQueue(任务队列) : 用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,按FIFO原则进行排序
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,吞吐量高于ArrayBlockingQueue。静态工厂方法Excutors.newFixedThreadPool()使用了这个队列
- SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,静态工厂方法Excutors.newCachedThreadPool()使用了这个队列
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
3 maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没用了。
4 threadFactory(线程工厂):可以通过线程工厂为每个创建出来的线程设置更有意义的名字,如开源框架guava
5 RejectedExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略还处理新提交的任务。它可以有如下四个选项:
- AbortPolicy:直接抛出异常,默认情况下采用这种策略
- CallerRunsPolicy:只用调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
更多的时候,我们应该通过实现RejectedExecutionHandler 接口来自定义策略,比如记录日志或持久化存储等。
6 keepAliveTime(线程活动时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程利用率。
7 TimeUnit(线程活动时间的单位):可选的单位有天(Days)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
2 提交任务
可以使用execute和submit两个方法向线程池提交任务
(1)execute方法用于提交不需要返回值的任务,利用这种方式提交的任务无法得知是否正常执行
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
(2) submit方法用于提交一个任务并带有返回值,这个方法将返回一个Future类型对象。可以通过
这个返回对象判断任务是否执行成功,并且可以通过future.get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。
Future> future=threadPoolExecutor.submit(futureTask);
Object value=future.get();
3 关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无响应中断的任务可能永远无法停止。但是他们存在一定的区别,shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有正在执行的任务。
只要调用了这两个关闭方法的一个,isShutdown就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这是调用isTerminated方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定执行完,则可以调用shutdownNow方法。
4 合理配置线程池
要想合理地配置线程池,首先要分析任务特性
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应该配置尽可能少的线程,如配置N+1个线程,N位CPU的个数。而IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*N。混合型任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量
优先级不同的任务可以交给优先级队列PriorityBlcokingQueue来处理。
执行时间不同的任务可以交给不同规模的线程池来处理。
依赖数据库的任务,因此线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间越长,那么线程数应该设置的越大,这样能更好滴利用CPU。
5 线程池应用示例
1 示例1 验证shutdown和shutdownNow的区别
(1)首先构造一个线程池,用ArrayBlockingQueue作为其等待队列,队列初始化容量为10。该线程池核心容量为 10,最大容量为20,线程存活时间为1分钟。
static BlockingQueue blockingQueue=new ArrayBlockingQueue<>(10);
static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);
(2)另外构造了一个实现Runable接口的类TaskWithoutResult,其逻辑很简单,睡眠1秒
/**
* 无返回值的任务
* @author songxu
*
*/
class TaskWithoutResult implements Runnable
{
private int sleepTime=1000;//默认睡眠时间1s
public TaskWithoutResult(int sleepTime)
{
this.sleepTime=sleepTime;
}
@Override
public void run()
{
System.out.println("线程"+Thread.currentThread()+"开始运行");
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {//捕捉中断异常
System.out.println("线程"+Thread.currentThread()+"被中断");
}
System.out.println("线程"+Thread.currentThread()+"结束运行");
}
}
(3)验证
/**
* 中断测试
*/
public static void test1()
{
for(int i=0;i<10;i++)
{
Runnable runnable=new TaskWithoutResult(1000);
threadPoolExecutor.submit(runnable);
}
//threadPoolExecutor.shutdown();//不会触发中断
threadPoolExecutor.shutdownNow();//会触发中断
}
分别测试shutdown和shutdownNow()方法,结果shutdown()方法的调用并不会引发中断,而shutdownNow()方法则会引发中断。这也正验证前面所说的,shutdown方法只是发出了停止信号,等所有线程执行完毕会关闭线程池;而shutdownNow则是立即停止所有任务。
2 示例2 验证线程池的扩容
在本例中想要验证线程池扩容到核心数量,然后再扩容到最大数量,最后再缩小到核心数量的过程。
(1)首先构造一个线程池,用ArrayBlockingQueue作为其等待队列,队列初始化容量为1。该线程池核心容量为 10,最大容量为20,线程存活时间为1分钟。
static BlockingQueue blockingQueue=new ArrayBlockingQueue<>(1);
static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);
(2)另外构造了一个实现Runable接口的类TaskBusyWithoutResult类,其模拟一个繁忙的任务
class TaskBusyWithoutResult implements Runnable
{
public TaskBusyWithoutResult()
{
}
@Override
public void run()
{
System.out.println("线程"+Thread.currentThread()+"开始运行");
int i=10000*10000;
while(i>0)
{
i--;
}
System.out.println("线程"+Thread.currentThread()+"运行结束");
}
}
(3)测试验证,向线程池提交20个任务
/**
* 扩容测试
*/
public static void test2()
{
for(int i=0;i<20;i++)
{
Runnable runnable=new TaskBusyWithoutResult();
threadPoolExecutor.submit(runnable);
}
}
(4)验证结果
在VisualVM中观察线程的变化,在任务提交的瞬间,线程池完成了预热到扩容到最大线程,之所以这么迅速是因为本例中的等待队列长度只有1,可以适当地增加队列长度,但不并不一定能看到扩大最大容量,其原因将在下一节中讲到。在线程池中任务都运行完毕后,可以看到线程池回收了多余的线程,但并没有完全回收,而是保持在核心线程数量。从这里也可以看出,合理地设置核心线程的数量可以减少线程的频繁创建和回收,而这才是线程池的真正作用。