线程池实现原理
为什么要使用线程池?
提高程序的执行效率
JVM在HotSpot的线程模型下,Java线程会一对一映射为内核线程,因此Java每次创建和回收线程都会去线程创建回收。创建新线程还涉及到用户态和内核态的切换,最后就有可能导致大量时间都耗费在创建和销毁线程上,因而比较浪费时间,系统效率很低
而线程池里的每一个线程任务结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用,因而借助线程池可以提高程序的执行效率
控制线程的数量,防止程序崩溃
如果不加限制地创建和启动线程很容易造成程序崩溃,比如高并发1000W个线程,JVM就需要有保存1000W个线程的空间,这样极易出现内存溢出
线程池中线程数量是一定的,可以有效避免出现内存溢出
使用线程池的风险
死锁
任何多线程应用程序都有死锁风险。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。
虽然任何多线程程序中都有死锁的风险,但线程池却引入了另一种死锁可能:所有池线程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。也就是说,线程池里的线程等待等待队列里的线程执行,等待队列里的等待线程池里的完成,在相互等待造成死锁。
资源不足
如果线程池中的线程数目非常多,这些线程会消耗包括内存和其他系统资源在内的大量资源,从而严重影响系统性能。
并发错误
线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心。而最好使用现有的、已经知道能工作的实现,例如 util.concurrent 包。
线程泄露
线程池中一个比较严重的风险是线程泄漏,主要有两种情况会导致线程泄露。
1.当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时。
当线程池中的线程执行任务抛出一个 RuntimeException 或一个 Error 时,如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生发生多次后,将没有可用的线程来处理任务,等于线程池为空。
2.有些池中的任务可能在永远等待某些资源或来自用户的输入,当被遗忘时,此类任务会永久停止,此时,同样会引起线程泄漏问题。
如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间
线程过载
当工作队列中有大量排队等候执行的任务时,这些任务本身可能会消耗太多的系统资源而引起系统资源缺乏。比如:向服务器发送大量请求。因此,服务器应根据系统的承载能力,限制客户并发连接的数目。当客户并发连接的数目超过了限制值,服务器可以拒绝连接请求,并友好地告知客户:服务器正忙,请稍后再试。
JUC包内部架构
https://blog.youkuaiyun.com/qq_26012495/article/details/84325445?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159975502519725264626075%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=159975502519725264626075&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v3~pc_rank_v4-1-84325445.first_rank_ecpm_v3_pc_rank_v4&utm_term=ThreadPoolExecutor类&spm=1018.2118.3001.4187
上述图片可以说很清晰概括出了JUC内部接口和类的关系,接下来我们自上而下一一说明。
Executor
Executor是java.util.concurrent包下的一个线程池的鼻祖接口,里面只声明了一个execute(Runnable command)方法,返回值为void,这个方法顾名思义就是用来执行传进去的任务的。
ExcutorService
ExcutorService接口继承了Executor接口,并且额外声明了一些方法:shutdown()、submit(Callable task)等,shutdown()返回值为void,用于关闭线程池(不再接受新的,但是线程池中仍存在的还会继续运行),submit()的参数Callable与Runnable类似,也是创建线程的一种方式,需要实现其call()方法,方法有返回值,且可以抛出异常,返回值为Future类型(Future也是一个接口,表示一个可能还没有完成的异步任务的结果)
AbstractExecutorService
AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中所有方法。
ScheduledExecutorService
ScheduledExecutorService除了实现ExecutorService中基本方法外,新增了定时任务处理的功能。
ThreadPoolExecutor
ThreadPoolExecutor继承了类AbstractExecutorService,是我们最常使用的线程池类。
重要参数
corePoolSize 核心线程数量
maximumPoolSize 最大线程数量
keepAliveTime 线程空余时间
workQueue 阻塞队列
handler 任务拒绝策略
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是ScheduledExecutorService的默认实现类。
Executors
Executors是一个辅助类,用以创建此包中所定义的 Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类的工厂和实用方法。包含4种创建线程池的方法newFixedThreadPool(int corePoolSize)、newSingleThreadExecutor、newCachedThreadPool和newScheduledThreadPool(int corePoolSize)
Executors与ThreadPoolExecutor的区别
- ThreadPoolExecutor
- 可以灵活的自定义的创建线程池,可定制性很高
- 想创建好一个合适的线程池比较难
- 使用较为麻烦
- 实际中很少使用
- Executors
- 可以创建4种线程池,这四种线程池基本上已经包含了所有需求,将来根据业务特点选用就好
- 使用非常简单
- 实际中很常用
Executors可以创建的四种线程池简介
- newFixedThreadPool(int corePoolSize)
- 创建一个线程数固定(corePoolSize==maximumPoolSize)的线程池
- 核心线程会一直运行,到了线程池最大容量后,如果有任务完成让出占用线程,此线程就会一直处于等待状态,不会消亡
- 如果一个核心线程出现异常,在提交任务时会新创建一个线程
- 无界队列 LinkedBlockingQueue(当大量任务超过线程池最大容量需要处理时,队列无线增大,会使服务器资源迅速耗尽)
- newSingleThreadExecutor
- 创建一个线程数固定(corePoolSize==maximumPoolSize==1)的线程池
- 核心线程会一直运行
- 无界队列LinkedBlockingQueue
- 所有task都是串行执行的(被提交任务按优先级依次执行,且同一时刻只有一个任务在执行)
- newCachedThreadPool
- corePoolSize==0(线程数量不确定,只要有空闲线程空闲时间超过keepAliveTime,就会终止,执行新任务,先使用空闲线程,若不够,再新建线程。)
- maximumPoolSize==Integer.MAX_VALUE(线程池没有最大线程数量限制,所以当大量线程蜂拥而至,会造成资源耗尽)
- 队列:SynchronousQueue
- newScheduledThreadPool(int corePoolSize)
- 长度为输入参数自定义
- 支持定时周期任务执行,可根据时间需要在指定时间对线程进行调度
线程池实现原理
线程池的状态
RUNNING:接受新任务并处理排队的任务
SHUTDOWN:不接受新任务,但处理排队的任务
STOP:不接受新任务,不处理排队的任务,中断正在进行的任务
TIDYING:所有任务都已终止,WorkerCount为零,线程转换为状态整理将运行终止的()hook方法
TERMINATED:已终止()已完成
线程池中线程初始化
默认情况下线程池在创建时是不会创建线程的,如果需要初始化一个或所有核心线程,
将调用addWorker ()方法,该方法是用来创建,运行,清理Workers,过程示意图如下
任务缓存队列
Java并发包中的阻塞队列一共是7个,它们都是线程安全的。
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
,基于数组的先进先出,创建时必须指定大小,超出直接corePoolSize个任务,则加入到该队列中,只能加该queue设置的大小,其余的任务则创建线程,直到(corePoolSize+新建线程)> maximumPoolSize。 - LinkedBlockingQueue:一个由链表结构组成的无界阻塞队列。基于链表的先进先出,无界队列。超出直接corePoolSize个任务,则加入到该队列中,直到资源耗尽。
- synchronousQueue:一个不存储元素的阻塞队列。这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DealyQueue:一个使用优先级队列实现的无界阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
任务排队策略
- 直接提交
工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
- 无界队列
使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
- 有界队列
当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
任务拒绝策略
-
ThreadPoolExecutor.AbortPolicy:直接丢弃任务,并抛出RejectedExecutionException异常。
-
ThreadPoolExecutor.CallerRunsPolicy:在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
-
ThreadPoolExecutor.DiscardPolicy:直接丢弃任务,什么都不做,不会抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy:抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。
线程池的关闭
线程池的关闭有两种方法:
-
shutdown:提供一种有序的关机,会等待当前缓存队列任务全部执行完成才会关闭,但不会再接收新的任务(相对较优雅)。
-
shutdownNow:会立即关闭线程池,会打断正在执行的任务并且会清空缓存队列中的任务,返回的是尚未执行的任务。
线程池容量的动态调整
-
setCorePoolSize:顾名思义就是来设置corePoolSize大小的但是值得注意的是如果设置值小于当前值那么多余的线程将变的无所事事,如果当前设置值大于新的值,那么新的线程会去执行缓存队列中的任务。
-
setMaximumPoolSize:设置当前线程池的maximumPoolSize,设置的值如果小于旧值时超出的线程将在下一次空闲时终止。