共计 3013 个字符,预计需要花费 8 分钟才能阅读完成。
一、前言
对应从事后端开发的同学来说,线程是必须要应用了,因为应用它能够晋升零碎的性能。然而,创立线程和销毁线程都是比拟耗时的操作,频繁的创立和销毁线程会节约很多 CPU 的资源。此外,如果每个工作都创立一个线程去解决,这样线程会越来越多。咱们晓得每个线程默认状况下占 1M 的内存空间,如果线程十分多,内存资源将会被耗尽。这时,咱们须要线程池去治理线程,不会呈现内存资源被耗尽的状况,也不会呈现频繁创立和销毁线程的状况,因为它外部是能够复用线程的。
二、从实战开始
在介绍线程池之前,让咱们先看个例子。
这个类的性能就是应用 Executors 类的 newSingleThreadExecutor 办法创立了的一个单线程池,他外面会执行 Callable 线程工作。
三、创立线程池的办法
咱们认真看看 Executors 类,会发现它外面给咱们封装了不少创立线程池的静态方法,如下图所示:
其实,咱们总结一下其实只有 6 种:
1.newCachedThreadPool 可缓冲线程池
它的外围线程数是 0,最大线程数是 integer 的最大值,每隔 60 秒回收一次闲暇线程,应用 SynchronousQueue 队列。SynchronousQueue 队列比拟非凡,外部只蕴含一个元素,插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且以后不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
2.newFixedThreadPool 固定大小线程池
它的外围线程数 和 最大线程数是一样,都是 nThreads 变量的值,该变量由用户本人决定,所以说是固定大小线程池。此外,它的每隔 0 毫秒回收一次线程,换句话说就是不回收线程,因为它的外围线程数 和 最大线程数是一样,回收了没有任何意义。此外,应用了 LinkedBlockingQueue 队列,该队列其实是有界队列,很多人误会了,只是它的初始大小比拟大是 integer 的最大值。
3.newScheduledThreadPool 定时工作线程池
它的外围线程数是 corePoolSize 变量,须要用户本人决定,最大线程数是 integer 的最大值,同样,它的每隔 0 毫秒回收一次线程,换句话说就是不回收线程。应用了 DelayedWorkQueue 队列,该队列具备延时的性能。
4.newSingleThreadExecutor 单个线程池
其实,跟下面的 newFixedThreadPool 是一样的,略微有一点区别是外围线程数 和 最大线程数 都是 1,这就是为什么说它是单线程池的起因。
5.newSingleThreadScheduledExecutor 单线程定时工作线程池
该线程池是对下面介绍过的 ScheduledThreadPoolExecutor 定时工作线程池的简略封装,外围线程数固定是 1,其余的性能截然不同。
6.newWorkStealingPool 窃取线程池
它是 JDK1.8 减少的新线程池,跟其余的实现形式都不一样,它底层是通过 ForkJoinPool 类来实现的。会创立一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的形式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。
讲了这么多,具体要怎么用呢?
其实 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor 和 newWorkStealingPool 办法创立和应用线程池的办法是一样的。这四个办法创立线程池返回值是 ExecutorService,通过它的 execute 办法执行线程。
newScheduledThreadPool 和 newSingleThreadScheduledExecutor 办法创立和应用线程池的办法也是一样的
以上两个办法创立的线程池返回值是 ScheduledExecutorService,通过它的 schedule 提交线程,并且能够配置提早执行的工夫。
四、自定义线程池
Executors 类有这么多办法能够创立线程池,然而阿里巴巴开发标准中却明确规定不要应用 Executors 类创立线程池,这是为什么呢?
newCachedThreadPool 可缓冲线程池,它的最大线程数是 integer 的最大值,意味着应用它创立的线程池,能够创立十分多的线程,咱们都晓得一个线程默认状况下占用内存 1M,如果创立的线程太多,占用内存太大,最初必定会呈现内存溢出的问题。
newFixedThreadPool 和 newSingleThreadExecutor 在这里都称为固定大小线程池,它的队列应用的 LinkedBlockingQueue,咱们都晓得这个队列默认大小是 integer 的最大值,意味着能够往该队列中加十分多的工作,每个工作也是要内存空间的,如果工作太多,最初必定也会呈现内存溢出的问题。
阿里倡议应用 ThreadPoolExecutor 类创立线程池,其实从刚刚看到的 Executors 类创立线程池的 newFixedThreadPool 等办法能够看出,它也是应用 ThreadPoolExecutor 类创立线程池的。
从上图能够看出 ThreadPoolExecutor 类的构造方法有 4 个,外面蕴含了很多参数,让咱们先一起认识一下:
咱们依据下面的内容自定义一个线程池:
从下面能够看到,咱们应用 ThreadPoolExecutor 类自定义了一个线程池,它的外围线程数是 8,最大线程数是 10,闲暇线程回收工夫是 30,单位是秒,寄存工作的队列用的 ArrayBlockingQueue,而队列满的解决策略用的 AbortPolicy。应用这个队列,根本能够放弃线程在零碎的可控范畴之内,不会呈现内存溢出的问题。然而也不是相对的,只是呈现内存溢出的概率比拟小。
当然,阿里巴巴开发标准倡议不应用 Executors 类创立线程池,并不示意它齐全没用,在一些低并发的业务场景照样能够应用。
五、最佳线程数
在应用线程池时,很多同学都有这样的疑难,不晓得如何配置线程数量,明天咱们一起探讨一下这个问题。
1. 经验值
配置线程数量之前,首先要看工作的类型是 IO 密集型,还是 CPU 密集型?
什么是 IO 密集型?
比方:频繁读取磁盘上的数据,或者须要通过网络近程调用接口。
什么是 CPU 密集型?
比方:非常复杂的调用,循环次数很多,或者递归调用档次很深等。
IO 密集型配置线程数经验值是:2N,其中 N 代表 CPU 核数。
CPU 密集型配置线程数经验值是:N + 1,其中 N 代表 CPU 核数。
如果获取 N 的值?
那么问题来了,混合型(既蕴含 IO 密集型,又蕴含 CPU 密集型)的如何配置线程数?
混合型如果 IO 密集型,和 CPU 密集型的执行工夫相差不太大,能够拆离开,以便于更好配置。如果执行工夫相差太大,优化的意义不大,比方 IO 密集型耗时 60s,CPU 密集型耗时 1s。
2. 最佳线程数目算法
除了下面介绍是经验值之外,其实还提供了计算公式:
很显然线程等待时间所占比例越高,须要越多线程。线程 CPU 工夫所占比例越高,须要越少线程。
虽说最佳线程数目算法更精确,然而线程等待时间和线程 CPU 工夫不好测量,理论状况应用得比拟少,个别用经验值就差不多了。再配合零碎压测,根本能够确定最适宜的线程数。