线程池创建及重要参数:
线程池可以自动创建也可以手动创建,自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadPool、newScheduledThreadPool。
可以通过Executors类来创建线程池,但是不推荐。Executors类只是静态工厂,提供创建线程池的几个静态方法,而真正的线程池类是ThreadPoolExecuor。构造方法如下:
corePoolSize:核心线程数。如果等于0,则任务执行完后,没有任务请求进入时销毁线程池中的线程。如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。设置过大会浪费系统资源,设置过小导致线程频繁创建。核心线程的创建是懒加载。
maximumPoolSize:最大线程数。必须大于等于1,且大于等于corePoolSize。如果与corePoolSize相等,则线程池大小固定。如果大于corePoolSize,则最多创建maximumPoolSize个线程执行任务,其他任务加入到workQueue缓存队列中,当workQueue为空且执行任务数小于maximumPoolSize时,线程空闲时间超过keepAliveTime会被回收。
keepAliveTime:线程空闲时间。线程池中线程空闲时间达到keepAliveTime值时,线程会被销毁,只到剩下corePoolSize个线程为止。默认情况下,线程池的最大线程数大于corePoolSize时,keepAliveTime才会起作用。如果allowCoreThreadTimeOut被设置为true,即使线程池的最大线程数等于corePoolSize,keepAliveTime也会起作用(回收超时的核心线程)。
unit:TimeUnit表示时间单位。
workQueue:缓存队列。如果当前运行的线程小于corePoolSize,则任务根本不会添加到workQueue中,如果此时运行的线程等于或大于corePoolSize时,这此时会首选将请求加入workQueue中,如果无法将请求加入workQueue(workQueue已满),则创建新的线程。如果创建线程数大于maximumPoolSize,新的任务会被拒绝。
threadFactory:线程工厂。用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀、是否为守护线程以及优先级等。设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的。
handler:执行拒绝策略对象。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务,执行拒接策略,可以看作简单的限流保护。
任务执行过程如下:
假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3,然后任务 4~13 被放入队列。这时候队列满了,任务 14、15、16 会被马上执行,而任务 17~20 则会抛出异常。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。
newFixedThreadPool:
创建固定线程数的线程池。核心线程数等于最大线程数,不存在空闲时间,keepAliveTime为0。
使用的无界的阻塞队列LinedBlockingQueue,如果线程获取一个任务后,执行时间特别长,会导致队列的任务越积越多,造成OOM。
适用于CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能少的分配线程,即适合执行长期的任务。
newSingleThreadExecutor:
创建单线程的线程池,核心线程数和最大线程数都为1。
适用于串行执行任务的场景,一个线程一个任务一个任务的依次执行。
newScheduledThreadPool:
创建支持定时以及周期性任务执行的线程池。最大线程数是Integer.MAX_VALUE。存在OOM风险。keepAliveTime为0,所有工作线程都不不回收。
scheduleAtFixedRate() :按某种速率周期执行
period:两次开始执行最小间隔时间
scheduleWithFixedDelay():在某个延迟后执行
command:要执行的任务
initialdelay:首次执行的延迟时间
delay: 一次执行终止和下一次执行开始之间的延迟
unit:initialdelay 和 delay 参数的时间单位
适用于周期性执行的场景。
newCachedThreadPool:
核心线程数为0,最大线程为Integer.MAX_VALUE,是一个高度可伸缩的线程池。存在OOM风险。keepAliveTime为60,工作线程处于空闲状态超过keepAliveTime会回收线程。
SynchronousQueue 是一个不存储元素的队列,可以认为这个队列里永远都是满的,因此只要有一个线程任务来了就会去创建非核心线程来执行任务。
适用在并发执行大量的短期小任务,因为空闲时间核心线程数为0,不占用CPU资源。
禁止直接使用Executors创建线程池的原因:
Executors.newCachedThreadPool和Executors.newScheduledThreadPool两个方法最大线程数为Integer.MAX_VALUE,如果达到上限,没有任务服务器可以继续工作,肯定会抛出OOM异常。
Executors.newSingleThreadExecutor和Executors.newFixedThreadPool两个方法的workQueue参数为new LinkedBlockingQueue<Runnable>(),容量为Integer.MAX_VALUE,如果瞬间请求非常大,会有OOM风险。
可以直接new一个ThreadPoolExecutor对象来进行指定各个线程池核心参数。
线程拒绝策略:
ThreadPoolExecutor提供了四个公开的内部静态类:
AbortPolicy:默认,丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy:丢弃任务,但是不抛出异常(不推荐)。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中。
CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。
友好的拒绝策略:
保存信息到数据库进行削峰填谷。在空闲时间再提出来执行。
转向某个提示页面。
打印日志信息。
线程池5种状态(RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED):
RUNNING
- 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
- 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
- 调用线程池的shutdownNow()方法,可以切换到STOP状态;
SHUTDOWN
- 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
- 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;
STOP
- 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
- 线程池中执行的任务为空,进入TIDYING状态;
TIDYING
- 该状态表明所有的任务已经运行终止,记录的任务数量为0。
- terminated()执行完毕,进入TERMINATED状态
TERMINATED
- 该状态表示线程池彻底终止
线程池常用的工作队列:
ArrayBlockingQueue 数组型阻塞队列(FIFO)
- 初始化一定容量的数组。
- 使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。
- 是有界设计,如果容量无法满足继续添加直到有元素被移除(阻塞)。
- 使用时开辟一段连续的内存。
LinkedBlockingQueue 链表型阻塞队列(FIFO)
- 基于链表的阻塞队列。
- 吞吐量高于ArrayBlockingQueue。
DelayQueue 延时队列
- 一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序
- 只有当指定的延迟时间到了,才能从队列中获取到该元素。
- 因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。可以用来管理一个DelayQueue来管理一个超时未响应的连接队列。
- DelayQueue只能添加(offer/put/add)实现了Delayed接口的对象。
SynchronousQueue 同步队列
- 内部容量是0
- 每次删除操作都要等到插入操作
- 每次插入操作都要等到删除操作
- 一个元素,一旦有了插入和移除线程,那么很快就由插入线程移交到移除线程,这个容器相当于是通道,本身不存储元素。
- 在多任务队列,是最快的处理任务方式。
PriorityBlockingQueue 优先阻塞队列
- 具有优先级的无界阻塞队列。
面试经典题目:
1. Java的线程池说一下,各个参数的作用,如何进行的?
2. 按线程池内部机制,当提交新任务时,有哪些异常要考虑。
(1)在任务代码中try/catch进行异常捕获。
(2)通过Future对象的get方法接收跑出的异常。
(3)重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用。
3. 线程池都有哪几种工作队列?
4. 使用无界队列的线程池会导致内存飙升吗?
5. 说说几种常见的线程池及使用场景?
线程池的阻塞队列为什么都用LinkedBlockingQueue,而不用ArrayBlockingQueue?
LinkedBlockingQueue使用单链表实现,可以指定链表长度,也可以不指定链表长度(默认为Integer.MAX_VALUE)。而且执行put的时候,将元素放到链表尾部节点;执行take的时候从头部取元素,两种操作分别有一个锁putLock和takeLock。
ArrayBlockingQueue使用数组实现,必须指定长度。指定长度大了浪费内存,指定长度小了会造成并发性不高,put操作只能阻塞等待,或者返回false。并且ArrayBlockingQueue只定义了一个lock,put和take使用同一把锁,不能同时进行。
总结:
1. LinkedBlockingQueue 无须指定长度,放入和取出元素使用不同的锁,双锁机制,互不影响,效率高,通用性强。
2. ArrayBlockingQueue 必须指定长度,大了浪费内存,小了性能不高,使用同一把锁,效率低。
为什么建议在不用线程池的时候,关闭线程池?
线程池的作用确实是为了减少频繁创建线程,使线程达到复用。但如果在不用线程池的情况下,线程池中的核心线程会一直存在,浪费资源,所以建议在不用的情况下调用shutdown方法关闭线程池。在需要的时候再调用创建线程池。
参考文档: