多线程(六)
目录
一 . 线程池
(1)什么是线程池?
问题:为什么直接从线程池里取比从系统申请更高效呢?
(2)标准库中的线程池
问题:使用线程池的时候,如何指定线程个数?
二 . 模拟实现线程池
一 . 线程池
(1)什么是线程池?
如字面意思一样,就是用来存放线程的一个 “ 池子 ” 。这个线程池的作用,存在的意义是什么呢?
例如:我们买纸巾,当我们家里需要买纸巾的时候,我们出门去买一包,放在家里,隔一天用完了我们又下楼去买一包,用完了再买一包。这样子的效率当然是比较低下的,因为我们纸巾是日常消耗品,用的很快,我们一趟只买一包这种方法是不可取的。所以我们就一提或者一箱一箱的买。
线程就相当于我们的纸巾,线程池就相当于我们买回来放纸巾的箱子,我们先买好一大箱纸巾放在家里,当我们需要用纸巾的时候,我们从箱子里拿出来使用即可。
最初引入线程,就是因为进程太 “ 重 ” 了,我们频繁地创建 / 销毁进程的开销太大了,所以就有了线程。随着时代科技的发展,业务上对于性能的要求越来越高。那么对应的线程的创建 / 销毁的开销就变得很明显了,让我们不能忽略不计。此时线程池就应运而生了。
线程池就是将线程提前从系统中申请好,共同存到一个地方,后续有需要使用该线程的时候,就直接到这个地方来取,而不是系统重新申请,将线程用完了之后,也还是放回到原来的地方。
所以线程池最大的好处就是可以减少我们每次启动,销毁线程的损耗。
问题:为什么直接从线程池里取比从系统申请更高效呢?
这个问题,就要提到我们操作系统中的两个概念:内核态与用户态。
操作系统就是由 “ 操作系统内核 ” + “ 操作系统配套的应用程序 ” 。而对应的,执行很多代码逻辑,都是要用户态的代码和内核态的代码配合完成的。
内核态的代码在执行过程中整个过程是不一定连贯且不可控的,效率比较低的。 用户态的代码在执行过程中整个过程是连贯且可控的,效率是比较高的。
因此,通常情况下我们认为纯用户态操作,就比经过内核态的操作效率要更高。而这里我们从线程池里直接取线程就是用户态操作,从系统申请线程,调用系统 API 这就是内核态的操作。
(2)标准库中的线程池
咱们来看看 Java 标准库的线程池,在标准库中提供了类:ThreadPoolExecutor 这一构造参数。
可以看到,这一构造函数中有着很多参数,咱们一个一个来看。
(2 . 1)int corePoolSize :核心线程数;int maxmumPoolSize :最大线程数。
在 Java 标准库的线程池中,就把里面的线程分成两类:核心线程与非核心线程。核心线程我们就可以理解为最少有多少个线程。非核心线程就是指在线程扩容的过程中新增的线程。
核心线程数 + 非核心线程 = 最大线程数
注意:CPU上的核心数目是有限的,且核心线程会始终存在于线程池的内部,而非核心线程,在繁忙的被创建出来,不繁忙了,在空闲时候就会把这些线程释放掉。
(2 . 2)long keepAliveTime : 数值,表示非核心线程允许 “ 摸鱼 ” 的最大时间,超过这个时间还一直空闲就会被释放掉。
TimeUnit unit : 单位(秒、分钟、小时、天 . . . . . .)
(2 . 3)BlockingQueue<Runnable> workQueue : 工作队列。
线程池的工作过程就是典型的 “ 生产者消费者模型 ”。当程序员使用的时候,通过形如 “ submit ” 这样的方法,将要执行的任务设定到线程池里,线程池内部的工作线程就负责执行这些任务。
此处的队列可以由我们自行指定,可以指定队列的容量(capacity),执行队列的类型等。
(2 . 4)ThreadFactory threadFactory : 线程工厂
就是 Thread 这个类的工厂类,通过这个类,完成 Thread 的实例创建和初始化操作。
此处的 ThreadFactory 可以针对线程池里的线程进行批量的设置属性。
(2 . 5)RejectedExecutionHandler handler : 拒绝策略 (敲黑板!重点!!!)
在线程池中,如果我们的任务队列满了,但还要继续给这个队列添加任务,此时应该怎么办呢?
答案当然是不要继续阻塞,而是要直接拒绝!
例如我给女神表白,然后女神明确拒绝了我,这是坏事儿嘛?这也就比女神接受我的表白坏那么点,她明确拒绝了我,那就算咯,我们就继续追求下一个。但是还有一种更坏的情况:女神就笑笑,也不正面回答我,对我忽冷忽热的,这就很危险了啊兄弟!这样子就被女人拿捏了啊,咱们又不能脱身去追求下一个,这样等下去又不知道什么时候是个头。
所以,有时候拒绝不一定是坏事。
当我们的任务队列满了,但这个时候还要给这个队列添加任务,此时就应该采取 “ 拒绝策略 ” 。
在我们 Java 标准库中给出了四种不同的拒绝策略:
当我们队列放满了的情况是无法避免的,此时需要我们结合实际情况,选择合适的拒绝策略。
问题:使用线程池的时候,如何指定线程个数?
在实际开发中,建议通过 “ 实验 ” 的方式来找到一个合适的线程池的个数的值。
怎么实验呢?给线程池设置不同的线程数,分别进行性能测试,关注响应时间和消耗资源等各项指标,来选取合适的的数值
二 . 模拟实现线程池
现在我们来模拟实现一个基础简单的线程池:
实现线程池其实很简单,我们这也是一个很基础很简单的线程池代码案例,其核心操作原理就是借助我们的一个阻塞队列来进行我们任务的交换,一边来添加任务,一边来获取任务并执行。
OKK,有关线程池的相关知识就讲这么多了,这一板块概念性的知识很多,比较抽象,不太好理解,大家还是得自己上手试试更好。话不多说,咱们下期再见吧!!!