Java基础 并发知识回顾------线程池

9 篇文章 0 订阅
1 篇文章 0 订阅

目录

1. 为什么要使用线程池

2. 线程池的主要执行过程 

3. JDK提供的线程池API

4. ThreadPoolExecutor 详解

4.1. workQueue 参数

4.1.1. 直接提交队列(SynchronousQueue)

4.1.2. 有界的任务队列(ArrayBlockingQueue)

4.1.3. 无界的任务队列(LinkedBlockingQueue)

4.1.4. 优先任务队列(PriorityBlockingQueue)

4.2. handler 参数

4.2.1. AbortPolicy

4.2.2. CallerRunsPolicy

4.2.3. DiscardOldestPolicy

4.2.4. DiscardPolicy

4.3. ThreadPoolExecutor 核心代码详解

5. Executors 主要方法介绍

5.1. newFixedThreadPool 

5.2. newSingleThreadExecutor

5.3. newCachedThreadPool

5.4. newSingleThreadScheduledExecutor

5.5. newScheduledThreadPool

6. 总结

参考:


1. 为什么要使用线程池

在《Java并发编程的艺术》里有提到使用线程池的好处,主要有三点

  • 降低资源消耗。让每个线程都能重复使用,减少频繁的线程创建和销毁

  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

总结:

cpu的资源是有限的,线程的创建也需要代价,线程的创建和销毁的花销往往比线程空转的花销要大,所以我们需要一个能管理线程,让其能够重复利用。

 


2. 线程池的主要执行过程 

当一个任务提交到线程池,线程池的执行过程如下:

1. 判断线程数是否小于corePoolSize且都在执行任务。如果不是则创建新的/分配线程来执行任务;如果是则进入下一个流程。

2. 线程池判断工作队列是否已满。如果未满则将新提交的任务存储在这里;如果已满则进入下一个流程。

3. 线程池判断线程池的线程是否都处于工作状态。如果没有则创建一个新的线程来执行任务;如果是则交给拒绝策略来处理。

 


3. JDK提供的线程池API

(基于JDK1.8)

为了更好地控制多线程,JDK提供了一套 Executor 框架,其本质就是一个线程池,它的总体架构如下图:

Executor 接口提供一种将任务提交与每个任务将如何运行的机制分离开来的方法。它只提供了execute(Runnable)这么一个方法,用于执行已提交的Runnable任务。

 

ExecutorService 继承了 Executor 接口,其中有线程池生命周期管理方法、提交用于执行的Runnable任务、执行给定任务。

 

AbstractExecutorService 提供了 ExecutorService 的默认实现。

 

ThreadPoolExecutor 实现了 Executor 接口,因此通过这个接口,任何 Runnable 对象都可被 ThreadPoolExecutor 线程池调度,是用的最多的线程池

 

ForkJoinPoll 是JDK 1.7 新增的线程池,其可以充分利用多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。所以适用于在大多数由任务产生大量子任务的情况,或从外部客户端大量提交小任务到池中的情况下。

 

Executors 类在其中扮演着线程池工厂的角色,我们可以通过 Executors 取得一个有特定功能的线程池。

 

但是!

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

使用 Executors 创建线程池风险如下:

  • newFixedThreadPoolnewSingleThreadExecutor 允许请求的队列长度为 Integer.MAX_VALUE,可能会堆集大量请求,而导致OOM。
  • newCachedThreadPool newScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

总的来说就是不支持自定义拒绝策略

而且上述 Executors 中 newFixedThreadPool 、newSingleThreadExecutor、 newCachedThreadPool 内部其实也是通过调用 ThreadPoolExecutor 的构造函数实现的,它们都只是 ThreadPoolExecutor 的封装类。

当然在清晰自己的需求时酌情使用无可厚非,学习的话当然是通过 ThreadPoolExecutor 来构建线程池最能加深理解。

 


4. ThreadPoolExecutor 详解

这个就是 ThreadPoolExecutor 中最重要的构造函数,另外三个构造函数都是内部再调用了该构造函数。

因此手动配置线程池的关键就是学好构造方法中的几个参数的设置。

· corePoolSize:指定线程池中线程的数量

· maximumPoolSize:指定线程池中的最大线程数

· keepAliveTime:当线程池线程的数量超过 corePoolSize 时,多余的空闲线程的存活时间,即超过corePoolSize的空闲线程,在多长时间内会被销毁。当任务很多,且任务执行时间很短的情况下,可以将该值调大,提高线程利用率。

· unit:KeepAliveTime的单位(时间单位)。

· workQueue:任务队列,被提交但尚未被执行的任务。

· threadFactory:线程工厂,用于创建线程,一般用默认的即可。

· handler:拒绝策略,当任务太多来不及处理时,如何拒绝任务。

取上面的参数中 workQueuehandler 做详细说明:

 

4.1. workQueue 参数

workQueue 是用来存放已提交但未执行任务的阻塞队列,仅用于存放 Runnable 对象,在ThreadPoolExecutor 类的构造函数中可以使用以下几种 BlockingQueue 接口:

4.1.1. 直接提交队列(SynchronousQueue)

SynchronousQueues 是一个特殊的 BlockingQueue,因为它没有容量,提交的任务不会被真实地保存,而总是将新任务提交给线程执行。如果没有空闲的进程,则尝试创建新的进程,如果进程数量已经达到最大值,则执行拒绝策略。

因此使用 SynchronousQueues 通常要设置一个很大的 maximumPoolSize,否则很容易执行拒绝策略。

Executors.newCachedThreadPool 用的阻塞队列就是它。

 

4.1.2. 有界的任务队列(ArrayBlockingQueue)

ArrayBlockingQueue 类的构造函数必须带一个容量参数,表示该队列的最大容量

使用有界的任务队列时,当有新任务需要执行,如果当前线程数小于 corePoolSize 则会优先创建新线程;若大于corePoolSize 则会将新任务加入到等待队列

若等待队列已满了,则在线程池的线程数小于 maximumPoolSize 的前提下创建新的线程执行任务;若大于 maximumPoolSize 则执行拒绝策略

可见使用 ArrayBlockingQueue 时只有在队满的情况下线程数才会提升到 corePoolSize 以上,即除非系统非常繁忙,否则可以确保核心线程数维持在 corePoolSize

 

 

4.1.3. 无界的任务队列(LinkedBlockingQueue)

LinkedBlockingQueue 与有界队列相比不同的是,除非系统资源耗尽,否则无界的任务队列不存在入队失败的情况

当有新任务到来,线程池线程数小于 corePoolSize,线程池会创建新线程执行任务;当线程数达到 corePoolSize 以后,线程池的线程数将不再继续增加后面的任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会一直增长,很可能会耗尽系统内存 。

所以对于 LinkedBlockingQueue 来说 corePoolSize 即是最大可允许创建的线程

Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor()方法中使用的就是此策略。

 

4.1.4. 优先任务队列(PriorityBlockingQueue)

PriorityBlockingQueue 是一个特殊的无界队列,和前面的 FIFO 不同,它可以根据任务自身的优先级顺序先后执行,总是确保高优先级的任务先执行,在确保系统性能的同时,也能有很好的质量保证。

 

4.2. handler 参数

handler 指定了拒绝策略,当线程池已经关闭或者任务数量超过系统实际的承载能力时,就要用到拒绝策略了。

JDK内置了四种拒绝策略:

4.2.1. AbortPolicy

默认策略,该策略会直接抛出 RejectedExecutionException 异常,阻止系统正常工作。

 

4.2.2. CallerRunsPolicy

只要线程池未关闭,该策略直接在调用者线程中运行当前被丢弃的任务。这样做虽然能做到无任务被丢弃,但任务提交线程的性能极有可能急剧下降。

 

4.2.3. DiscardOldestPolicy

该策略将丢弃最老的一个请求(即位于队列头部的任务,也就是当前即将被执行的一个任务),并再次尝试提交当前任务。如果再次失败将重复此过程。

 

4.2.4. DiscardPolicy

该策略默默的丢弃无法处理的任务,不做任何处理

 

当然我们也可以自己拓展 RejectedExecutionHandler 接口来自定义拒绝策略

 

4.3. ThreadPoolExecutor 核心代码详解

(QQ的截图双击一下就没了...懒得再打一遍了)

那 JDK 中是如果知道线程的状态与当前核心线程数量的呢?

就是通过 AtomicInteger 类的 ctl 来获得,ctl 的前3位用来表示当前线程池的状态后29位来表示线程的数量。(整形32位)

这样我们就可以通过简单的位运算来获得线程池的状态与线程的数量了。

线程池一共有五种状态

各个状态的转换如下图:

 

现在我们再回头看看 Executors 所提供的线程池的底层是怎样调用 ThreadPoolExecutor 的构造函数的:

 


5. Executors 主要方法介绍

虽然《阿里巴巴Java开发手册》规定不要用 Executors 来创建线程,但理解其底层还是还是很有必要。

5.1. newFixedThreadPool 

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

根据代码可以发现线程池的核心线程数(corePoolSize)等于最大线程数(maximumPoolSize),所以不存在线程数大于 corePoolSize 的情况,后面的 keepAliveTime 和 unit 可忽略不看。

它的任务队列是 LinkedBlockingQueue,是一个无界的阻塞队列

所以它返回的是一个固定线程数且数量为 nThread 的线程池,当一个新的任务提交时,如果线程池线程数小于 nThread,则立即执行;如果大于则进入等待队列。

 

5.2. newSingleThreadExecutor

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

和上一个线程池相仿,但它的corePoolSize与maximumPoolSize都只有 1。

它返回的是只有一个线程的线程池,多余的任务会被提交到无界的任务队列中。

 

5.3. newCachedThreadPool

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

这个线程的corePoolSize为 0,但 maximumPoolSize 为整形的最大值,说明当又线程空闲时间超过 60s 了,就会被回收。

该线程池当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存。适合处理大量短时间工作任务的线程池。

 

5.4. newSingleThreadScheduledExecutor

    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

这个底层不是调用 ThreadPoolExecutor,该方法会返回一个 ScheduledExecutorService 对象,线程池大小为 1。可以进行定时或周期性的工作调度。

 

5.5. newScheduledThreadPool

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

该方法和 newSingleThreadScheduledExecutor 的区别就是可以指定线程池的大小。

 

6. 总结

线程池能有效的管理线程并提升线程复用率,减少系统开销,Executors 提供了一些常用的线程池,但其中有一定的风险,合理的使用 ThreadPoolExecutor 构建自己想要的线程池,相信能有很大的收益!

 

参考:

Java并发编程的艺术

实战Java高并发程序设计(第2版)

https://www.cnblogs.com/Java3y/p/8996365.html

https://snailclimb.top/JavaGuide/#/

https://blog.csdn.net/panweiwei1994/article/details/79038846#executor

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值