java里的线程池一般指的是ThreadPoolExecutor及其相关的一些类和接口.
类的结构图如下:

Executor 框架的基本组成
杨晓峰 极客时间专栏
ThreadPoolExecutor的构造函数如下:

corePoolSize是核心线程数,可以理解为长期驻留的线程数量.
maximumPoolSize是最大线程数量,根据后续的实验结果验证,是在核心线程数和工作队列都满了的情况下,会创建的最大线程数量.
keepAliveTime和unit指定非核心线程的空闲等待时间.
workQueue是工作队列,必须是BlockingQueue
rejectedExecutionHandler则指定拒绝策略
使用不同的入参组合,可以构建出各种风格迥异的线程池实例.
Executors提供了5种构造线程池的方法
newFixedThreadPool(int nThreads) 固定线程池
newCachedThreadPool 缓存线程池
newSingleThreadExecutor 单任务线程池
newSingleThreadScheduledExecutor和newScheduledThreadPool(int corePoolSize) 定时任务线程池
newWorkStealingPool(int parallelism) WorkStealing线程池
下面结合代码实例一一分析
newFixedThreadPool(int nThreads)
只有1个入参,核心线程数和最大线程数都是nThreads,任何时候同时只有这么多的线程处于活跃状态,超出这个数量的话,新提交的任务会在无界队列中进行等待,直到有任务结束,再执行新的任务.

我们先写一个简单的Run类.它只打印当前时间,task名称和当前线程名称,然后sleep 1秒后退出.

然后写一个简单的exec方法,传入指定类型的线程池,对这个线程池做5次submit.

执行fix方法,会输出什么内容呢?
由于这里指定的nThread是3,因此理论上会先打印3行,然后等1秒后,这3个任务结束后,再打印2行.
我们看一下,实际的执行结果如下:

输出结果符合预期.
task-1,task-2,task-3分别被thread-1,thread-2,thread-3执行,同时task-4和task-5被放入队列中.
1秒后task-1~3这三个任务结束,从队列中取出task-4和task-5,由thread-2,thread-1执行.
核心线程数和最大线程数都是3.
newCachedThreadPool()
适合处理大量短时间的工作任务.没有入参,其核心线程数是0,最大线程数可以认为是无限大,它会尝试缓存线程来复用,如果没有可用的缓存线程,就创建新的线程来执行任务,线程空闲60秒后,会被回收.
其使用的SynchronousQueue有点复杂,后续可以考虑专门写篇文章来分析.

执行结果如下:

5个任务,同时被5个线程执行.
newSingleThreadExecutor()
只有一个工作线程,可以保证任务是按提交顺序来执行.

这个线程池执行exec的输出如下:

每隔一秒,执行一个task,全部都是被thread-1执行.
newSingleThreadScheduledExecutor() newScheduledThreadPool(int corePoolSize)
这两个是用于定时或周期性地执行任务.
newSingleThreadScheduledExecutor实际上就是
newScheduledThreadPool(1),因此可以一起分析


定时任务线程池主要使用schedule/scheduleAtFixedRate/scheduleWithFixedDelay方法,而不是submit来执行定时任务.
schedule方法是在指定时间间隔后执行所有任务,但是也会收到corePoolSize的限制,示例如下:

可以看到corePoolSize设置为3的情况下,延迟2秒后,启动3个线程执行了3个任务,1秒后,3个任务结束.thread-2和thread-3再执行task-4和task-5.
scheduleAtFixedRate方法,initialDelay是初始等待时间,然后按照period周期性执行所有任务.

示例中,1秒后,执行3个任务,然后过了一秒,任务结束,再执行剩余的2个任务.
从2021-03-27 16:13:58第一次执行的3秒后,再次执行所有任务.
scheduleWithFixedDelay方法是上一批任务执行完成后,等待指定间隔,再周期性执行下一批任务

可以看到它和scheduleAtFixedRate的区别在于,scheduleAtFixedRate是按照任务开始时间计算周期间隔,而scheduleWithFixedDelay是要等上一批任务都执行完后,再周期性地执行下一批任务.
newWorkStealingPool(int parallelism)
这个线程池是从JDK1.8后提供的,在不指定parallelism的情况下,默认使用CPU核心数来设置核心线程数和最大线程数.

它的内部使用ForkJoinPool来实现,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
表现形式上似乎和newFixedThreadPool没有太大区别.
为了更好地测试这个线程池,我们使用CountDownLatch来协调线程.


我的电脑CPU型号是intel i5 4代 4200M,双核四线程,四个逻辑核心.对于JVM来说,可用核心数就是4个,因此指定核心线程数和最大线程数都是4.
打印结果可以看出,使用了4个线程来处理任务.
显式指定parallelism的话就会按照指定的并行度来设置线程数.

说完了Executors提供的5种方法,其实直接使用这些方法都是有隐患的.
newFixedThreadPool和newSingleThreadExecutor的工作队列是无界队列
newCachedThreadPool和newScheduledThreadPool的问题则是最大线程数为Integer.MAX_VALUE,可能创建出很大数量的线程.
这4种方法都可能导致OOM,示例如下:
使用-Xmx20m,把最大堆内存设置为20m,然后submit大量线程,就会出现OOM


那么newWorkStealingPool会不会出现OOM呢? 有些人说不会,但是经过测试,还是会出现OOM.

Java提供的方法都是尽可能通用的.
我们应该根据自己的实际需求,来创建合适的线程池.
当在execute(Runnable)方法中提交新任务并且少于corePoolSize线程正在运行时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多于corePoolSize但小于maximumPoolSize线程正在运行,则仅当队列已满时才会创建新线程。
徐志毅 https://www.jianshu.com/p/c41e942bcd64

我们使用ThreadPoolExecutor构造器测试一下自定义的线程池.

把等待队列的capacity设为2,核心线程数设为2,最大线程数设为3.
一开始,创建2个核心线程,接着task-3和task-4被放入等待队列,由于最大线程数设成了3,因此除了2个核心线程外,还能再启动一个非核心线程来处理task-5.
如果我们把最大线程数改为2,那么task-1和task-2占据了2个所有可用线程,task-3和task-4被放入等待队列.提交task-5时,就会执行拒绝策略.

虽然默认的拒绝策略会抛出exception,但是exception总比OOM要好,毕竟exception只是个异常,不会影响到应用程序的其他功能.而OOM会导致整个程序不可用.
另外我们还可以通过设置其他的拒绝策略来处理被拒绝的请求.
那么在实际应用中,如何选择线程数呢?
如果我们的任务是CPU密集型的,为了避免线程切换影响性能,可以使用CPU核数来作为线程数.
如果是IO密集型的任务,则可以参考Brain Goetz 推荐的计算方法:
线程数 = CPU核数 × 目标CPU利用率 ×(1 + 平均等待时间/平均工作时间)
杨晓峰 极客时间专栏 / Brain Goetz 童云兰 并发编程实战 141
平均等待时间和平均工作时间需要根据实际情况来进行测算和调整.




