本文已参与「新人创作礼」活动,一起开启掘金创作之路。
线程池分类
(1)FixThreadPool
作用:固定线程数量的线程池(优点),最大线程数量等于核心线程数量(初期没有线程,每提交一个任务就创建一个线程,线程数达到最大值时就会保持不变),无法处理的任务会被放入阻塞队列中。
缺点:使用LinkedBlockingQueue链表阻塞队列,默认容量为Integer.MAX_VALUE,可能会堆积大量的任务,导致OOM(内存溢出)
适用场景:可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。
(2)SingleThreadPool
作用:只有一个线程的线程池,无法处理的任务会被放入阻塞队列中。
缺点:使用LinkedBlockingQueue链表阻塞队列,默认容量为Integer.MAX_VALUE,可能会堆积大量的任务,导致OOM(内存溢出)
适用场景:适用于保证任务顺序执行
(3)CacheThreadPool
作用:线程数量可根据实际需求调整,若任务没有空闲线程处理,会创建新线程处理该任务。如果有线程空闲时间超过60秒会被销毁。(何为空闲?没有接到任务就是空闲。线程池的线程是轮询接收任务。)
缺点:线程池大小不受限制,依赖于JVM可承受的最大线程池大小,线程最大数量为Integer.MAX_VALUE,创建大量线程容易导致OOM(内存溢出)
适用场景:快速处理大量耗时较短的任务,如Netty的NIO接受请求
(4)ScheduledThreadPool
作用:核心线程数量自定义,支持定时以及周期性的任务执行。(若线程异常结束,会创建新线程替代)
缺点:线程最大数量为Integer.MAX_VALUE,可能会OOM(内存溢出)
适用场景:做定时任务时使用
使用Executors创建线程池的缺点
Executors创建线程会使用默认参数,线程池内部情况不可控,底层也是使用FixThreadPool等创建线程,且指定的默认参数没有限制线程数量的上限,可能导致OOM。
FixThreadPool创建线程可以更清楚线程池内部的运行规则,可以尽量避免内存溢出的情况。
线程池的几个重要参数
-
int corePoolSize, 核心线程数
-
int maximumPoolSize, 最大线程数
-
long keepAliveTime, TimeUnit unit, 线程存活时间。
- 当任务来的时候不是直接调用核心线程或随机调用一个线程,而是在Condition 等待队列中轮询调用线程,因此即便任务数小于核心线程数,非核心线程仍然可能会一直存活。
-
unit 空闲线程存活时间单位
-
BlockingQueue workQueue, 阻塞队列(排队的线程放入)
-
ThreadFactory threadFactory, 创建线程的线程工厂
-
RejectedExecutionHandler handler 拒绝策略,线程满了该怎么做
线程池参数如何设置
考虑以下几点
1、下游系统的并发能力
要考虑下游系统可以抗住多少并发量,不能让下游系统挂了,给下游系统造成的并发量取决于线程数。
例:多线程访问数据库,设计数据库连接池大小时,若数据库并发量太大,影响数据库响应时间,数据库可能会挂掉;并发量太小,系统资源利用率不够,QPS低。
2、CPU使用率
线程数设置得比较大时:
(1)线程的创建,切换,销毁等会消耗比较多的cpu资源,使CPU使用率维持在比较高的水平。
(2)任务短时间迅速执行时,任务的集中执行会给cpu造成比较大的压力,使得cpu的使用率呈现锯齿状,短时间内cpu升高,再迅速下降到闲置状态。
可以观察机器的cpu使用率和cpu负载,这两个参数来判断线程数是否合理。
cpu使用率:可以知道CPU实际运行的情况。
cpu负载:是正在执行的线程和等待执行(running状态但没有被CPU调度的线程)的线程之和。
负载的值通常约等于cpu核数比较合理。
压测时出现高负载,低CPU使用率的情况
原因:等待磁盘I/O的进程过多,导致进程队列过长,但cpu运行的进程却很少。
场景1:磁盘读写请求过多,导致大量I/O等待(内核读取磁盘,进程转换为不可中断睡眠状态,睡眠状态的进程多了,负载就高)。
场景2:MySQL存在慢查询或死锁的情况(慢查询或死锁会造成IO阻塞,使睡眠的进程不可中断)。
3、线程池中执行的任务性质
计算密集型的任务占cpu使用率高,线程数略微大于cpu的核数。
IO型任务主要时间消耗在IO等待上,cpu压力不是很大,线程数一般设置的比较的大。
例:多线程访问数据库,数据库有20个常访问的表,考虑使用20个以上的线程数。
4、内存使用率
线程数和任务队列大小会影响内存使用率。
队列大小应该根据任务量设置,队列太大占用内存高甚至可能OOM,队列太小溢出会走拒绝策略,对队列的性能会有一定影响。
线程池拒绝策略
-
抛异常(默认策略)CallerRunsPolicy
抛出RejectedExecutionException异常,阻止系统正常运行。
-
不理不问,DiscardPolicy
丢弃任务但不抛出异常。如果线程队列已满,则后续提交的任务都会静默丢弃,不处理也不抛出异常。
-
抛弃最久执行当前,DiscardOldestPolicy
抛弃队列中等待最久的任务,把当前任务加入队列尝试再次提交当前任务。
-
谁调用找谁,CallerRunsPolicy
将任务退回到调用者,由调用线程处理该任务,从而降低任务流量。
线程池线程工作过程
corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略
核心线程在线程池中一直存活(未使用线程池时无核心线程),当有任务需要执行时,直接使用核心线程执行任务。当任务数量大于核心线程数时,任务加入等待队列。当任务队列数量达到队列最大长度时,继续创建线程,最多达到最大线程数。当设置回收时间,核心线程数以外的空闲线程会被回收。如果达到了最大线程数还不能够满足任务执行需求,则根据拒绝策略做拒绝处理。
(1)当线程数小于核心线程数的时候,使用核心线程数。
(2)如果核心线程数小于需要的线程数,就将多余的线程任务放入任务队列(阻塞队列)中
(3)当任务队列(阻塞队列)满的时候,就创建线程,最多达到最大线程数.
(4)当最大线程数也达到后,就将启动拒绝策略。
线程池的队列
ArrayBlockingQueue
由数组结构组成的有界阻塞队列。
FIFO排序,默认不是公平访问队列。
公平访问队列,指阻塞的所有生产者线程或消费者线程,当队列可用时按照阻塞的先后顺序访问队列。为了保证公平性会降低吞吐 量。ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
LinkedBlockingQueue
由链表结构组成的有界阻塞队列。
FIFO排序,对于生产者端和消费者端分别采用了独立的锁来控制数据同步,在高并发的情况下生产者和消费 者可以并行地操作队列中的数据,提高并发性能。
LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),可能OOM。
PriorityBlockingQueue
支持优先级排序的无界阻塞队列。
元素采取升序排列。可以自定义排序规则,需要注意的是不能保证同优先级元素的顺序。
DelayQueue
使用优先级队列实现的支持延时获取元素的无界阻塞队列。
使用PriorityQueue来实现,队列中的元素必须实 现 Delayed 接口,只有在延迟期满时才 能从队列中提取元素。以下应用场景:
- 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询 DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从 DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的。
SynchronousQueue
不存储元素的阻塞队列。
是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。 SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线 程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给 另外一个线程使用,SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue
LinkedTransferQueue
由链表结构组成的无界阻塞队列。
是一个由链表结构组成的无界阻塞 TransferQueue 队列。相对于其他阻塞队列, LinkedTransferQueue多了 tryTransfer 和transfer方法。
- transfer 方法:如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的 poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如 果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素 被消费者消费了才返回。
- tryTransfer 方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费 者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否 接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
对于带有时间限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传 入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时 还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。
LinkedBlockingDeque
由链表结构组成的双向阻塞队列
是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。 双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其 他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast, peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队 列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的后一个元素。另 外插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同 于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后的方法更清楚。 在初始化 LinkedBlockingDeque 时可以设置容量防止其过渡膨胀。另外双向阻塞队列可以运用在 “工作窃取”模式中。
自己创线程和使用线程池的区别
(1)new Thread 的弊端
- new Thread新建线程对象性能差。
- 线程缺乏统一管理,可能无限制新建线程,导致竞争过大或OOM。
- 缺乏功能,如定时执行、定期执行、线程中断。
(2)Java提供的四种线程池相比new Thread的优势
- 重用存在的线程,减少对象创建、消亡的开销,提高系统资源的使用率。
- 可有效控制最大并发线程数,同时避免过多资源竞争和堵塞。
- 提供定时执行、定期执行、线程并发数的控制等功能。
怎么理解线程池?
程序的运行就是占用系统的资源,多线程的环境下,每次创建和释放线程都十分的消耗资源。
使用一个容器来管理线程,让线程空闲时不释放,而是放到容器里,需要时不用去创建,而是从容器中拿去,这样就大大减少了资源的消耗,这就是池化技术的概念。
池化技术常见的应用还有:jdbc 连接池,内存池,对象池,常量池。
池化技术的好处:
- 降低资源的消耗
- 提高响应的速度(没有创建和销毁过程)
- 方便管理。(线程复用,控制最大并发数,管理线程使用)
池化技术的容器一般都选择队列,队列有先进先出(FIFO)的特性,线程池具有并发的限制,故要选择阻塞队列 BlockingQueue。
BlockingQueue 下有 ArrayBlockingQueue,LinkedBlockQueue,LinkedBlockingDeque(双向),ConcurrentLinkedQueue 等。
怎么理解数据库连接池?为了解决什么问题?
普通的JDBC数据库连接
每次操作数据库都要向数据库建立连接,执行完成后再断开连接,这会消耗大量的资源和时间。数据库的连接资源并没有得到很好的重复利用。若同时有几百人甚至几千人在线,频繁地进行数据库连接将占用系统资源,甚至造成服务器崩溃。
每次数据库连接使用完后都要断开,否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。
这样不能控制连接对象数,系统资源分配缺乏管理,如连接过多,也可能导致内存泄漏,服务器崩溃。
数据库连接池
数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
数据库连接池技术的优点:
资源重用:
由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增加了系统运行环境的平稳性。
更快的系统反应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间。
新的资源分配手段
对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源。
统一的连接管理,避免数据库连接泄露
在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄露。