万字线程池学习总结!!!

本文目录

目录

为什么要使用线程池?

为什么要使用线程池?

线程池好处

线程池参数详解

介绍线程池各个参数含义

线程池处理任务流程图:

corePoolSize和maximumPoolSize:

KeepAliveTime+时间单位:

ThreadFactory:

workQueue:

handler:

六种常见的线程池

 FixedThreadPool

CachedThreadPool

ScheduledThreadPool

SingleThreadExecutor

SingleThreadScheduleExecutor

ForkJoinPool

为什么不能直接自动创建线程池

FixedThreadPool

SingleThreadExecutor

CachedThreadPool

ScheduledThreadPool和SingleThreadScheduledExector

 SingleThreadExecutor

如何自定义合适的线程池

CPU密集型任务

耗时IO任务

如何正确关闭线程池

void shutdown()

boolean isShutdown()

boolean isTerminated()

boolean awaitTermination(long timeout,TimeUnit unit),throws IntereuptedException

List shutdownNow()

线程池复用原理

线程池复用原理

总结


为什么要使用线程池?

为什么要使用线程池?

  • 反复创建线程系统开销比较大,而且每个线程的创建和销毁都需要时间,如果任务比较简单,那么有可能导致线程的创建和销毁占用的资源超过执行任务所消耗的资源.

  • 如果当要执行的任务比较多时,每个线程负责一个任务,那么需要创建很多线程去执行任务,过多的线程会占用过多的内存资源等,还会带来上下文切换,同时还会导致系统不稳定.

线程池好处

  • 线程池解决了线程生命周期的系统开销问题,线程池中的线程可以反复使用,可以用少量的线程去执行大量的任务,减少了线程创建和销毁的开销,而且线程都是创建好的,来任务就可以执行.

  • 通过设置合适的线程池的线程数,可以避免资源使用不当,线程池可以通过线程数和任务灵活的控制线程数量,任务多的时候可以继续创建线程,任务少的时候只保留核心线程,这样可以避免系统资源浪费和线程过多导致内存溢出.

  • 线程池可以统一管理资源,通过线程书和任务队列,可以统一开始和结束,并设置相关的拒绝策略.

 

线程池参数详解

介绍线程池各个参数含义

  • corePoolSize:核心线程数,常驻线程池的线程数量

  • maxPoolSize:线程池最大线程数量,当任务特别多的时候,corePoolSize线程数量无法满足需求的时候,就会继续创建线程,最大不超过maxPoolSize.

  • KeepAliveTime+时间单位:空闲线程的存活时间

  • ThreadFactory:线程工厂,用来创建线程

  • WorkQueue:任务队列,用来存放任务

  • Handler:处理被拒绝的策略

线程池处理任务流程图:

 

如上图所示,流程如下:

  1. 当提交任务后,,线程池首先会检查当前线程数,如果当前线程数小于核心线程数,则新建线程并执行任务.

  2. 随着任务不断增加,线程数达到了核心线程数的数量,此时任务依然在增加,那么新来的任务将会放到workQueue等待队列中,等核心线程执行完任务后重新从队列中提取出等待被执行的任务

  3. 如果已经达到了核心线程数,且任务队列也满了,则线程池就会继续创建线程来执行任务,如果任务不断提交,线程池会持续创建线程直到达到maximumPoolSize最大线程数,当达到了最大线程数后,任务仍不断提交,那么此时就超过了线程池的最大处理能力,这个时候线程池就会拒绝处理这些任务,处理策略就是handler.

corePoolSize和maximumPoolSize:

从上面的流程中可以看出,线程池初始化时,默认的线程数是0,当有任务提交后,开始创建核心线程去执行任务,当线程数达到核心线程数时且任务队列满了后,开始创建非核心线程执行任务,最大可以达到maximumPoolSize,如果这是任务不提交了,线程开始空闲,那么默认情况下大于corePoolSize的线程在超过设置的KeepAliveTime时间后会被合理的收回,所以默认情况下,线程池中的线程数量处于corePoolSize和maximumPoolSize之间.

KeepAliveTime+时间单位:

默认情况下,当线程池中的数量多于核心线程数时,而此时有没有任务可做,那么线程池就会检测线程的KeepAliveTime,如果超过了规定的时间,则无事可做的线程就会被销毁,以便减少内存的占用和资源消耗,如果后期任务又多了起来,则线程池根据规则重新创建线程,通过这个可伸缩的功能,可以实现对资源的合理使用,我们可以通过setKeepAliveTime设置keepAliveTime时间,还可以通过设置allowCoreThreadTimeOut参数,这个参数默认是false,如果设置成ture,则会给核心线程数设置超时等待时间, 如果超过时间了核心线程就会销毁.

ThreadFactory:

ThreadFactory是一个线程工厂,负责生产线程去执行任务,默认的线程工厂,创建的线程会在同一个线程组,并且拥有一样的优先级,且都不是守护线程,我们也可自定义线程工厂,以便给线程自定义名字.

workQueue:

阻塞队列,用来存放任务,我们主要分析一下5种阻塞队列:

  1. ArrayBlockingQueue是基于数组的有界阻塞队列,按照FIFO排序,新来的队列会放到队列尾部,有界的数组可以防止资源被耗尽问题,当线程达到了核心线程数,再来任务的时候就放到队列的尾部,当队列满了的时候,则继续创建非核心线程,如果线程数量达到了maxPoolSize,则会执行拒绝策略.

  2. LinkedBlockingQueue是基于链表的无界阻塞队列(最大容量是Integer.MAX),按照FIFO排序,当线程池中线程数量达到核心线程数时,继续来了新任务会一直存放到队列中,而不会创建新线程.因此使用此队列时,maxPoolSize是不起做的

  3. SynchronousQueue是一个不缓存任务的阻塞队列,当来了新任务的时候,不会缓存到队列中,而是直接被线程执行该任务,如果没有核心线程可用就创建新线程去执行任务,达到了maxPoolSize时,就执行拒绝策略.

  4. PriorityBlockingQueue是一个具有优先级的无界阻塞队列,优先级通过参数Comparator实现

  5. DelayedWorkQueu队列的特点是内部的任务并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”数据结构.而且它也是一个无界队列.

handler:

拒绝策略是当线程池中任务达到了队列最大容量,且线程数量也达到了最大maxPoolSize的时候,如果继续有新任务来了,则执行这个拒绝策略来处理新来的任务,jdk提供4种拒绝策略,它们都实现了RejectedExecutionHandler接口:

  1. CallRunsPolicy:该策略下,在调用者线程中直接执行被拒绝任务的run方法,就是谁提交的任务,谁负责执行任务,这样任务不会丢失,而且执行任务比较费时,那么提交任务的线程也会被占用,就可以减缓任务提交速度.

  2. AbortPolicy:该策略下,直接抛弃任务,并抛RejectedExecutionException异常.

  3. DiscardPolicy:该策略下,直接抛弃任务.

  4. DiscardOldestPolicy:该策略下,抛弃最早进入队列中的那个任务,然后尝试把这次拒绝的任务放入队列.

除此之外,我们还可以通过实现 RejectedExecutionHandler 接口来实现自己的拒绝策略,在接口中我们需要实现rejectedExecution方法,在rejectedExecution方法中,执行例如暂存任务、重新执行等自定义拒绝策略.

六种常见的线程池

 FixedThreadPool

这个线程池的核心线程数和最大线程数是一样的,所以可以看作是固定线程数的线程池,特点是当线程达到核心线程数后,如果任务队列满了,也不会创建额外的非核心线程去执行任务,而是执行拒绝策略.

CachedThreadPool

这个线程池叫做缓存线程池,特点是线程数几乎是可以无限增加的(最大值是Integer.MAX_VALUE,基本不会达到),当线程闲置时还可以进行回收,而且它采用的存储任务的队列是SynchronousQueue队列,队列容量是0,实际不存储任务,只负责对任务的中转和传递,所以来一个任务线程池就看是否有空闲的线程,有的话就用空闲的线程去执行任务,否则就创建一个线程去执行,效率比较高.

ScheduledThreadPool

通过这个线程池的名字可以看出,它支持定时或者周期性执行任务,实现这种功能的方法主要有三种:

ScheduledExecutorService service = Executors.newScheduledThreadPool(10);service.schedule(new Task(), 10, TimeUnit.SECONDS);service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);

第一种是schedule,通过延迟指定时间后执行一次任务,代码中设置的是10秒,所以10秒后执行一次任务就结束.

第二种是scheduleAtFixedRate,通过名称我们可以看出,第二种是以固定频率去执行任务,它的第二个参数initialDelay表示第一次延迟时间,第三个参数period表示周期,总体按照上面的代码意思就是,第一次延迟10秒后执行任务,然后,每次延迟10秒执行一次任务.

第三种方法是scheduleWithFixeddelay这种与第二种方法类似,也是周期执行任务,不同的是对周期的定义,之前的scheduleAtFixedRate是以任务的开始时间为起点开始计时,时间到了就开始执行第二次任务,而不管任务需要多久执行,而scheduleWithFixeddelay是以任务结束的时间作为下一次循环开始的时间起点.

SingleThreadExecutor

第四种线程池中只有一个线程去执行任务,如果执行任务过程中发生了异常,则线程池会创建一个新线程来执行后续任务,这个线程因为只有一个线程,所以可以保证任务执行的有序性.

SingleThreadScheduleExecutor

这个线程池它和ScheduledThreadPool很相似,只不过它的内部也只有一个线程,他只是将核心线程数设置为了1,如果执行期间发生异常,同样会创建一个新线程去执行任务.

ForkJoinPool

最后一种线程池是ForkJoinPool,这个线程池是来支持将一个任务拆分成多个“小任务”并行计算,这个线程池是在jdk1.7之后加入的,它主要用于实现“分而治之”的算法,特别是分治之后递归调用的函数,这里只是对ForkJoinPool做了一个简单的介绍,我们先来介绍一下ForkJoinPool和之前的线程池主要的两个特点。

第一点是fork和join:

我们现来看看fork和join的含义,fork就是将任务分解成多个子任务,多个子任务互相独立,不受影响,执行的时候可以利用 CPU 的多核优势,并行计算,计算完成后各个子任务在调用join方法进行结果汇总,第一步是拆分也就是 Fork,第二步是汇总也就是 Join,我们通过下图来理解:

我们通过举例斐波那契数列来展示这个线程池的使用。

1.首先我们创建任务类FibonacciTask继承RecursiveTask类,重写compute方法。其中的ForkJoinTask代表一个可以并行、合并的任务,ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask。其中RecusiveTask代表有返回值的任务,而RecusiveAction代表没有返回值的任务,

2.我们在compute方法中实现斐波那契数列计算并获取返回值。

3.在main方法中创建ForkJoinPool,并调用线程池的submit(ForkJoinTask<T>task)方法,通过获取返回的task.get()方法获取计算的返回值。

任务类:FibonacciTask

/** * 这里我们的定义任务类继承RecursiveTask,需要重写一个compute方法,或者任务执行的返回值 * RecursiveAction和RecursiveTask是ForkJoinTask的两个抽象子类, * 其中的ForkJoinTask,代表一个可以并行、合并的任务其中RecursiveAction * 表示没有返回值的任务,RecursiveTask是有返回值的任务 */public class FibonacciTask extends RecursiveTask<Integer> {    private int i;    FibonacciTask(int i){        this.i=i;    }    @Override        protected Integer compute() {        if(i<=1){            return i;        }        FibonacciTask f1=new FibonacciTask(i-1);        //用 fork() 方法分裂任务并分别执行        f1.fork();        FibonacciTask f2=new FibonacciTask(i-2);        f2.fork();        //使用 join() 方法把结果汇总        return f1.join()+f2.join();    }}

main方法:

 public static void main(String[] args) {        ForkJoinPool forkJoinPool=new ForkJoinPool();        for(int i=0;i<10;i++){            ForkJoinTask<Integer> task = forkJoinPool.submit(new FibonacciTask(i));            try {                System.out.println(task.get());            } catch (InterruptedException e) {                e.printStackTrace();            } catch (ExecutionException e) {                e.printStackTrace();            }        }    }

计算结果如下:

 

第二点是内部结构不同:

之前的线程池所有的线程共用一个队列,但 ForkJoinPool 线程池中每个线程都有自己独立的任务队列,这个队列是双端队列,如图下所示:

ForkJoinPool 线程池内部除了有一个共用的任务队列之外,每个线程还有一个对应的双端队列 deque,这时一旦线程中的任务被 Fork 分裂了,分裂出来的子任务放入线程自己的 deque 里,而不是放入公共的任务队列中(公共任务队列采用数组存放),如果此时有三个子任务放入线程 t1 的 deque 队列中,对于线程 t1 而言获取任务的成本就降低了,可以直接在自己的任务队列中获取而不必去公共队列中争抢也不会发生阻塞(除了后面会讲到的 steal 情况外),减少了线程间的竞争和切换,是非常高效的。

我们再考虑一种情况,此时线程有多个,而线程 t1 的任务特别繁重,分裂了数十个子任务,但是 t0 此时却无事可做,它自己的 deque 队列为空,这时为了提高效率,t0 就会想办法帮助 t1 执行任务,这就是“work-stealing”的含义。

双端队列 deque 中,线程 t1 获取任务的逻辑是后进先出,也就是LIFO(Last In Frist Out),而线程 t0 在“steal”偷线程 t1 的 deque 中的任务的逻辑是先进先出,也就是FIFO(Fast In Frist Out),如图所示,图中很好的描述了两个线程使用双端队列分别获取任务的情景。你可以看到,使用 “work-stealing” 算法和双端队列很好地平衡了各线程的负载。

 

 最后,我们用一张全景图来描述 ForkJoinPool 线程池的内部结构,你可以看到 ForkJoinPool 线程池和其他线程池很多地方都是一样的,但重点区别在于它每个线程都有一个自己的双端队列来存储分裂出来的子任务。ForkJoinPool 非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。

 

 

为什么不能直接自动创建线程池

首先自动创建线程池通过直接调用Executors.newCachedThreadPool()方法直接创建线程池.但是开发中我们不能直接使用创建的线程池,原因如下:    

FixedThreadPool

通过下面FiexdThreadPool内部代码可以看出,FixedThreadPool内部调用的是ThreadPoolExecutor的构造函数,构造函数中是的的阻塞队列是LinkedBlockingQueue,那么这就带来了问题,当任务处理速度比较慢的时候,虽然新增任务越来越多,队列中堆积的任务就越来越多,最终会占用大量内存,并发生OOM,就会严重影响到程序运行.

public static ExecutorService newFixedThreadPool(int nThreads) {     return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}

SingleThreadExecutor

通过看下面SingleThreadExecutor的内部代码可以发现,newSingleThreadExecutor和newFixedThreadPool的原理是一样的,只不过是核心线程数和最大线程数都设置成了1,但是任务队列还是无界的LinkedBlockingQueue,所以也会导致任务堆积,发生OOM问题.

public static ExecutorService newSingleThreadExecutor() {     return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));}

CachedThreadPool

继续看下图CachedThreadPool内部代码,从代码中可以看出,CachedThreadPool使用的任务阻塞队列是SynchronousQueue,SynchronousQueue队列我们前面介绍过,并不存储任务,只是对任务进行直接转发,这个队列不会引发OOM问题,但是我们在看最大线程数设置成了Integer.MAX_VALUE,所以CachedThreadPool线程池并不线程的数量,那么任务特别多的时候,就会创建非常多的线程,进而导致系统内存不足.

public static ExecutorService newCachedThreadPool() {     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}

ScheduledThreadPool和SingleThreadScheduledExector

ScheduledThreadPool和SingleThreadScheduledExector差不多,只不过是后者线程池中只有一个线程,ScheduledThreadPool的源码如下:

  public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {        return new ScheduledThreadPoolExecutor(corePoolSize);    }

我们在进入ScheduledThreadPoolExecutor构造方法中去,从下图可以看出,它采用的任务队列是DelayWorkQueue,上面我们说过这个队列一个延迟队列同时也是一个无界队列,所以它和LinkedBlockingQueue一样,如果任务过多就可能OOM,代码如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {     super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());}

 SingleThreadExecutor

第四种线程池中只有一个线程去执行任务,如果执行任务过程中发生了异常,则线程池会创建一个新线程来执行后续任务,这个线程因为只有一个线程,所以可以保证任务执行的有序性.

 

 

如何自定义合适的线程池

这个也是面试中会被问到的问题,如果自定义合适的线程池呢?首先我们要调整线程池中的线程数量以便充分并合理的使用CPU和内存资源,从而最大限度的提高性能.

CPU密集型任务

如果任务是一些列比较消耗CPU资源的任务,比如加密、解密、压缩、计算等,那么最佳线程数是CPU核心数的1~2倍,过多很导致占用大量CPU资源,这时每个CPU的核心工作基本都是满负荷,设置过多的线程会造成不必要的上下文切换,而导致性能下降,而且在同一台机器上,我们还要考虑到其他会占用较多CPU资源的程序运行,然后做整体平衡.

耗时IO任务

例如数据库、文件的读写,网络通信等任务,这种任务的特点是不会消耗很多CPU资源,但是IO操作很费时.这个时候可以设置最大线程数一般会大于CPU核心线程数很多倍,因为IO速度相比于CPU速度比较慢,我们设置较少的线程数,就会浪费CPU资源,如果设置更多的线程数,那么一部分线程正在等待IO的时候,他们此时不需要CPU计算,就能有更多线程去执行IO操作,任务队列中的等待任务就会减少,更合理的利用了资源.

java并发编程实战中有推荐:线程数 = CPU核心数 *(1+平均等待时间/平均工作时间),我们可以通过这个式子计算出一个合理的线程数量,同时也可以根据进行压测、监控jvm的线程情况等方式,确定线程数,更合理的利用资源.

总结以上特点可以得出以下几点:

线程的平均工作时间所占比例越高,就需要越少线程.

线程的平均等待时间所占比例越高,就需要越多的线程

针对不同的程序,进行对应的实际测试就可以获得更合适的选择.

 

 

如何正确关闭线程池

首先有5种在ThreadPoolExecutor中涉及的关闭线程的方法我们挨个来分析。

void shutdown()

它可以安全的关闭一个线程池,调用shutdown()方法后,线程池不会立刻关闭,而是等执行完正在执行的任务和队列中等待的任务后才彻底关闭,而且调用shutdown()方法后,如果还有新的任务继续到来,那么线程池会根据拒绝策略直接拒绝后面来的新任务.

boolean isShutdown()

这个方法可以返回ture或者false来判断是否已经开始了关闭工作,也就是是否执行了shutdown或者shutdownNow方法,调用isShutdown()方法后如果返回true,并不代表线程池已经彻底关闭了,仅仅代表开始了关闭流程,仍然可能有线程正在执行任务,队列里也可能有任务等待被执行.

boolean isTerminated()

这个方法可以检测是否真正关闭了,不仅代表线程池是否已经关闭,同时也代表线程池中的所有任务是否已经都执行完毕,比如已经调用了shutdown()方法,但是有一个线程正在执行任务,则此时调用isShutdown方法返回true,而调用isTerminated方法便返回false,,因为线程池中还有任务再执行,线程池没有真正关闭,直到所有线程都执行完毕,任务都执行完毕,再调用isTermainted就返回ture.

boolean awaitTermination(long timeout,TimeUnit unit),throws IntereuptedException

       awaitTermination并不是用来关闭线程池的,而是用来判断线程池状态的,参数需要传入一个时间,如果我们设置10秒钟,那么会有以下几种情况:

  1. 等待期间,线程池已经关闭且所有提交的任务都执行完毕,那么方法就返回ture,相当于线程池真正关闭了.

  2. 等待时间超时后,第一种情况未发生,那么方法返回false.

  3. 等待时间中,执行任务的线程被中断了,方法会抛出InterruptedException异常.

       所以综上可以看出,调用 awaitTermination 方法后当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回 true,否则超时返回 fasle,我们则可以根据 awaitTermination() 返回的布尔值来判断下一步应该执行的操作。

List<Runnable> shutdownNow()

调用shutdownNow()方法后,首先会给所有线程池中的线程发送interrupt中断信号,尝试中断这些任务的执行,然后就任务队列中在等待被执行的任务转移到一个List中并返回,我们可以再根据List做一些操作,shutdownNow() 的源码如下所示:

public List<Runnable> shutdownNow() {        List<Runnable> tasks;        final ReentrantLock mainLock = this.mainLock;        mainLock.lock();        try {            checkShutdownAccess();            //转换线程运行状态            advanceRunState(STOP);            //让每一个已经启动的线程都中断,如果被中断的线程对于中断信号不理不睬            //那么依然有可能导致任务不会停止            interruptWorkers();            //将队列中任务放入tasks集合中,并返回.            tasks = drainQueue();        } finally {            mainLock.unlock();        }        tryTerminate();        return tasks;    }

线程池复用原理

线程池复用原理

线程池可以把线程和任务进行解耦,线程归线程,任务归任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从 BlockingQueue 中不断提取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run 方法,把 run 方法当作和普通方法一样的地位去调用,相当于把每个任务的 run() 方法串联了起来,所以线程数量并不增加。其中execute代码如下:

public void execute(Runnable command) {     //如果传入的Runnable的空,就抛出异常    if (command == null)         throw new NullPointerException();    int c = ctl.get();    /**    * 当前线程数是否小于核心线程数,如果小于核心线程数就调用 addWorker()     * 方法增加一个 Worker,这里的 Worker 就可以理解为一个线程    */    if (workerCountOf(c) < corePoolSize) {         if (addWorker(command, true))             return;        c = ctl.get();    }     if (isRunning(c) && workQueue.offer(command)) {         int recheck = ctl.get();        if (! isRunning(recheck) && remove(command))             reject(command);        else if (workerCountOf(recheck) == 0)             addWorker(null, false);    }     else if (!addWorker(command, false))         reject(command);}

我们先来分析上面部分代码,我们先分析下面这一段代码:

if (workerCountOf(c) < corePoolSize) {         if (addWorker(command, true))             return;        c = ctl.get();    }

我们主要看addWorK(comond,tue)方法,addWorker 方法的主要作用是在线程池中创建一个线程并执行第一个参数传入的任务,它的第二个参数是个布尔值,如果布尔值传入 true 代表增加线程时判断当前线程是否少于 corePoolSize,小于则增加新线程,大于等于则不增加;同理,如果传入 false 代表增加线程时判断当前线程是否少于 maxPoolSize,小于则增加新线程,大于等于则不增加,所以这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增线程的判断。addWorker() 方法如果返回 true 代表添加成功,如果返回 false 代表添加失败。

接下来我们看下面这部分代码

 if (isRunning(c) && workQueue.offer(command)) {            int recheck = ctl.get();            if (! isRunning(recheck) && remove(command))            //执行拒绝策略                reject(command);            else if (workerCountOf(recheck) == 0)                addWorker(null, false);        }

如果代码执行到这里,说明当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过 if (isRunning(c) && workQueue.offer(command)) 检查线程池状态是否为 Running,如果线程池状态是 Running 就把任务放入任务队列中,也就是 workQueue.offer(command)。如果线程池已经不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略。

接下来我们上面这部分代码的else分支逻辑:

else if (workerCountOf(recheck) == 0)                addWorker(null, false);

能进入这个 else 说明前面判断到线程池状态为 Running,那么当任务被添加进来之后就需要防止没有可执行线程的情况发生(比如之前的线程被回收了或意外终止了),所以此时如果检查当前线程数为 0,也就是 workerCountOf**(recheck) == 0,那就执行 addWorker() 方法新建线程。

接着我们再看最后一部分代码:

else if (!addWorker(command, false))     reject(command);

执行到这里,说明线程池不是 Running 状态或线程数大于或等于核心线程数并且任务队列已经满了,所以此时需要添加新线程,直到线程数达到“最大线程数”,所以此时就会再次调用 addWorker 方法并将第二个参数传入 false,传入 false 代表增加线程时判断当前线程数是否少于 maxPoolSize,小于则增加新线程,大于等于则不增加,也就是以 maxPoolSize 为上限创建新的 worker;addWorker 方法如果返回 true 代表添加成功,如果返回 false 代表任务添加失败,说明当前线程数已经达到 maxPoolSize,然后执行拒绝策略 reject 方法。如果执行到这里线程池的状态不是 Running,那么 addWorker 会失败并返回 false,所以也会执行拒绝策略 reject 方法。

所以看到这里我们需要着重分析addWorker()方法,这里的 Worker 可以理解为是对 Thread 的包装,Worker 内部有一个 Thread 对象,它正是最终真正执行任务的线程,所以一个 Worker 就对应线程池中的一个线程,addWorker 就代表增加线程。我们看部分addWorker内的方法:

boolean workerStarted = false;        boolean workerAdded = false;        //worker是内部类实现了接口Runnable,封装了Thread        Worker w = null;        try {            //获取队列第一个任务            w = new Worker(firstTask);            final Thread t = w.thread;            if (t != null) {                final ReentrantLock mainLock = this.mainLock;                mainLock.lock();                try {                    int rs = runStateOf(ctl.get());                    if (rs < SHUTDOWN ||                        (rs == SHUTDOWN && firstTask == null)) {                        if (t.isAlive()) // precheck that t is startable                            throw new IllegalThreadStateException();                        //集合,包含池中的所有工作线程。只有当持有主锁时才能访问。                        workers.add(w);                        int s = workers.size();                        if (s > largestPoolSize)                            largestPoolSize = s;                        workerAdded = true;                    }                } finally {                    mainLock.unlock();                }                if (workerAdded) {                //调用线程的start方法。                    t.start();                    workerStarted = true;                }            }        } finally {            if (! workerStarted)                addWorkerFailed(w);        }

通过上图中的注释我们可以看出,addWork方法实际上是调用自己封装的线程的start方法来启动线程,我们继续看worker内部类的run方法是如何实现的:

public void run() {            runWorker(this);        }final void runWorker(Worker w) {        Thread wt = Thread.currentThread();        //获取第一个要执行的任务,先进先出        Runnable task = w.firstTask;        w.firstTask = null;        w.unlock(); // allow interrupts        boolean completedAbruptly = true;        try {            //实现线程复用的逻辑主要在一个不停循环的 while 循环体中            while (task != null || (task = getTask()) != null) {                w.lock();                if ((runStateAtLeast(ctl.get(), STOP) ||                     (Thread.interrupted() &&                      runStateAtLeast(ctl.get(), STOP))) &&                    !wt.isInterrupted())                    wt.interrupt();                try {                    beforeExecute(wt, task);                    Throwable thrown = null;                    try {                    //直接调用task的run方法,而不是新建线程                        task.run();                    } catch (RuntimeException x) {                        thrown = x; throw x;                    } catch (Error x) {                        thrown = x; throw x;                    } catch (Throwable x) {                        thrown = x; throw new Error(x);                    } finally {                        afterExecute(task, thrown);                    }                } finally {                    task = null;                    w.completedTasks++;                    w.unlock();                }            }            completedAbruptly = false;        } finally {            processWorkerExit(w, completedAbruptly);        }    }

从上图中我们可以看到内部类worker的run()方法实际上是调用runWorker(this)方法,实现线程复用的逻辑主要是在一个不同的循环体while中进行,所以在runWorker(this)方法中主要做了两件事:

  1. 通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。

  2. 直接调用 task 的 run 方法来执行具体的任务(而不是新建线程,调用线程的start()方法)。

总结

好了,本篇文章主要分析了线程池的基本概念和核心原理,也是作者对线程池学习的各方面的总结,基本上看完本篇文章能应对很多线程池的相关面试以及日常开发需求,如果有什么不足或者错误的地方希望读者能给出建议!喜欢就关注我吧

 

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值