创建线程池主要有两种方式:
- 通过Executor工厂类创建,创建方式比较简单,但是定制能力有限
- 通过ThreadPoolExecutor创建,创建方式比较复杂,但是定制能力强
但我们一般不建议使用Executor工厂类来进行线程的创建。
原因如下:
Executor提供的很多方法默认使用的都是无界的LinkedBlockingQueue,在高负载情况下,无界队列很容易导致OOM——OOM 全称 “Out Of Memory”,表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误。
那么一旦出现了OOM,就会导致所有的请求都无法处理,这个是致命问题。所有要尽量避免使用无界队列——》即慎用Executor来创建线程
那么下面让我们详细了解推荐使用的ThreadPoolExecutor这个核心工具类。
ThreadPoolExecutor的构造函数
ThreadPoolExecutor 的构造函数非常复杂,如下面代码所示,这个最完备的构造函数有 7 个参数。
ThreadPoolExecutor ( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler )
一起来看看这7个参数的含义分别是什么吧!
corePoolSize
核心线程数,就是该线程池保有的最下线程数
maximumPoolSize
最大线程数
keepAliveTime 和unit
我们首先要知道线程分为空闲(阻塞等待中)和忙碌(执行任务)两种状态,当一个线程空闲了较长时间,同时此时线程池子中的线程数目大于核心线程数目corePoolSize,该线程就会被销毁回收
keepAlive和unit就是用来衡量空闲时间的,当空闲时间大于keepAliveTime时候(unit是keepAliveTime是时间单位),同时线程数目大于corePoolSize时,该线程就会被注销
workQueue
工作队列(一个阻塞队列,用来存放可执行任务Runnable Task)
threadFactory
通过这个参数你可以自己定义如何创建线程,比如你可以给线程指定一个有意义名称
handler
通过这个参数,你可以自定义任务的拒绝策略,如果线程池中的线程都在忙碌,并且工作队列也满了(前提工作队列是有界队列),此时当有一个新的任务添加进来,提交任务,线程池会拒绝接收。
至于拒绝的策略,你可以通过handler这个参数来指定,ThreadPoolExecutor以及提供了以下四种策略。
CallerRunsPolicy:提交任务的线程自己去执行该任务
AbortPolicy:默认的拒绝策略,会throw RejectedExecutorException—》这是个运行时异常,编辑器不强制catch、容易忽略,开发人员要谨慎使用(可以自己定义自己的拒绝策略)
DiscardPolicy:直接丢弃任务,没人任何异常输出
DiscardOldestPolicy:丢弃最古老的任务,,其实就是把最早进入任务队列workQueue的任务给丢弃掉,然后把新任务添加到任务队列中
关于线程池的一些补充
线程是一个重量级的对象,应该避免频繁创建和销毁
目前业界线程池的设计,普遍采用的都是生产者 - 消费者模式。
线程池的使用方是生产者,线程池本身是消费者
比如上面我们的任务队列workQueue(生产的Runnable任务就放到了该队列中),这些任务供线程池中的线程执行(消费)
线程池运行原理分析
概念原理解释
1、创建一个线程池,当此时还没有任务提交的时候(阻塞队列workQueue为空的时候,此时默认线程池中是没有线程的,当然了,此时也可以调用prestartThread方法,来预先创建一个核心线程
2、当线程池里面还没有线程或者是线程池中存活的线程小于核心线程数corePoolSize的时候,每新提交一个任务,线程池就会专门创建一个新的线程来处理刚刚提交的任务(阻塞队列workQueue中的Runnable任务)。
此时线程池中的线程会一直存活着,即使线程是空闲的,甚至空闲时间超过了keepAliveTime ,线程也不会销毁(此时线程池中的线程小于核心线程数corePoolSize)
那么这个时候线程就只能等任务队列中什么时候有可执行的任务了(Runnable task) ,才会重写开始执行(变得忙碌起来,在此之前,线程一直是阻塞在那里的——》这也是为什么线程池队列用的是阻塞队列)
3、当线程池中的线程数等于线程核心数corePoolSize,同时任务队列有空间,此时当再有新的任务提交的时候,该任务会被放到任务队列中(workQueue)排队等待执行。而不会在新创建一个线程来执行刚刚提交的任务。
此时之前创建的线程并不会被注销,而是会不断的去拿阻塞队列中的任务,当任务队列中的任务为空的时候,线程会阻塞,直到有任务(Runnable Task)被放进任务队列中。
这也是为什么线程池的任务队列需要是阻塞队列。
我们之前说过Java中的线程池是生产者消费者模型,线程的使用方——》生产可运行的任务Runnable task,并且放到任务队列中)供线程池中的线程进行消费(任务的执行)
4、当线程池的线程数等于线程核心数corePoolsize,并且此时任务队列workQueue是满的状态。这时如果来了新的Runnable Task,线程池子就会创建新的线程来处理该任务。
直到线程数达到了maximumPoolSize——》就不会再继续创建新的线程来处理任务了。
这些新的线程在执行完了当前的任务后,也不会被销毁。而是执行任务队列中的RunnableTask,(此时线程是忙碌状态,非空闲)当把任务队列中的任务执行完了后(此时线程池中线程数应该是大于corePoolSize的,那么接下来就会有一个判断逻辑——》判断线程是否需要被销毁,当线程因为任务队列为空,陷入阻塞(进入空闲状态)的时间大于keepAliveTime的时候,该线程就会被销毁——》直到线程数等于corePoolSize
5、如果线程池中的线程数目达到了maximumSize,并且此时任务队列也满了。
这种情况下还有新的任务过来,那就直接采用拒绝的处理器进行处理。默认的处理器逻辑是抛出一个RejectedExecutionException异常
整个流程图如下:
一点补充
使用线程池,还要注意异常处理的问题,例如通过 ThreadPoolExecutor 对象的 execute() 方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常。虽然线程池提供了很多用于异常处理的方法,但是最稳妥和简单的方案还是捕获所有异常并按需处理
关于线程池的创建思路和思想:
参考:线程创建的开销与线程池 - 知乎