转自:https://blog.csdn.net/u010723709/article/details/50377543
开发过程中,合理地使用线程池可以带来3个好处:
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
ThreadPoolExecutor有以下四个构造方法
这里面需要几个参数
1 corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
2 workQueue(任务队列) : 用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
3 maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没用了。
4 threadFactory(线程工厂):可以通过线程工厂为每个创建出来的线程设置更有意义的名字,如开源框架guava
5 RejectedExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略还处理新提交的任务。它可以有如下四个选项:
更多的时候,我们应该通过实现RejectedExecutionHandler 接口来自定义策略,比如记录日志或持久化存储等。
6 keepAliveTime(线程活动时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程利用率。
7 TimeUnit(线程活动时间的单位):可选的单位有天(Days)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
可以使用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();
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无响应中断的任务可能永远无法停止。但是他们存在一定的区别,shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有正在执行的任务。
只要调用了这两个关闭方法的一个,isShutdown就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这是调用isTerminated方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定执行完,则可以调用shutdownNow方法。
要想合理地配置线程池,首先要分析任务特性
性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应该配置尽可能少的线程,如配置N+1个线程,N位CPU的个数。而IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*N。混合型任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量
优先级不同的任务可以交给优先级队列PriorityBlcokingQueue来处理。
执行时间不同的任务可以交给不同规模的线程池来处理。
依赖数据库的任务,因此线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间越长,那么线程数应该设置的越大,这样能更好滴利用CPU。
(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则是立即停止所有任务。
在本例中想要验证线程池扩容到核心数量,然后再扩容到最大数量,最后再缩小到核心数量的过程。
(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,可以适当地增加队列长度,但不并不一定能看到扩大最大容量,其原因将在下一节中讲到。在线程池中任务都运行完毕后,可以看到线程池回收了多余的线程,但并没有完全回收,而是保持在核心线程数量。从这里也可以看出,合理地设置核心线程的数量可以减少线程的频繁创建和回收,而这才是线程池的真正作用。