线程池学习笔记
自 JDK1.5 起,util 包提供了 ExecutorService 线程池的实现,主要目的是为了重复利用线程,提高系统效率。我们得知 Thread 是一个重量级的资源,创建、启动以及销毁都是比较耗费系统资源的,因此对线程的重复利用一种是非常好的程序设计习惯,加之系统中可创建的线程数量是有限的,线程数量和系统性能是一种抛物线的关系,也就是说当线程数量达到某个数值的时侯,性能反倒会降低很多,因此对线程的管理,尤其是数量的控制更能直接决定程序的性能。
线程池的核心线程数、最大线程数该如何设置?
1、CPU密集型任务
CPU核心数+1
额外加的一个线程,是备用,还是为了更好压栈CPU
2、IO密集型任务
线程数=CPU核心数 * (1+线程等待时间、线程运行时间)
执行的IO时间越长,就可以开更多的线程
最终还是要压测
3、对于核心业务(访问频率高),可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最大线程数
4、 对于非核心业务(访问频率不高),核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可以设置为我们计算或压测出来的结果。
阻塞队列的大小如何计算
要看每个任务平均执行多久,比如1s
100个核心线程,那么1s内能执行100个任务;如果队列大小为500,那么队列中的最后一个任务要等5s才能执行完,排队了4s。这时候要看是否可以接受任务执行耗时5s。
所以阻塞队列的大小要根据核心线程数、任务平均执行时间、可接受任务执行最大耗时决定的。
核心线程执行完任务后如何存货?
通过阻塞的方式存活。
线程池里面是非公平的
后来的任务被非核心线程执行了,先来的任务可能进入了阻塞队列
线程池执行任务的具体流程是怎样的?
Tomcat中的线程池
启动时就把核心线程创建出来了
线程池中线程数量等于最大线程数量才会入队;
有空闲线程,任务会入队
任务执行完了,非核心线程如何回收
getTask()方法返回null,然后执行processWorkerExit(w, completedAbruptly)方法
核心线程为何不会被回收
因为参数allowCoreThreadTimeOut默认为false,但是可以通过executor.allowCoreThreadTimeOut(true);将值设置为true,这样核心线程也会被回收了
任务发生了异常,处理任务的线程会不会被淘汰?
会被淘汰!
如果当前线程数等于核心线程数,怎么办?
会有一个替补线程。
为什么会抛出异常,感觉应该是为了拓展。抛出异常可以被线程工厂中的异常处理器(如果设置了)捕获,然后进行一些定制化的处理,如果不抛出异常的化,就无法被线程工厂中的异常处理器所捕获。
线程池的五种状态是如何流转的?
线程池有五种状态:
1、RUNNING:会接受新任务、会处理队列中的任务;
2、SHUTDOWN:不会接受新任务,但是会处理队列中的任务;
3、STOP:不会接受新任务、不会处理队列中的任务。并且会中断正在处理的任务(注:一个任务能不能被中断还是要看任务本身);
4、TIDYING:所有的任务都终止了,线程池也没有任务了,这样的线程池状态就会转换为TIDYING,一旦达到此状态,就会调用线程池的terminated();
5、TERMINATED:terminated()方法执行完后就会变为TERMINATED。
最后一个线程退出的时候会把线程池状态改为TIDYING,并执行terminated()方法,最后把线程池状态改为TERMINATED。
创建线程池
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
- corePoolSize:核心线程数大小,如果提交任务时,线程池核心线程数小于corePoolSize,则会创建新线程来执行任务,即使县城池中有空闲线程;如果达到了corePoolSize,则不创建新线程。
- maxmunPoolSize:线程池能创建的最大线程数。如果堵塞队列已满,并且当前线程数没有超过maxmiuPoolSize,就会创建新线程来执行任务;
- keepAliveTime 空闲线程存活时间,如果当前线程数已经超过corePoolSize,并且空闲时间超过了keepAliveTime,则将这些空闲线程销毁;
- unit 时间单位,为keepAliveTime指定时间单位
- workQueue 阻塞队列,用于保存任务的阻塞队列
- threadFactory 创建线程的工厂类,可以指定线程工厂为创建出的每个线程设置更有意义的线程名,如果出现问题时,方便排查;
- handler 拒绝策略。
Java也为我们提供了一些常用的线程池类供我们直接使用,可以通过如下几种方式创建一个线程池:
Executors.newCachedThreadPool()
Executors.newFixedThreadPool(int nThreads)
Executors.newSingleThreadExecutor()
- newCachedThreadPool() 允许创建的最大线程为Integer.MAX_VALUE,可能会创建大量线程,导致OOM;
- newFixedThreadPool(int nThreads)和newSingleThreadExecutor()堵塞队列长度最大都是Integer.MAX_VALUE可能会堵塞大量任务,导致OOM
故在阿里开发手册中个,明确提出不允许用这三个线程池。
【强制】线程池不允许使用Executors创建,建议通过ThreadPoolExecutor的方式创建,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
自定义线程池:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20,
2 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));//自定义线程池
demo示例:
public class ThreadPoolDemo {public static void main(String[] args) {//自定义线程ThreadPoolExecutor executorService= new ThreadPoolExecutor(10, 20,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));for (int i = 1; i <= 100; i++) {executorService.execute(new MyTask(i));}}
}class MyTask implements Runnable {int i = 0;public MyTask(int i) {this.i = i;}@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程处理第" + i + "个任务");try {Thread.sleep(2000L);//业务逻辑} catch (Exception e) {e.printStackTrace();}}
}
这个demo 10个核心线程,最多有20个线程,阻塞队列的长度为10,循环100次,向线程池提交了100个任务,执行结果如下。
通过打印结果知道:
1-10个任务最先执行,其次是21-30个任务,在此期间还抛出了一个异常(执行了拒绝策略),最后执行的是11-20个任务。
为什么11-20个任务是最后执行?
是因为阻塞队列中任务执行的优先级最低。
接下来看下线程池的几个核心方法:
execute
创建线程池后,通过调用该方法,即可将创建的任务交给线程池处理。
入队后还会再次检查下线程池的状态,如果不是RUNNING状态的话,就把刚才入队的任务移除,并执行拒绝策略。
入队成功后会检查线程池中线程个数,如果线程个数为0,则新建一个线程。
如果allowCoreThreadTimeOut=true,那么核心线程也会被淘汰。这里的任务是null,对应runWork()方法中的getTask(),让该线程去阻塞队列中取任务。
拒绝策略
ThreadPoolExecutor内部有实现4个拒绝策略:
(1)、CallerRunsPolicy,由调用execute方法提交任务的线程来执行这个任务;
(2)、AbortPolicy,抛出异常RejectedExecutionException拒绝提交任务;
(3)、DiscardPolicy,直接抛弃任务,不做任何处理;
(4)、DiscardOldestPolicy,去除任务队列中的第一个任务(最旧的),重新提交;
在项目中一般是自己实现拒绝策略
addWorker
1、根据线程池的状态和当前线程池中运行的线程数,决定能否创建新的线程
2、创建新的线程
runWorker
从这里我们可以看到执行任务的优先级
线程池的优先级包括执行提交优先级和执行优先级;队列中的执行优先级低
线程池优先从传入的worker对象上去拿要执行的任务,取到就执行该任务,没有取到则会调用getTask()方法从阻塞队列中获取任务。getTask方法负责从阻塞队列的对头获取任务。这些任务都是在执行execute方法时存入的。
获取要执行的任务 > 优先执行当前worker对象持有的任务,阻塞队列中的任务优先级最低。阻塞队列中的任务优先级最低是为了节省资源。
该方法中核心逻辑被放入到一个while循环中执行,每一次执行完task.run()方法后,都会将task置空,然后开始新一轮的循环(再次从阻塞队列中获取新的任务),这里体现了线程池线程复用的设计思想。