Java线程池基础
目录:
一、线程池概述
二、线程池参数
三、线程池的执行过程
四、线程池的主要实现
五、线程池的使用
六、线程池的正确关闭方式
七、线程池参数调优
一、线程池概述
1、线程池类
目前线程池类一般有两个,一个来自于Spring,一个来自于JDK:
- 来自Spring的线程池:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
- 来自JDK的线程池:java.util.concurrent.ThreadPoolExecutor
说明:两个线程池类中的参数【线程池最大线程数】写法不同,在Spring线程池中为maxPoolSize,在JDK线程池中为maximumPoolSize,等价。
两个线程池类的配置差不多,Spring的做了一些配置参数的简化,最终调用JDK的API。
在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要线程池里有空闲的线程,任务就会分配给一个线程执行。在线程池的内部,当线程数量达到线程池核心线程数时,后续的任务被插入一个阻塞队列(BlockingQueue)进行等待,线程池里的空闲线程会去取这个队列里的任务。
利用线程池的三个好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
2、相关概念比喻
- 线程池(thread pool) 工厂
- 线程(thread) 工人,属于某个工厂,被工厂所管理
- 任务(task) 等待工人处理的事情,即实现Runnable或Callable的类
3、线程池行为比喻
- 小赵(任务)去银行(线程池)办理业务,银行刚开始营业,此时窗口工作人员还未就位(初始线程数为0)
- 于是经理(线程池管理者)催促1号正式员工到1号窗口接待小赵(创建线程),于是小赵被安排到1号窗口办理业务(执行线程任务)
- 接着小钱(任务)也来到银行(线程池)办理业务,此时小赵还没有办理完业务,1号窗口轮不到小钱。该银行总共有2个窗口(corePoolSize为2),于是经理又催促2号正式员工到2号窗口接待小钱(又创建线程),小钱也开始办理业务(执行线程任务)
- 紧接着小孙(又一个任务)也来到银行办理业务,此时前面两人还未办理完业务。在银行等待区有一张座位(缓存队列size为1)空着,于是小孙被经理安排到座位上等待并被告知:当1、2号窗口有空闲时,小孙就可以去窗口办理业务。此时,窗口满了,等待区也满了。
- 这时小李也来到银行办理业务,于是经理安排临时工(corePoolSize之外的线程)在大堂手持移动设备为小李办理业务
- 银行业务很繁忙,窗口满了、等待区满了、临时工也用上了(线程数达到maxPoolSize)。此时小周来到银行办理业务,于是经理只能按照《超出银行最大接待能力处理办法》(拒绝策略)拒绝小周办理业务
- 随后,小赵、小钱、小孙、小李陆续办完业务离开银行。忙碌了大半天,来办理业务的人终于少了,此时临时工已经闲置了2个小时(keepAliveTime),2个窗口可处理之后并不繁忙的业务,经理见临时工没事做就让他下班,以避免造成不必要的资源浪费
- 根据银行《正式员工空闲时处理办法》(是否清理corePoolSize线程开关),即使正式员工闲着也不得提前下班。所以,1号、2号窗口的正式员工继续等待接待客户(线程池内保持corePoolSize个线程)
二、线程池参数
ThreadPoolExecutor类的构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
构造函数的参数含义如下:
- corePoolSize:指定了线程池中核心线程的大小。它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去。当提交一个任务到线程池,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建新的线程,等到需要执行的任务数大于corePoolSize时就不再创建。(1、在创建线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。除非调用了prestartAllCoreThreads()方法或prestartCoreThread()方法,在任务没有到来之前就预创建corePoolSize个线程或一个线程。2、在创建线程池后,默认情况下,线程池中的线程数为0,当有任务到来时线程池就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把后续到达的任务放到缓存任务队列当中。核心线程在allowCoreThreadTimeout被设置为true时会超时并被回收,默认情况下不会被回收)
- maxPoolSize/maximumPoolSize:指定了线程池中最大线程数量,即线程池允许创建的最大线程数。这个参数会根据使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量。如果任务队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。(当线程数大于等于corePoolSize,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maximumPoolSize。如果线程已等于maximumPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会按照一定的处理策略处理)
- keepAliveTime:线程活动保持时间,线程池的工作线程空闲后,保持存活的时间。当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间之后被销毁。(1、当线程空闲时间达到keepAliveTime,该线程会退回,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会陆续退出直到线程数量为0)
- unit:线程活动保持时间的单位。常用取值如下:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微秒
TimeUnit.NANOSECONDS; //纳秒
- workQueue:阻塞队列,用来存储等待执行的任务。
阻塞队列有以下几种选择:
1、ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序
2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状 态,吞吐量通常要高于 LinkedBlockingQueue。静态工厂方法Executors.newCachedThreadPool使用了这个队列
4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列
- threadFactory:线程工厂,用于设置创建线程,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
- handler:饱和策略(拒绝策略),当线程池和阻塞队列都满了,说明线程池处于饱和状态,必须采取一种策略处理提交的新任务。
当线程数量达到maximumPoolSize时的处理策略有以下几种:
1)ThreadPoolExecutor.AbortPolicy:丢弃任务,并抛出RejectedExecutionException异常
2)ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常
3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的等待时间最久的任务,然后重新尝试执行任务(重复此过程)
4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务,谁调用返回给谁
注意:Spring的线程池和JDK线程池中的拒绝策略默认值都是ThreadPoolExecutor.AbortPolicy
三、线程池的执行过程
执行流程图:
1、当线程池中线程数小于corePoolSize时,对于新提交的任务,线程池将创建一个新线程来执行任务,即使此时线程池中存在空闲线程
2、当线程池中线程数达到corePoolSize时,新提交的任务将会被线程池放入workQueue队列中,等待线程池中任务调度执行
3、当workQueue已满,且corePoolSize < maximumPoolSize时,对于新提交的任务,线程池将创建新线程来执行任务
4、当提交的任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5、当线程池中线程数超过corePoolSize时,空闲线程的空闲时间达到keepAliveTime时,空闲线程会被关闭
6、当设置allowCoreThreadTimeOut(true)时,线程池中的核心线程空闲时间达到keepAliveTime时也将被关闭
线程池的工作顺序:corePoolSize -> 任务队列 -> maximumPoolsize -> 拒绝策略
四、线程池的主要实现
通过调用Executors类中的静态工厂方法可创建不同的线程池,这些线程池的内部实现原理都是相同的,仅仅是使用了不同的工作队列或线程池大小,如下:
1、newFixedThreadPool:创建一个固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小,线程池的大小一旦达到最大值就会保持不变。如果某个线程因为执行异常而结束,线程池会补充一个新的线程。
构造函数如下:
//第一个构造函数,参数只有线程数量,核心线程数与最大线程数一致
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
} //第二个构造函数,参数包含核心线程数和线程工厂,核心线程数与最大线程数一致
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
2、newSingleThreadExecutor:创建一个单线程的线程池,这个线程池只有一个线程在工作,也就是串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
构造函数如下:
//构造函数,默认核心线程数和最大线程数都是1
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3、newCachedThreadPool:创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程数,那么就会回收部分空闲(60秒不执行任务)的线程;当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或JVM)能够创建的最大线程大小。
构造函数如下:
//构造函数,核心线程数为0,最大线程数为Integer.MAX_VALUE,空闲线程超时时间为60秒
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
4、newScheduledThreadPool:创建一个固定长度的线程池,支持定时的以及周期性的任务执行,类似于Timer
构造函数如下:
//第一个构造函数,指定核心线程数大小
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
} //第二个构造函数,指定核心线程数大小及线程工厂
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
注意:线程池一般不允许使用Executors去创建,而要通过ThreadPoolExecutor方法创建,一方面是由于Executors框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活。另外,由于前面几种方法内部都是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
五、线程池的使用
1、向线程池提交任务方式
- 使用execute向线程池提交任务
public class ExecuteTest { public static void main(String[] args) { BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 3, 60, TimeUnit.SECONDS, workQueue);
poolExecutor.execute(new TaskOne());
poolExecutor.execute(new TaskTwo());
poolExecutor.shutdown();
}
} class TaskOne implements Runnable{ @Override
public void run() {
System.out.println("正在执行任务1...");
}
} class TaskTwo implements Runnable{ @Override
public void run() {
System.out.println("正在执行任务2...");
}
}
执行结果:
正在执行任务1...
正在执行任务2...
- 使用submit方法向线程池提交任务,返回一个Future对象。可通过这个Future对象来判断任务是否执行成功,通过get()方法获取返回值,get()方法会阻塞直到任务完成
public class SubmitTest { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> resultList = new ArrayList<Future<String>>();
//创建10个任务并执行
for(int i = 0;i < 10;i++){
//使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
Future<String> future = executorService.submit(new TaskWithResult(i));
resultList.add(future);
}
//遍历结果集
for(Future<String> future : resultList){
//Future返回如果没有完成,则一直循环等待,直到Future返回完成
while(!future.isDone());{
//打印各个线程(任务)执行的结果
System.out.println(future.get());
}
}
executorService.shutdown();
}
} class TaskWithResult implements Callable<String>{ private int id;
public TaskWithResult(int id){
this.id = id;
} @Override
public String call() throws Exception {
return "执行结果" + id;
}
}
执行结果:
执行结果0
执行结果1
执行结果2
执行结果3
执行结果4
执行结果5
执行结果6
执行结果7
执行结果8
执行结果9
2、执行定时及周期性任务
- Timer工具管理定时及周期性任务。示例代码如下:
public class TimerTest { static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { TimerTask timerTaskOne = new TimerTask() {
@Override
public void run() {
System.out.println("任务1执行时间:" + sdf.format(new Date()));
try{
//模拟任务1执行时间3秒
Thread.sleep(3000);
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
}; System.out.println(String.format("当前时间:" + sdf.format(new Date())));
Timer timer = new Timer();
//间隔4秒钟周期性执行任务1
timer.schedule(timerTaskOne, new Date(), 4000);
}
}
执行结果:
当前时间:2019-09-24 16:35:39
任务1执行时间:2019-09-24 16:35:39
任务1执行时间:2019-09-24 16:35:43
任务1执行时间:2019-09-24 16:35:47
任务1执行时间:2019-09-24 16:35:51
任务1执行时间:2019-09-24 16:35:55
上述任务1以4秒为间隔周期性执行。但是Timer存在一些缺陷,主要是两方面的问题:
缺陷1:Timer只能创建一个唯一的线程来执行所有的TimerTask任务,如果一个TimerTask任务的执行很耗时,会导致其他的TimerTask的准确性出现问题。代码如下:
public class TimerDefectTestOne { static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { TimerTask timerTaskOne = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务1执行时间:" + sdf.format(new Date())));
try{
Thread.sleep(10000);
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
}; TimerTask timerTaskTwo = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务2执行时间:" + sdf.format(new Date())));
}
}; System.out.println("当前时间:" + sdf.format(new Date()));
Timer timer = new Timer();
//间隔1秒周期性执行任务1
timer.schedule(timerTaskOne, new Date(), 1000);
//间隔4秒周期性执行任务2
timer.schedule(timerTaskTwo, new Date(), 4000);
}
}
执行结果:
当前时间:2019-09-24 16:40:51
任务1执行时间:2019-09-24 16:40:51
任务2执行时间:2019-09-24 16:41:01
任务1执行时间:2019-09-24 16:41:01
任务1执行时间:2019-09-24 16:41:11
任务2执行时间:2019-09-24 16:41:21
任务1执行时间:2019-09-24 16:41:21
任务1执行时间:2019-09-24 16:41:31
任务2执行时间:2019-09-24 16:41:41
任务1执行时间:2019-09-24 16:41:41
由执行结果可看出任务2的执行周期并不是4秒,与缺陷1内容描述符合。
缺陷2:如果TimerTask抛出未检查的异常,Timer将产生无法预料的行为。Timer线程并不捕获异常,所有TimerTask抛出的未检查的异常都会终止Timer线程。代码如下:
public class TimerDefectTestTwo { static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { TimerTask timerTaskOne = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务1执行时间:" + sdf.format(new Date())));
throw new RuntimeException();
}
}; TimerTask timerTaskTwo = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务2执行时间:" + sdf.format(new Date())));
}
}; System.out.println("当前时间:" + sdf.format(new Date()));
Timer timer = new Timer();
//间隔1秒周期性执行任务1
timer.schedule(timerTaskOne, new Date(), 1000);
//间隔4秒周期性执行任务2
timer.schedule(timerTaskTwo, new Date(), 4000);
}
}
执行结果:
当前时间:2019-09-24 16:48:27
任务1执行时间:2019-09-24 16:48:27
Exception in thread "Timer-0" java.lang.RuntimeException
at com.aisino.threadPool.TimerDefectTestTwo$1.run(TimerDefectTestTwo.java:22)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
- Timer缺陷的解决方法:使用ScheduledThreadPoolExecutor替换Timer
针对缺陷1,使用ScheduledThreadPoolExecutor的替换Timer。代码如下:
public class ScheduledThreadPoolExecutorTestOne { static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { TimerTask timerTaskOne = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务1执行时间:" + sdf.format(new Date())));
try{
Thread.sleep(10000);
}catch(InterruptedException ex){
ex.printStackTrace();
}
}
}; TimerTask timerTaskTwo = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务2执行时间:" + sdf.format(new Date())));
}
}; System.out.println("当前时间:" + sdf.format(new Date()));
ScheduledThreadPoolExecutor poolExecutor = new ScheduledThreadPoolExecutor(2);
poolExecutor.scheduleAtFixedRate(timerTaskOne, 0, 1000, TimeUnit.MILLISECONDS);
poolExecutor.scheduleAtFixedRate(timerTaskTwo, 0, 4000, TimeUnit.MILLISECONDS);
}
}
执行结果:
当前时间:2019-09-24 16:52:05
任务1执行时间:2019-09-24 16:52:05
任务2执行时间:2019-09-24 16:52:05
任务2执行时间:2019-09-24 16:52:09
任务2执行时间:2019-09-24 16:52:13
任务1执行时间:2019-09-24 16:52:15
任务2执行时间:2019-09-24 16:52:17
任务2执行时间:2019-09-24 16:52:21
任务1执行时间:2019-09-24 16:52:25
任务2执行时间:2019-09-24 16:52:25
任务2执行时间:2019-09-24 16:52:29
根据执行结果可看出,任务1以10秒为间隔执行,任务2以4秒为间隔周期性执行,解决缺陷1。
针对缺陷2,使用ScheduledThreadPoolExecutor的替换Timer。代码如下:
public class ScheduledThreadPoolExecutorTestTwo { static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { TimerTask timerTaskOne = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务1执行时间:" + sdf.format(new Date())));
throw new RuntimeException();
}
}; TimerTask timerTaskTwo = new TimerTask() {
@Override
public void run() {
System.out.println(String.format("任务2执行时间:" + sdf.format(new Date())));
}
}; System.out.println("当前时间:" + sdf.format(new Date()));
ScheduledThreadPoolExecutor poolExecutor = new ScheduledThreadPoolExecutor(2);
poolExecutor.scheduleAtFixedRate(timerTaskOne, 0, 1000, TimeUnit.MILLISECONDS);
poolExecutor.scheduleAtFixedRate(timerTaskTwo, 0, 4000, TimeUnit.MILLISECONDS);
}
}
执行结果:
当前时间:2019-09-24 16:56:42
任务1执行时间:2019-09-24 16:56:42
任务2执行时间:2019-09-24 16:56:42
任务2执行时间:2019-09-24 16:56:46
任务2执行时间:2019-09-24 16:56:50
任务2执行时间:2019-09-24 16:56:54
任务2执行时间:2019-09-24 16:56:58
任务2执行时间:2019-09-24 16:57:02
任务2执行时间:2019-09-24 16:57:06
由执行结果可看出,当任务1因异常而停止时,任务2仍正常以4秒为间隔周期性执行,解决缺陷2。
3、关闭线程池
关闭线程池可通过调用的shutdown()方法或shutdownNow()方法来实现,两个方法的实现原理不同。shutdown()方法的原理是将线程池的状态由RUNNING转变为SHUTDOWN状态,SHUTDOWN状态下线程池不再接受新任务,但是会将工作队列中的任务执行结束,然后中断空闲线程。shutdownNow()方法的原理是遍历线程池中的所有线程,然后逐个调用线程的interrupt方法来中断线程。shutdownNow()方法会首先将线程池的状态设置为STOP,然后尝试中断所有线程(包括工作线程和空闲线程),并返回工作队列中所有未完成任务的列表。
只要调用了两个方法中的任意一个,isShutdown()方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminated()方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown()来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow()方法。
六、线程池的正确关闭方式
应用停机时,需要释放资源,关闭连接。对于一些定时任务或者网络请求服务将会使用线程池,当应用停机时需要正确安全的关闭线程池,如果处理不当,可能造成数据丢失,业务请求结果不正确等问题。
关闭线程池我们可以选择什么都不做,JVM关闭时会自然的清除线程池对象。这么做存在很大的弊端,线程池中正在执行的线程以及队列中还未执行的任务将会变得不可控。所以我们需要想办法控制这些正在执行的线程以及未执行的任务。
ThreadPoolExecutor类中提供了两个主动关闭的方法:shutdown()和shutdownNow(),这两个方法都可以用于关闭线程池,但是具体效果不一样。
1、线程池的状态
线程池状态关系图如下:
线程池总共存在5种状态,分别为:
- RUNNING:线程创建之后的初始状态,这种状态下可以执行任务。
- SHUTDOWN:该状态下的线程池不再接受新任务,但是会将工作队列中的任务执行结束。
- STOP:该状态下线程池不再接受新任务,但是不会处理工作队列中的任务,并且将会中断线程。
- TIDYING:该状态下所有任务都已终止,将会执行terminated()钩子方法。
- TERMINATED:执行完terminated()钩子方法之后。
当执行shutdown()方法时将会使线程池状态从RUNNING转变为SHUTDOWN,而调用shutdownNow()方法之后线程池状态将会从RUNNING转变为STOP。从上图可看出,当线程池处于SHUTDOWN状态,还可以继续调用shutdownNow()方法,将其状态转变为STOP。
2、shutdown()方法
shutdown()方法源码如下:
public void shutdown(){
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//检查权限
checkShutdownAccess();
//设置线程池状态
advanceRunState(SHUTDOWN);
//中断空闲线程
interruptIdleWorkers();
//钩子函数,主要用于清理一些资源
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}
shutdown()方法首先加锁,其次检查系统安装状态,接着将线程池状态转变为SHUTDOWN,在这之后线程池不再接收提交的新任务。此时如果继续向线程池提交任务,将会使用线程池拒绝策略响应,默认情况下将会使用ThreadPoolExecutor.AbortPolicy,抛出RejectedExecutionException异常。
interruptIdleWorkers()方法只会中断空闲的线程,不会中断正在执行任务的线程。空闲的线程将会阻塞在线程池的阻塞队列上。
3、shutdownNow()方法
shutdownNow()方法源码如下:
public List<Runnable> shutdownNow(){
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//检查状态
checkShutdownAccess();
//将线程池状态转变为STOP
advanceRunState(STOP);
//中断所有线程,包括工作线程以及空闲线程
interruptWorkers();
//丢弃工作队列中的存量任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
shutdownNow()方法将会把线程池状态设置为STOP,然后中断所有线程,最后取出工作队列中所有未完成的任务返回给调用者。
对比shutdown()方法,shutdownNow()方法比较粗暴,直接中断工作线程。不过需要注意:中断线程并不代表线程立刻结束。这里需要线程主动配合线程中断响应。
线程池的shutdown()方法与shutdownNow()方法都不会主动等待执行任务的结束,如果需要等到线程池任务执行结束,需要调用awaitTermination主动等待任务调用结束。
调用方法如下:
poolExecutor.shutdown();
try{
while(!poolExecutor.awaitTermination(60, TimeUnit.SECONDS)){
System.out.println("线程池任务还未执行结束");
}
}catch(InterruptedException ex){
ex.printStackTrace();
}
如果线程池任务执行结束,awaitTermination()方法将会返回true,否则当等待时间超过指定时间后将会返回false。如果需要使用这种机制,建议在上面的基础上增加一定重试次数。
线程中断机制:线程中的interrupt()方法只是设置一个中断标志,不会立即中断正常的线程。如果想让中断立即生效,必须在线程内调用Thread.interrupted()判断线程的中断状态。对于阻塞的线程,调用中断时,线程将会立即退出阻塞状态并抛出InterruptedException异常。所以对于阻塞线程需要正确处理InterruptedException异常。
4、优雅关闭线程池
由线程池状态关系图可知,处于SHUTDOWN状态下的线程池依旧可以调用shtudownNow()方法,所以可以结合shutdown、shutdownNow、awaitTermination,更加优雅地关闭线程池。
//调用shutdown()方法关闭线程池
poolExecutor.shutdown();
try{
//等待60秒
if (!poolExecutor.awaitTermination(60, TimeUnit.SECONDS)){
//调用shutdownNow取消正在执行的任务
poolExecutor.shutdownNow();
//再次等待60秒,如果还未结束,可以再次尝试,或者直接放弃
if(!poolExecutor.awaitTermination(60, TimeUnit.SECONDS)){
System.err.println("线程池任务未正常执行结束");
}
}
}catch(InterruptedException ex){
//重新调用shutdownNow
poolExecutor.shutdownNow();
}
七、线程池参数调优
参数如何设置跟系统的负载有直接的关系,假设下面的参数表示目前的系统负载。
tasks:每秒需要处理的最大任务数量
tasktime:处理一个任务所需要的时间
responsetime:系统允许任务最大的响应时间,比如每个任务的响应时间不得超过2秒
1、corePoolSize
每个任务需要tasktime秒处理,则每个线程每秒可以处理1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime),即tasks*tasktime个线程。假设系统每秒任务数范围为100至1000,每个任务耗时0.1秒,则需要的线程数为100*0.1至1000*0.1,即10至100。那么corePoolSize应该设置为大于10,corePoolSize可设置为20。
2、workQueue
任务队列的长度与核心线程数以及系统对任务响应时间的要求有关。队列长度可设置为(corePoolSize/tasktime)*responsetime,如(20/0.1)*2=400,即队列长度可设置为400。
注意:队列长度设置过大,会导致任务响应时间过长,切忌使用new LinkedBlockingQueue(),队列LinkedBlockingQueue将队列长度设置为Integer.MAX_VALUE,将会导致线程数永远为corePoolSize,再也不会增加。当任务数量陡增时,任务响应时间也将随之陡增。
3、maximumPoolSize
当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒任务达到1000个任务时,则需要(1000 - workQueue)*(20/200),即60个线程,可将maximumPoolSize设置为60。
4、keepAliveTime
线程数量不能只增加不减少。当负载降低时,可减少线程数量,如果一个线程空闲时间达到keepAliveTime,该线程就该退出,默认情况下线程池最少会保持corePoolSize个线程(allowCoreThreadTimeout设置为false),keepAliveTime可设置为0。
5、allowCoreThreadTimeout
默认情况下,核心线程不会退出。可将allowCoreThreadTimeout设置为true,让核心线程也退出。
以上关于线程数量的计算并没有考虑CPU的情况。若结合CPU的情况,比如,当线程数量达到50时,CPU达到100%,则将maxPoolSize设置为60也不合适,此时若系统负载长时间维持在每秒1000个任务,则超出线程池处理能力,应设法降低每个任务的处理时间(tasktime)。
Java线程池基础的更多相关文章
- Java线程和多线程(十二)——线程池基础
Java 线程池管理多个工作线程,其中包含了一个队列,包含着所有等待被执行的任务.开发者可以通过使用ThreadPoolExecutor来在Java中创建线程池. 线程池是Java中多线程的一个重要概 ...
- Java线程池与java.util.concurrent
Java(Android)线程池 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行 ...
- Java线程池使用和分析(一)
线程池是可以控制线程创建.释放,并通过某种策略尝试复用线程去执行任务的一种管理框架,从而实现线程资源与任务之间的一种平衡. 以下分析基于 JDK1.7 以下是本文的目录大纲: 一.线程池架构 二.Th ...
- 07深入理解Java线程池
之前面试baba系时遇到一个相对简单的多线程编程题,即"3个线程循环输出ADC",自己答的并不是很好,深感内疚,决定更加仔细的学习<并发编程的艺术>一书,到达掌握的强度 ...
- java线程池技术(二): 核心ThreadPoolExecutor介绍
版权声明:本文出自汪磊的博客,转载请务必注明出处. Java线程池技术属于比较"古老"而又比较基础的技术了,本篇博客主要作用是个人技术梳理,没什么新玩意. 一.Java线程池技术的 ...
- Java 线程池原理分析
1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...
- 【java线程系列】java线程系列之java线程池详解
一线程池的概念及为何需要线程池: 我们知道当我们自己创建一个线程时如果该线程执行完任务后就进入死亡状态,这样如果我们需要在次使用一个线程时得重新创建一个线程,但是线程的创建是要付出一定的代价的,如果在 ...
- Java线程池源码解析
线程池 假如没有线程池,当存在较多的并发任务的时候,每执行一次任务,系统就要创建一个线程,任务完成后进行销毁,一旦并发任务过多,频繁的创建和销毁线程将会大大降低系统的效率.线程池能够对线程进行统一的分 ...
- 并发编程(十二)—— Java 线程池 实现原理与源码深度解析 之 submit 方法 (二)
在上一篇<并发编程(十一)—— Java 线程池 实现原理与源码深度解析(一)>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法.这篇文章是接着上一篇文章 ...
随机推荐
- 盘一盘 NIO (三)—— Selector解析
Selector是个啥? Selector是Java NIO核心组件中的选择器,用于检查一个或多个Channel(通道)的状态是否处于可读.可写.实现一个单独的线程可以管理多个channel,从而管理 ...
- FIS 插件机制
FIS 插件机制 author: @TiffanysBear 当我们使用 FIS 插件的时候,有没有想过自己也开发一个基于 FIS 的插件,参与 FIS 打包编译的整个流程:那么问题就来了: FIS ...
- 10.源码分析---SOFARPC内置链路追踪SOFATRACER是怎么做的?
SOFARPC源码解析系列: 1. 源码分析---SOFARPC可扩展的机制SPI 2. 源码分析---SOFARPC客户端服务引用 3. 源码分析---SOFARPC客户端服务调用 4. 源码分析- ...
- 驰骋工作流引擎与jFinal集成版本2.0
驰骋工作流引擎与jFinal集成版本2.0 发布说明 关键字: 驰骋工作流程快速开发平台 工作流程管理系统java工作流引擎. 使用协议:GPL. 关于JFinal: https://www.jfin ...
- HTTP首部字段完全解析
http协议是前端开发人员最常接触到的网络协议.在开发过程中,尤其是调试过程中避免不了需要去分析http请求的详细信息.在这其中头部字段提供的信息最多,比如通过响应状态码我们可以直观的看到响应的大致状 ...
- [python]创建文本文件,并读取
代码如下: # coding=gbk import os fname = raw_input("Please input the file name: ") print if os ...
- hdu-6638 Snowy Smile
题目链接 Snowy Smile Problem Description There are n pirate chests buried in Byteland, labeled by 1,2,-, ...
- POJ 3207 Ikki's Story IV - Panda's Trick 2-sat模板题
题意: 平面上,一个圆,圆的边上按顺时针放着n个点.现在要连m条边,比如a,b,那么a到b可以从圆的内部连接,也可以从圆的外部连接.给你的信息中,每个点最多只会连接的一条边.问能不能连接这m条边,使这 ...
- Go语言os标准库常用方法
1. os.Getwd()函数 原型:func Getwd()(pwd string, err error) 作用:获取当前文件路径 返回:当前文件路径的字符串和一个err信息 示例: package ...
- 【Offer】[66] 【构建乘积数组】
题目描述 思路分析 测试用例 Java代码 代码链接 题目描述 给定一个数组A[0, 1, -, n-1],请构建一个数组B[0, 1, -, n-1],其中B中的元素B[i] =A[0]×A[1]× ...