详解线程池
引言:在最早的时候有写过关于线程池的文章,针对四种不同的线程池,讲解了他们的特性,本文将更加深入,从源码来解释四种不同的线程池框架。
一、线程池的作用以及好处
在JDK1.5以前,线程池的使用是十分简陋的,直到JDK1.5,在并发包下,才出现了现如今的线程池。
线程池的作用是控制线程的数量,因为每一个线程都会占用系统的内存,以及在运行时,会争夺CPU的使用权,线程多了,系统很容易挂掉,线程少了,系统的资源就没办法得到完全的利用,这时,能够根据系统的状况定制线程的数量就显得十分的有效。控制了线程的数量,如果有新的任务提交,线程都在忙碌,线程池则会将任务加入到一个队列中进行等待,这时线程池最初的设计。
线程池的好处就显而易见了,线程池可以避免频繁地新建和销毁线程,让线程重复利用,执行多个任务,同时定制线程数量。
二、简述四种线程池的特性
线程池都是实现了ExecutorService接口的,通过Executors类中的静态工厂方法来构建,JDK提供有如下的四种静态工厂方法,配置了四种类型的线程池(推荐使用):
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
第一种线程池fixedThreadPool,提供了一个定长的线程池,可以加整形参数,当线程池中加入任务后,若有空闲的线程则用该线程跑任务,否则进入等待队列。
第二种线程是cachedThreadPool,缓冲线程池,不定长,一旦有任务进来不存在可利用的空闲线程,便会为任务新建线程,空闲线程超过60s会被回收。
第三种线程池singleThreadExecutor,单线程池,只有一个线程,如果线程因为异常结束,会有新的线程来替代,仅用这一个线程来处理任务队列。
第四种线程是scheduledThreadPool,同样也是定长的线程池,但是可以执行定时的任务,以及周期性的任务。
前三种线程池方法的使用都比较雷同,都是通过实例化线程池的execute(Thread thread)加入线程类,最后调用shutdown方法平滑关闭线程池(等线程池中线程的任务完全结束,才会关闭)。第四种线程池代码如下:
public class TestScheduledThreadPoolExecutor { public static void main(String[] args) { ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1); exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间就触发异常 @Override publicvoid run() { //throw new RuntimeException(); System.out.println("================"); } }, 1000, 5000, TimeUnit.MILLISECONDS); exec.scheduleAtFixedRate(new Runnable() {//每隔一段时间打印系统时间,证明两者是互不影响的 @Override publicvoid run() { System.out.println(System.nanoTime()); } }, 1000, 2000, TimeUnit.MILLISECONDS); } }
三、解释线程池的源码
线程池的构造,点击进入工厂类方法,可以发现Executor类底层实现了一个方法ThreadPoolExecutor。该方法的完整签名为:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
一共有着以上参数,第一个corePoolSize,顾名思义,为核心线程数,默认情况下会一直存在,即使空闲,不受keepAliveTime的约束;
第二个则为最大的线程数,当核心线程都在忙碌,就会创建多余的线程,但是最大不能超过线程池最大数量;第三个为线程所存活的时间keepAliveTime;
第四个为时间单位;
第五个为一个阻塞队列的接口,用于存放加入的任务,workQueue;
后两个为执行线程创建所调用的工厂,和当超出线程池数量和任务队列容量阻塞时执行的方法,称之为拒绝策略。
(当使用有界队列时,若是新进了任务,但是核心线程全都在运作,则加入任务队列中,若任务队列也满了,则在没有超出线程池容量的前提下新建线程,若是超过最大线程数,执行该方法)
(无界队列就不会那么尴尬,理论上他是永远不会执行handler方法的,因为当新进了一个任务,若核心线程在忙,就会进入队列中等待,不会再去新建线程,所以线程池线程最大数没有什么意义了此时)
讲究的就一个策略:先核心,再队列,再新建,新建到最大就炸给你看。
拓展:JDK提供了四种拒绝策略,若是想要自定义拒绝策略,必须实现一个reject**的接口,然后重新其中的一个方法,该方法传入了线程池对象和当前任务对象。
让我们来看看不同线程池的工厂类源码:
1、fixedThreadPool:
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
可以看出核心线程数和最大线程数是一样的,存活时间为0无效,使用了LinkedBlockingQueue无界阻塞队列来实现。
2、singleThreadExecutor:
1. public static ExecutorService newSingleThreadExecutor() { 2. return new FinalizableDelegatedExecutorService 3. (new ThreadPoolExecutor(1, 1, 4. 0L, TimeUnit.MILLISECONDS, 5. new LinkedBlockingQueue<Runnable>())); 6. }
单线程线程池,核心线程数和最大线程池数量为1,存活时间也为0,同样使用了无界队列。
3、cachedThreadPool:
1. public static ExecutorService newCachedThreadPool() { 2. return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 3. 60L, TimeUnit.SECONDS, 4. new SynchronousQueue<Runnable>()); }
缓存线程池的实现就十分有意思了,他没有核心线程数量,同时线程池的上限也可视为无,非核心线程存活时间为60S,使用了一个无缓存队列来实现阻塞队列,意味着不缓存任何任务,所有任务即来即用。(重点是没有核心线程)
4、scheduledThreadPool:
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());定时线程池定义了核心线程的数量,没有对线程池的数量设置上限,同时也没有让非核心线程有存活时间,最有意思的是他使用了一个定时队列来实现任务队列,完成定时的作用。