Java线程池详解
Java线程池解析
在Java中有两种方式创建线程池,一种是直接使用Executors工具类创建预先定义好的线程池。一共有以下四种线程池
-
newCachedThreadPool:可缓存的无边界的线程池,最大线程数Integer.MAX_VALUE
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } -
newFixedThreadPool:定长的线程池,超出则等待,线程不可扩容
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } -
newScheduledThreadPool:定长的线程池,支持定时及周期性任务的执行。最大线程数Integer.MAX_VALUE。
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); } -
newSingleThreadExecutor:单线程的线程池,保证所有任务FIFO执行。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
可以看到这四个线程池中涉及到了三个阻塞队列,根据阻塞队列的不同,线程池也提供了不一样的功能
-
SynchronousQueue:这是一个无缓冲的队列,意思是不会存储元素,每个插入操作必须等待移除操作完成,它没有容量的概念,因此被称作“传递性”队列。当线程池中不允许任务等待,需要立刻创建新线程执行时使用。newCachedThreadPool使用了这个队列,所以在newCachedThreadPool中,如果线程没有空闲的,会立刻创建新线程来执行新任务。
-
LinkedBlockingQueue:基于链表的阻塞队列,按照FIFO的原则对元素进行排序,内部使用锁机制来保证多线程的安全性,可以设置容量大小。
-
DelayedWorkQueue:支持延时获取元素的阻塞队列,是一个基于堆的阻塞队列,插入删除的效率都是logN,可以理解为优先级队列,可以为每个元素设置到期时间,只有延迟时间到了,才能取走。适用于那些需要延迟处理的任务,如缓存失效、任务调度等。
讲完了内置的预定义的四个线程池,但是一般开发中不会使用这四个线程池,因为内置的不可变参数可能导致程序产生问题,例如线程数无限增加,一般来说我们希望定义一个任何操作都是可控的线程池,所以一般使用ThreadPoolExecutor进行线程池的创建。
使用ThreadPoolExecutor创建线程池
使用ThreadPoolExecutor创建线程池需要指定7个参数
-
核心线程数corePoolSize
-
最大线程数maximumPoolSize
-
线程生存时间keepAliveTime
-
线程生存时间的单位TimeUnit
-
阻塞队列BlockingQueue
-
线程工厂ThreadFactory
-
拒绝策略RejectedExecutionHandler
前面四个参数顾名思义,这里主要讲一下后面三个参数
阻塞队列
Java常用的阻塞队列一般有八个
ArrayBlockingQueue:基于数组的阻塞队列,需要初始化大小,因此是有界的。
LinkedBlockingQueue:基于链表的阻塞队列,最大长度MAX_VALUE,因此可以理解为无解的,可以指定大小。(常用)内部使用锁保证多线程安全性。
PriorityBlockingQueue:优先级队列,可以自定义比较器
DelayQueue:延迟队列,也可以理解为优先级队列
DelayedWorkQueue:实现和DelayQueue一样,不过是将优先级队列和DelayQueue的实现过程迁移到本身方法体中,从而可以在该过程当中灵活的加入定时任务特有的方法调用。可以理解为DelayQueue增强版。
SynchronouseQueue:上面说的“传递性”队列
LinkedTransferQueue:实现了一个重要功能:如果有等待的空闲线程,put操作直接将任务传递给线程,不会入队,一定程度上实现了插队的功能。可以理解为 LinkedBolckingQueue
和 SynchronousQueue
和合体。
LinkedBlockingDeque:顾名思义,是一个双向链表的队列,支持FIFIO和FILO操作,可以在队头和队尾同时操作。
线程工厂
线程工程是一个接口,内容很简单:收一个Runnable对象,并将其封装到Thread对象中,进行执行。
public interface ThreadFactory { /** * Constructs a new {@code Thread}. Implementations may also initialize * priority, name, daemon status, {@code ThreadGroup}, etc. * * @param r a runnable to be executed by new thread instance * @return constructed thread, or {@code null} if the request to * create a thread is rejected */ Thread newThread(Runnable r); }
因此我们可以自定义一个线程工厂,实现这个接口就行
public class MyFactory implements ThreadFactory{ @Override public Thread newThread(Runnable r) { return new Thread(r); } }
实际使用过程中可以添加自定义功能:例如线程名称、线程计数、线程总数控制等。
拒绝策略
拒绝策略是指,如果最大线程数到了并且等待队列满了之后,新到的任务应该怎么处理,JUC中定义了四种拒绝策略,如下:
-
AbortPolicy
:拒绝并抛出异常 -
CallerRunsPolicy
:拒绝并且调用者运行策略,谁给我的谁运行 -
DiscardOldestPolicy
:抛弃最早未处理任务 -
DiscardPolicy
:直接丢弃任务,啥也不管
讲完了线程池的概述,我们来讲一下线程池的运行逻辑
运行逻辑
运行逻辑是指一个新的任务提交,应该怎么做?
-
是否达到核心线程数?
-
没有达到则创建一个新的线程执行任务
-
注意:如果没有达到核心线程数,但是有空闲线程,是创建新的还是复用旧的?答案是创建新的线程。
-
-
是否队列已满?
- 没有满就加入工作队列中
-
是否达到最大线程数?
-
没有达到最大线程数,就会创建新线程执行任务
-
达到了就进入拒绝策略
-
也就是说,判断顺序是:是否大于核心线程数---队列是否已满---是否大于最大线程数
关于线程池的一些问题?
-
如果队列中的任务失效了怎么处理?
-
使用Future.cancel是可以取消一个任务的,原理是向正在运行任务的线程发送中断指令,即Interrupt方法。
-
如果任务没有开始执行,会仍然存在队列中,直到被拉取,拉取时发现Interrupt后不会执行。
-
如果任务正在运行切支持响应中断,会抛出
InterruptedException
并提前中止。
-
-
如果线程执行过程中异常停止了怎么办?线程池怎么处理异常?
首先提交任务可以使用execute方法或者submit方法
-
execute方法不会抛出异常给调用者,异常会被吞掉,但是可以重新uncaughtException方法进行异常主动处理。并且线程池内部会打印异常信息,这里指的被吞掉是指调用者无法感知。
-
submit方法会返回一个Future对象,抛出的异常会被封装在内,使用get获取返回值的时候会抛出异常。出现异常之后线程池也不会打印异常信息。
-
其次一个线程出现异常后,这个线程会被销毁,创建一个新的干净的线程放到线程池中。销毁线程是为了防止旧的线程中有脏数据影响新任务执行。
-
如何优雅的关闭一个线程池?
首先调用shutdown方法发送关闭指令,启动一个有序的关闭过程,已经提交的任务会继续执行,但是不会接收新的任务。
其次等待一定的可接受的时间threadPool.awaitTermination(为了给正在运行的任务时间),超时之后调用shutdownNow方法,强行退出,退出原理是给线程打上中断,但是中断不意味着能终止。
然后再等待一定的时间,还没有结束就做失败处理。。。
-
线程池是怎么知道线程完成了任务空闲了?
这个问题其实可以转变一下思路,改成
线程是怎么实现复用的?空闲就是线程复用呗
线程在线程池内部其实封装成了一个worker对象
Worker extends AbstractQueuedSynchronizer 可以看到继承了AQS,所以Worker自身是具有锁的特性的。在创建 Worker 对象的时候,会把线程和任务一起封装到 Worker 内部,然后调用 runWorker 方法来让线程执行任务
实现线程复用的逻辑就在runWorker方法中,由于使用了while循环,当第一个任务执行完成后,会不断通过getTask获取任务,只要能获取到任务,就会调用run方法执行任务。
并且getTask内部也是一个死循环,除非被一些原因退出(生存时间到、线程池关闭等)
如果获取不到getTask退出,就会执行finally中的processWorkerExit方法,将线程退出。
因为 Worker 继承了 AQS,每次在执行任务之前都会调用 Worker 的 lock 方法,执行完任务之后,会调用 unlock 方法,这样做的目的就可以通过 Woker 的加锁状态判断出当前线程是否正在执行任务。
如果想知道线程是否空闲,只需要调用Worker的tryLock方法,加锁成功就是空闲。
final void runWorker(Worker w) { // 获取当前工作线程 Thread wt = Thread.currentThread(); // 从 Worker 中取出第一个任务 Runnable task = w.firstTask; w.firstTask = null; // 解锁 Worker(允许中断) w.unlock(); boolean completedAbruptly = true; try { // 当有任务需要执行或者能够从任务队列中获取到任务时,工作线程就会持续运行 while (task != null || (task = getTask()) != null) { // 锁定 Worker,确保在执行任务期间不会被其他线程干扰 w.lock(); // 如果线程池正在停止,并确保线程已经中断 // 如果线程没有中断并且线程池已经达到停止状态,中断线程 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { // 在执行任务之前,可以插入一些自定义的操作 beforeExecute(wt, task); Throwable thrown = null; try { // 实际执行任务 task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { // 执行任务后,可以插入一些自定义的操作 afterExecute(task, thrown); } } finally { // 清空任务,并更新完成任务的计数 task = null; w.completedTasks++; // 解锁 Worker w.unlock(); } } completedAbruptly = false; } finally { // 工作线程退出的后续处理 processWorkerExit(w, completedAbruptly); } } -
如何设置线程池的核心线程数和最大线程数?
-
线程池怎么优化?
-
如何自己实现一个线程池?
-
什么是线程池?为什么使用线程池?
-
什么是线程池的预热机制?
-
线程池的优点和缺点是什么?
-
线程池中的线程是怎么执行任务的?
-
线程池中的任务可以返回执行结果吗?
-
线程池的内存泄漏问题?
-
在多线程的环境下,怎么保证线程池中的任务按照特定的顺序执行?
-
日常工作中有用到线程池吗?什么是线程池?为什么要使用线程池?
-
ThreadPoolExecutor使用到了哪些锁?为什么要使用锁?
-
线程池运行过程中怎么监控?
-
execute提交任务和submit提交任务有什么不同?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
2020-08-24 【Java数据结构】循环队列的数组实现
2020-08-24 【Java数据结构】普通矩阵与稀疏矩阵的互相转化,稀疏矩阵的物理存储