[线程池]
我们最初引入"线程"这个概念, 就是因为进程太"重量"了, 平凡的创建和销毁线程需要的开销很大, 所以引入一个更轻量级的概念 --> "线程". 线程可以看做是轻量级的"进程", 创建和销毁的开销很小. 引入"线程"这个概念, 大大提高了我们程序的性能. 然而, 时代发展飞快, 随着业务的不断复杂化和业务对性能要求的提升, 线程创建销毁带来的开销, 变得越来越明显(不再可以忽略了).
所以此时我们就引入了"线程池"来解决上述问题. "线程池" 就是把多个线程提前从系统申请好, 放到一个地方, 需要用线程的时候, 直接从这里取, 不用再去系统申请线程了, 之后当线程使用完之后, 还是放回到这个地方.
线程池这样的做法, 可以看做是把要用的资源"提前准备好", 然后等到需要的时候, 直接去拿. 这样就避免了"用一次创建一次"这样繁琐的步骤. (一次性创建好所有资源当然比一次一次的创建开销更小).
[理解]: 为什么从线程池里直接取线程, 比去系统申请线程的开销更小, 更加高效呢? --> 这里就涉及到"用户态"和"内核态"了. 我们直接从线程池里拿线程, 这个操作时纯纯的用户态代码, 根内核一点关系也没有 (因为线程都已经提前申请好了). 但是我们如果要去系统内核申请一个线程的话, 这个过程就变得"不可控"了, 谁知道内核会先去做什么, 做完之后才会去给你创建线程. 所以把这个任务交给内核, 内核创建线程的这个过程是"不可控"的. 所以, 这就是线程池为什么更高效的原因.
([注]: 我们通常认为, 纯用户态的操作, 就是比经过内核处理的操作 更加高效).
java标准库提供了现成的线程池来供我们使用. java.util.concurrent 包(简称juc包), 提供了很多有关多线程的API. 我们现在要提到的线程池的构造方法 ThreadPoolExecutor 就在这个包里面.
ThreadPoolExecutor是线程池的构造方法, 这个构造方法里面有很多参数. 每个参数都代表什么含义, 这是我们需要取重点理解的.
我们可以看到, ThreadPoolExecutor 这个类一共有7个参数, 接下来我们就看看这几个参数分别是干什么用的吧:
(1) int corePoolSize, int maximumPoolSize
corePoolSize -> 核心线程数, maximumPoolSize -> 最大线程数. 线程池, 一般都支持"线程扩容", 比如某线程池一开始有M个线程, 但是, 后来发现在实际使用中, M个线程不太够用, 所以就会对线程池进行扩容, 增加线程的个数. 这里的corePoolSize(核心线程数)可以理解为该线程池最少有多少个线程, 而线程池中, 除了有核心线程, 还有非核心线程 (在线程池扩容过程中增加的). maximumPoolSize 就可以理解为"最大线程数" (核心线程数 + 非核心线程数 的最大值).
(2) long keepAliveTime, TimeUnit unit
非核心线程在其空闲的时候就会被销毁, 这里的keepAliveTime就表示允许非核心线程空闲的最大时间, 如果某个非核心线程空闲时间超过了这一数值, 那么这个线程将被销毁. 后面的TImeUnit unit 表示空闲时间的单位, 有 秒, 分钟, 小时, 天 .
(3) BlockingQueue<Runnable> workQueue
该参数表示线程池的工作队列. 线程池的工作过程是典型的"生产者--消费者"模型. 我们在使用线程池的时候, 需要通过"submit"方法, 把要执行的任务提交给工作队列. 这里我们在参数中看到的"Runnable"就是要执行的任务. (Runnable这个接口本身的含义就是一段可执行的任务).
(4) ThreadFactory threadFactory
"线程工厂" , ("工厂"指的是"工厂设计模式") ThreadFactory这个类是线程的工厂类, 用于完成Thread的实例创建和初始化操作. 而起ThreadFactory可以对线程池中的多个线程进行批量的设置属性. (注: 这里我们一般不会做出调整, 使用ThreadFactory的默认值即可).
(5) RejectedExecutionHandler handler
这个参数的意思是 "拒绝策略". 这个参数是7个参数中最为复杂的一个, 也是最为重要的一个. 这个参数表示: 当线程池的工作队列满了之后, 如果还要继续给这个队列添加任务, 此时线程池应该怎样做? --> 拒绝!!! 那么怎样拒绝呢? java标准库给出了四种不同的拒绝策略.
如上图, java一共提供了4种不同的拒绝策略:
(1) AbortPolicy
添加任务的时候, 直接抛出异常.
(2) CallerRunsPolicy
线程池拒绝执行该任务, 让submit该任务的线程负责执行.
(3) DiscardOldestPolicy
把工作队列中最老的任务删除掉, 然后执行新添加的任务.
(4) DiscardPloicy
把工作队列中最新的任务删除掉.
虽然 ThreadPoolExecutor 的功能很强大, 但是由于参数太多, 使用起来十分的麻烦. 所以java标准库对这个类做了进一步封装 --> Executors. Executors提供了一些工厂方法, 可以更方便地构造出线程池.
上述这些方法都是对ThreadPoolExecutor的进一步封装.
其中, newCachedThreadPool() 设置了非常大的线程数, 使得我们可以对线程池进行不停的扩容; newFixedThreadPool() 把核心线程数和最大线程数设置成了一样的值, 那么此时线程池的线程数就变成了固定数量, 不会自动扩容.
在创建线程池的时候, 需要指定线程的个数, 那么该如何确定最合适的线程个数的值呢? --> 通过实验的方式. 通过实验测定不同线程数下程序的性能, 以确定最合适的线程个数.
好了, 本篇文章就介绍到这里啦, 大家如果有疑问欢迎评论, 如果喜欢小编的文章, 记得点赞收藏~~