并发编程之线程池(阻塞队列,拒绝策略,线程池种类)

目录

1.使用线程池比手动创建线程好在哪里?

2.线程池各个参数的含义

3.学习线程池参数从创建线程线程池创建(从一个小例子说起)

4.线程池拒绝策略

5.线程池常用阻塞队列

ArrayBlockingQueue(基于数组的先进先出队列,有界)

LinkedBlockingQueue (基于链表的先进先出队列,无界)

SynchronousQueue(无缓冲的等待队列,无界)

DelayedWorkQueue

6.有哪6种常见的线程池?什么是java8的ForkJoinPool

FixedThreadPool

CachedThreadPool

ScheduledThreadPool

SingleThreadExecutor

SingleThreadScheduleExecutor

ForkJoinPool

7.为什么不应该自动创建线程池(00M)

8.合适的线程数量是多少?CPU核心数和线程数的关系?

9.如何正确关闭线程池

         shutdwon()

         isShutdown()

         isTerminated()

         awaitTermination()

         shutdownNow()


1.使用线程池比手动创建线程好在哪里?

 (1)线程池可以解决线程周期的系统开销问题,同时还可以加快响应速度。

 (2)线程池可以统筹内存和CPU的使用,避免资源使用不当。

 (3)线程池可以统一管理资源

2.线程池各个参数的含义

corePoolSize(核心线程数)

maxPoolSize(最大线程数)

keepAliveTime+时间单位(空闲线程的存活时间)

ThreadFactory(线程工厂,用来创建新线程)

workQueue(用于存放任务的队列)

Handler(处理被拒绝的任务)

各个参数的执行时机如下解释:

 

如图所示,当提交任务后,线程池首先会检查当前线程数,如果此时线程数小于核心线程数,比如最开始线程数量为0,则新建线程并执行任务,随着任务的不断增加,线程数会逐渐增加并达到核心线程数,此时如果仍有任务被不断提交,就会被放入workQueue任务队列中,等待核心线程执行完当前任务后重新从workQueue中提取正在等待被执行的任务。等待核心线程执行完当前任务后重新从workQueue中提取正在等待被执行的任务。

    此时,假设我们的任务特别的多,已经达到了workQueue的容量上限,这时线程池就会启动后被力量,也就是maximumPoolSize最大线程数,线程池会在corePoolSize核心线程数的基础上继续创建线程来执行任务,假设任务被不断提交,线程池会持续创建线程直到线程数达到maximumPoolSize最大线程数,如果依然有任务被提交,这就超过了线程池的最大处理能力,这个时候线程池就会拒绝这些任务,我们可以看到实际任务进来之后,线程池会逐一判断corePoolSize、workQueue、maximumPoolSize,如果依然不能满足需求,则会拒绝任务。

    KeeepAliveTime+时间单位是当线程池中线程数量多余核心线程数时,而此时又没有任务可做,线程池就会检测线程的KeepAliveTime,如果超过规定的时间,无事可做的线程就会被销毁,以便减少内存的占用和资源消耗。如果后期任务又多了起来,线程池也会根据规则重新创建线程,所以这是一个可伸缩的过程,比较灵活,我们也可以用setKeepAliveTime方法动态改变KeepAliveTime的参数值。

    ThreadFactory实际上是一个线程工厂,它的作用是生产线程以便执行任务。我们可以选择使用默认的线程工厂,创建的线程都会在同一个线程组,并拥有一样的优先级,且都不是守护线程,我们也可以选择自己定制线程工厂,以方便给我线程自定义命名,不同的线程池内的线程通常会 根据具体业务来定制不同的线程名。

    workQueue和Handler,他们分别对应阻塞队列和任务拒绝策略,接下来一 一详细介绍。

3.学习线程池参数从创建线程线程池创建(从一个小例子说起)

           《阿里Java开发手册》线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

 Executors弊端:

      一.FixedThreadPool和SingleThreadPool允许的请求队列长度为Intener.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

     二.CachedThreadPool和SheduledThreadPool允许的创建线程数量为Intenger.MAX_VALUE,可能会创建大量线程,从而导致OOM. 

使用ThreadPoolExecutor创建一个线程池代码剖析如下:

       //设置核心池大小

        int corePoolSize = 5;

        //设置线程池最大能接受多少线程

        int maximumPoolSize=10;

        //当前线程数大于corePoolSize、小于maximumPoolSi,超出corePoolSize的线程数的生命周期

        long keepActiveTime = 200;

         //设置时间单位,秒

        TimeUnit timeUnit = TimeUnit.SECONDS;

        //设置线程池缓存队列的排队策略为FIFO,并且指定缓存队列大小为5。根据需求其它阻塞队列也可以的

        BlockingQueue<Runnable> workQueue=new ArrayBlockingQueue<Runnable>(5);

        //拒绝策略  将抛出 RejectedExecutionException.

        RejectedExecutionHandler handler =  new ThreadPoolExecutor.AbortPolicy();

        //创建ThreadPoolExecutor线程池对象,并初始化该对象的各种参数

        ThreadPoolExecutor executor= new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepActiveTime, timeUnit,workQueue,handler);

        //往线程池中循环提交线程

        for (int i = 0; i < 15; i++) {

            //创建线程类对象  根据自己需要创建对应的线程对象

            //TestTask myTask = new TestTask(i); 这里注释是创建的线程预留位

            //开启线程

            executor.execute(myTask);

            //获取线程池中线程的相应参数

            System.out.println("线程池中线程数目:" +executor.getPoolSize() + ",队列中等待执行的任务数目:"+executor.getQueue().size() + ",已执行完的任务数目:"+executor.getCompletedTaskCount());

        }

        //待线程池以及缓存队列中所有的线程任务完成后关闭线程池。

        executor.shutdown();       

 

4.线程池拒绝策略

  上述例子使用了AbortPolicy拒绝策略,可根据需要,选取以下参数。

  AbortPocy(线程池已满,抛出java.util.concurrent.RejectedExecutionException异常)

  DiscardPolicy(队列满了丢任务不异常)

  DiscardOlddestPolicy(将最早进入队列的任务删,之后再尝试加入队列)

  CallerRunsPolicy(如果添加到线程池失败,那么主线程会自己去执行该任务,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务.)

 

5.线程池常用阻塞队列

ArrayBlockingQueue(基于数组的先进先出队列,有界)

LinkedBlockingQueue (基于链表的先进先出队列,无界)

      对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为 Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列。由于 FixedThreadPool 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。

SynchronousQueue(无缓冲的等待队列,无界)

      SynchronousQueue阻塞队列,对应的线程池是 CachedThreadPool。线程池 CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

     我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。

DelayedWorkQueue

        DelayedWorkQueue阻塞队列,它对应的线程池分别是 ScheduledThreadPool 和 SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

 

6.有哪6种常见的线程池?什么是java8的ForkJoinPool

FixedThreadPool

     FixedThreadPool,它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了

CachedThreadPool

     CachedThreadPool,可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。

ScheduledThreadPool

     ScheduledThreadPool,它支持定时或周期性执行任务。比如每隔 10 秒钟执行一次任务,而实现这种功能的方法主要有 3 种,如代码所示:

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);

那么这 3 种方法有什么区别呢?

第一种方法 schedule 比较简单,表示延迟指定时间后执行一次任务,如果代码中设置参数为 10 秒,也就是 10 秒后执行一次任务后就结束。

第二种方法 scheduleAtFixedRate 表示以固定的频率执行任务,它的第二个参数 initialDelay 表示第一次延时时间,第三个参数 period 表示周期,也就是第一次延时后每次延时多长时间执行一次任务。

第三种方法 scheduleWithFixedDelay 与第二种方法类似,也是周期执行任务,区别在于对周期的定义,之前的 scheduleAtFixedRate 是以任务开始的时间为时间起点开始计时,时间到就开始执行第二次任务,而不管任务需要花多久执行;而 scheduleWithFixedDelay 方法以任务结束的时间为下一次循环的时间起点开始计时

SingleThreadExecutor

        SingleThreadExecutor,它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。

SingleThreadScheduleExecutor

      SingleThreadScheduledExecutor,它实际和第三种 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程,如源码所示:

new ScheduledThreadPoolExecutor(1);

它只是将 ScheduledThreadPool 的核心线程数设置为了 1。

 

ForkJoinPool

 

 

    ForkJoinPool 线程池和之前的线程池有两点非常大的不同之处。第一点它非常适合执行可以产生子任务的任务。

     如图所示,我们有一个 Task,这个 Task 可以产生三个子任务,三个子任务并行执行完毕后将结果汇总给 Result,比如说主任务需要执行非常繁重的计算任务,我们就可以把计算拆分成三个部分,这三个部分是互不影响相互独立的,这样就可以利用 CPU 的多核优势,并行计算,然后将结果进行汇总。这里面主要涉及两个步骤,第一步是拆分也就是 Fork,第二步是汇总也就是 Join,到这里你应该已经了解到 ForkJoinPool 线程池名字的由来了。

7.为什么不应该自动创建线程池(00M)

   自动创建的线程池都存在风险,想比较而言,我们自己手动创建会更好,因为我么可以更加明确线程池的运行规则,不仅可以选择适合自己的线程数量,更可以在必要的时候拒绝新的任务提交,避免线程资源耗尽的风险

8.合适的线程数量是多少?CPU核心数和线程数的关系?

   调整线程池中的线程数量的最主要的目的是为了充分并合理地使用 CPU 和内存等资源,从而最大限度地提高程序的性能。在实际工作中,我们需要根据任务类型的不同选择对应的策略。

CPU 密集型任务

首先,我们来看 CPU 密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU 核心数的 2 倍以上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。

针对这种情况,我们最好还要同时考虑在同一台机器上还有哪些其他会占用过多 CPU 资源的程序在运行,然后对资源使用做整体的平衡。

耗时 IO 型任务    

第二种任务是耗时 IO 型,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。

《Java并发编程实战》的作者 Brain Goetz 推荐的计算方法:线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)

通过这个公式,我们可以计算出一个合理的线程数量,如果任务的平均等待时间长,线程数就随之增加,而如果平均工作时间长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。太少的线程数会使得程序整体性能降低,而过多的线程也会消耗内存等其他资源,所以如果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情况衡量应该创建的线程数,合理并充分利用资源。

结论:

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

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

 (3)针对不同的程序,进行对应的实际测试就可以得到最合适的选择。

9.如何正确关闭线程池

shutdwon()

        shutdown(),它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。但这并不代表 shutdown() 操作是没有任何效果的,调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。

isShutdown()

       isShutdown(),它可以返回 true 或者 false 来判断线程池是否已经开始了关闭工作,也就是是否执行了 shutdown 或者 shutdownNow 方法。这里需要注意,如果调用 isShutdown() 方法的返回的结果为 true 并不代表线程池此时已经彻底关闭了,这仅仅代表线程池开始了关闭的流程,也就是说,此时可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务。

isTerminated()

       isTerminated(),这个方法可以检测线程池是否真正“终结”了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了,因为我们刚才说过,调用 shutdown 方法之后,线程池会继续执行里面未完成的任务,不仅包括线程正在执行的任务,还包括正在任务队列中等待的任务。比如此时已经调用了 shutdown 方法,但是有一个线程依然在执行任务,那么此时调用 isShutdown 方法返回的是 true ,而调用 isTerminated 方法返回的便是 false ,因为线程池中还有任务正在在被执行,线程池并没有真正“终结”。直到所有任务都执行完毕了,调用 isTerminated() 方法才会返回 true,这表示线程池已关闭并且线程池内部是空的,所有剩余的任务都执行完毕了。

awaitTermination()

 awaitTermination(),它本身并不是用来关闭线程池的,而是主要用来判断线程池状态的。比如我们给 awaitTermination 方法传入的参数是 10 秒,那么它就会陷入 10 秒钟的等待,直到发生以下三种情况之一:

一.等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回 true;

二.等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回 false;

三.等待期间线程被中断,方法会抛出 InterruptedException 异常。

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

shutdownNow()

     shutdownNow(),也是 5 种方法里功能最强大的,它与第一种 shutdown 方法不同之处在于名字中多了一个单词 Now,也就是表示立刻关闭的意思。在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值