资讯动态

深入解析Java线程池工作原理

发布时间:2026/8/17 19:11:17 来源:尧图企业网站定制
好, 终于抵达 Java 的线程池了, 这属于 Java 并发编程里极为关键的一项内容, 今天我们借助图文的形式来全面透彻地弄明白线程池的工作原理, 以及在实际项目当中应当怎样去自定义契合业务的线程池。线程池实际上是一种采用池化的技术达成, 池化技术的关键理念正是达成资源的再度利用, 防止因资源的反复创建以及销毁所引发的性能损耗。线程池能够管理一众线程, 使得线程执行完任务之后不去进行销毁工作, 而是转为持续处理其它线程已然提交的任务。使用线程池的好处Java主要借助构建来打造线程池。紧接着我们瞧瞧线程池是怎样构造而成的。的构造方法将一堆参数传入, 之后开展简单的赋值操作, 这便是线程池构造的实际情况, 其实它是很简单的。表明线程池的核心构造参数已讲完, 随后要阐述这些参数于线程池中怎样开展工作。线程池刚创建出来是什么样子呢如下图正是这样的, 才刚创建出来的线程池当中, 仅仅存在一个于构造的时候传进去的阻塞队列, 此队列里面是没有线程存在的, 要是打算在执行之前将核心线程数创建好, 能够调用ds方法去达成, 其默认状态下是不存在线程的。当有线程通过 方法提交了一个任务会发生什么呢首先, 会进行判断, 判断的内容是, 当前线程池的线程数, 是不是小于核心线程数, 而核心线程数, 是线程池构造的时候, 传入的参数。要是处于小于的状况, 那就径直借助创建一个线程这种方式去执行此项任务, 恰似图示那般。当任务将执行完毕之时, 线程并非会退出, 而是会前往阻塞队列那儿去获取任务, 就如同下面所展示的图片这样。随后要是又递交了一项任务, 同样会依照上述之步骤去判定是否小于核心线程数, 倘若小于, 依旧会创建线程用以执行任务, 执行完毕之后也会从阻塞队列里获取任务。这儿存在一个细节, 即在提交任务之际, 哪怕线程池里的线程于阻塞队列中无法获取到任务, 要是线程池里的线程数量依旧小于核心线程数, 那么仍然会持续创建线程, 而非复用已存在的线程。要是线程池当中的线程数量不再比核心线程数量少, 那会怎样? 这时, 就会试着把任务放置到阻塞队列里, 在入队成功以后, 就跟图里显示的一样。这样阻塞的线程就可以获取到任务了。然而, 即便任务数量持续增多, 以至队列已然满员, 不过任务投放却宣告失败, 那么该如何应对呢?这个时候, 会去判断, 当前线程池之内的线程数量, 是不是小于, 最大线程数, 也就相当于, 入参时候的那个参数, 对不对。要是处于小于最大的线程数这种状态, 那么同样会去创建不属于核心部分的线程, 以此来达成对提交的任务执行操作, 就如同图示那般。因此, 即便队列之中存在任务, 然而新创建出来的线程仍旧会率先去处理这个提交上来的任务, 并非从队列里面获取已有的任务去实施, 由此能够看出, 先提交的任务不见得会先被执行。假如线程数已经达到最大线程数量怎么办呢在这个时候, 就会去执行拒绝策略, 而所谓的拒绝策略, 其实就是在构造线程池的过程当中, 把传入的 dler 对象用于处理这个任务。JDK 自带的 dler 实现有 4 种线程池创建的时候如果不指定拒绝策略就默认是 策略。当然, 你能够自行去实现 dler 接口, 比如说把任务放置在数据库里, 或者是放置在缓存当中, 在采取这样的办法之后, 便能够从数据库那儿, 或者是从缓存那里, 获取到那些被拒绝掉的任务了。到此处, 我们发觉, 线程池构建的若干参数, 我们于上述的执行进程里皆已提及, 然而尚缺两个参数, 和 unitunit 为 的时间单位未提及, 所以 是怎样发挥作用的呢, 此问题留待后续剖析。说完整个执行的流程接下来看看 方法的代码是如何实现的。public void execute(Runnable command) { // 首先检查提交的任务是否为null是的话则抛出NullPointerException。 if (command null) throw new NullPointerException(); // 获取线程池的当前状态ctl是一个AtomicInteger其中包含了线程池状态和工作线程数 int c ctl.get(); // 1. 检查当前运行的工作线程数是否少于核心线程数corePoolSize if (workerCountOf(c) corePoolSize) { // 如果少于核心线程数尝试添加一个新的工作线程来执行提交的任务 // addWorker方法会检查线程池状态和工作线程数并决定是否真的添加新线程 if (addWorker(command, true)) return; // 重新获取线程池的状态因为在尝试添加线程的过程中线程池的状态可能已经发生变化 c ctl.get(); } // 2. 尝试将任务添加到任务队列中 if (isRunning(c) workQueue.offer(command)) { int recheck ctl.get(); // 双重检查线程池的状态 if (! isRunning(recheck) remove(command)) // 如果线程池已经停止从队列中移除任务 reject(command); // 如果线程池正在运行但是工作线程数为0尝试添加一个新的工作线程 else if (workerCountOf(recheck) 0) addWorker(null, false); } // 3. 如果任务队列满了尝试添加一个新的非核心工作线程来执行任务 else if (!addWorker(command, false)) // 如果无法添加新的工作线程可能因为线程池已经停止或者达到最大线程数限制则拒绝任务 reject(command); }再来另画一张图总结一下 的执行流程那个线程池的核心功能, 是达成线程的重复利用, 那么, 线程池究竟是怎样去实现线程的复用的呢线程在线程池内部其实被封装成了一个 对象继承了 AQS也就是具有一定锁的特性。有用于创建线程以执行任务的方法, 上面已提及, 该方法是借助特定方式。于创建特定对象之际, 会将线程以及任务一同融合封装至特定内部, 随后去调用特定方法以使线程能够执行任务, 紧接着我们就要来瞧一瞧特定方法。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); } }正是从这儿, 就能找出线程执行完任务却不退出的缘由了, 其内部是用了while死循环, 在首个任务执行完毕后, 会持续借助方法去获取任务, 只要能够获取到任务, 便会调用run方法进而继续执行任务, 而这正是线程得以复用的主要缘故哇。既然如此, 要是从某个地方没办法获取到相应方法, 那么就会去调用另一个地方里的特定方法, 进而使得线程退出。这里存在着这样一个细节, 那就是, 因继承了AQS, 所以每次在执行任务之前, 都会用以调用的lock方法, 而在执行完任务之后, 就会调用相应方法, 如此这般去做所具备的目的, 便能够借助Woker的加锁的状态, 从而判断出当前线程是不是正在执行任务。要是想晓得线程是不是正在开展任务, 那就仅需去调用Woker的方法, 依据加锁是否成功来做判断, 加锁成功表明当前线程没加锁, 进而就没执行任务, 在调用方法去关闭线程池之际, 便是运用这种方式来判别线程有无在执行任务, 要是没有的话, 会试着去打断没执行任务的线程。之前我们有提及, 线程于执行完任务之后, 会接着从 方法里获取任务, 要是获取不到便会退出, 随后我们就来瞧一瞧 方法的实现。private Runnable getTask() { // 标志表示最后一个poll()操作是否超时 boolean timedOut false; // 无限循环直到获取到任务或决定工作线程应该退出 for (;;) { int c ctl.get(); int rs runStateOf(c); // 如果线程池状态是SHUTDOWN或更高如STOP并且任务队列为空那么工作线程应该减少并退出 if (rs SHUTDOWN (rs STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc workerCountOf(c); // 检查工作线程是否应当在没有任务执行时经过keepAliveTime之后被终止 boolean timed allowCoreThreadTimeOut || wc corePoolSize; // 如果工作线程数超出最大线程数或者超出核心线程数且上一次poll()超时并且队列为空或工作线程数大于1 // 则尝试减少工作线程数 if ((wc maximumPoolSize || (timed timedOut)) (wc 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { // 根据timed标志决定是无限期等待任务还是等待keepAliveTime时间 Runnable r timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : // 指定时间内等待 workQueue.take(); // 无限期等待 if (r ! null) // 成功获取到任务 return r; // 如果poll()超时则设置timedOut标志 timedOut true; } catch (InterruptedException retry) { // 如果在等待任务时线程被中断重置timedOut标志并重新尝试获取任务 timedOut false; } } }前面就是线程池的一些状态判断这里有一行代码boolean timed allowCoreThreadTimeOut || wc corePoolSize;这行代码, 是用以判定, 当前前来获取任务的线程, 是否能够超时退出。要是ut被设置成true, 或者线程池当下的线程数量, 比核心线程数大, 也就是如此, 那么该获取任务的线程, 便能够超时退出。怎么做到超时退出呢就是这行核心代码Runnable r timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();会依据是否允许时限超出来抉择调用阻塞队列中的poll方法或者take方法, 要是允许时限超出, 那就调用poll方法, 传入的是, 也就是在构造线程池时所传入的空闲的时长, 此方法的含义是从队列里利用阻塞这种方式历经规定的时长去获取任务, 若获取不到便会返回null, 若不允许时限超出, 就会调用take方法, 该方法会持续处于阻塞状态去获取任务, 直至从队列当中获取到任务才会停止。从这里就可以看到 是如何使用的了。所以, 到达此地, 诸位理应明晰, 线程池里头的线程, 基于何种道理, 能够实现处于空闲既定时间段之时方行退出, 对?其实最为关键的要点便是运用了阻塞队列的poll方法, 此方法能够设定超时时长, 一旦线程达到该时长仍未获取到任务, 进而就会返回null, 一旦该方法返回null, 那么线程便会退出。这儿存在着一个细节, 那便是在判定当前获取任务的线程能不能超时退出之际, 要是把ut设定为true, 如此一来所有线程走到这个timed均会是true, 包含核心线程在内的所有线程都能够实现超时退出, 要是线程池打算让核心线程超时退出, 能够借助ut方法把ut变量设定为true。整个 方法以及线程超时退出的机制如图所示线程池内部有 5 个常量来代表线程池的五种状态private static final int RUNNING -1 COUNT_BITS; private static final int SHUTDOWN 0 COUNT_BITS; private static final int STOP 1 COUNT_BITS; private static final int TIDYING 2 COUNT_BITS; private static final int TERMINATED 3 COUNT_BITS;线程池的有关具体状态, 是于ctl成员变量里存在着的, ctl之中不光储存有线程池的状态, 还储存有当前线程池当中线程数的大小。private final AtomicInteger ctl new AtomicInteger(ctlOf(RUNNING, 0));最后画个图来总结一下这 5 种状态的流转事实上, 于线程池运作进程里, 绝大多数的操作在执行以前都必须去判定当前线程池处于哪样的状态, 进而再去决定是不是继续执行此项操作。线程池提供了 和 两个方法来关闭线程池。方法/** * 启动一次顺序关闭在这次关闭中执行器不再接受新任务但会继续处理队列中的已存在任务。 * 当所有任务都完成后线程池中的线程会逐渐退出。 */ public void shutdown() { final ReentrantLock mainLock this.mainLock; // ThreadPoolExecutor的主锁 mainLock.lock(); // 加锁以确保独占访问 try { checkShutdownAccess(); // 检查是否有关闭的权限 advanceRunState(SHUTDOWN); // 将执行器的状态更新为SHUTDOWN interruptIdleWorkers(); // 中断所有闲置的工作线程 onShutdown(); // ScheduledThreadPoolExecutor中的挂钩方法可供子类重写以进行额外操作 } finally { mainLock.unlock(); // 无论try块如何退出都要释放锁 } tryTerminate(); // 如果条件允许尝试终止执行器 }首先是把线程池的状态予以修改, 接着去尝试打断处于空闲状态的线程, 而怎样判断其处于空闲状态呢, 上面在讲述继承AQS的时候已然提及, 这里所说的处于空闲状态的线程, 指的就是正在阻塞等待任务的线程。方法/** * 尝试停止所有正在执行的任务停止处理等待的任务 * 并返回等待处理的任务列表。 * * return 从未开始执行的任务列表 */ public ListRunnable shutdownNow() { ListRunnable tasks; // 用于存储未执行的任务的列表 final ReentrantLock mainLock this.mainLock; // ThreadPoolExecutor的主锁 mainLock.lock(); // 加锁以确保独占访问 try { checkShutdownAccess(); // 检查是否有关闭的权限 advanceRunState(STOP); // 将执行器的状态更新为STOP interruptWorkers(); // 中断所有工作线程 tasks drainQueue(); // 清空队列并将结果放入任务列表中 } finally { mainLock.unlock(); // 无论try块如何退出都要释放锁 } tryTerminate(); // 如果条件允许尝试终止执行器 return tasks; // 返回队列中未被执行的任务列表 }转而把线程池的状态变更成 STOP, 接着试着去打断全部的线程, 再从阻塞队列里移除余下的任务, 这同样是致使不能执行剩余任务的缘由所在。因此同样能够看得出, 一种方法与另一种方法的主要差异在于, 前者在某情况之后还能够对处于队列里的任务予以处理, 而后者则是直接把任务从队列之中移除, 进而导致线程池里的线程不再对其进行处理了。于项目里运用线程池之际, 通常得对线程池予以监控, 以便在出现问题之时能够迅速定位线程池自身给出了一些办法来获取线程池的运行状态。线程池除了提供上述已然实现的那些方法之外, 同时还预留了诸多扩展方法比如说在某方法当中, 于执行任务之前会去回调另一方法, 在执行任务之后又会回调另外的某方法, 而这些方法默认情况下都是空实现, 小伙伴们能够自行继承某类来对这些方法进行重写, 进而实现自身想要达成的功能。在Java程序当中, 实际上常常需要借助多线程去处理某些业务, 然而并不建议仅仅通过继承, 或者是实现接口去缔造线程, 如此一来会致使频繁地创建以及销毁线程, 与此同时, 创建过多的线程也有可能诱发资源耗尽的风险。于是乎, 采用线程池乃是一种更具合理性的抉择方式便于对任务予以管理控制。并且呀 能达成线程的重复性利用目的。那么呢 线程池一般而言适宜于那些有异步需求或者需多线程处理任务的情景场合。以下是几个线程池使用场景的简单示例模拟一个简单的Web服务器接受请求并使用线程池进行处理。import java.util.concurrent.*; public class SimpleWebServer { private static final int NTHREADS 100; private static final ExecutorService exec Executors.newFixedThreadPool(NTHREADS); public static void main(String[] args) { while (true) { // 接收请求 Runnable request new Runnable() { public void run() { // 处理请求 System.out.println(Request handled by Thread.currentThread().getName()); } }; exec.execute(request); } } }使用线程池进行并行的数值计算。import java.util.concurrent.*; public class ParallelCalculation { private static final int NTHREADS 4; private static final ExecutorService exec Executors.newFixedThreadPool(NTHREADS); public static void main(String[] args) { CallableDouble task new CallableDouble() { Override public Double call() { // 这里模拟一些数值计算 return Math.random() * 100; } }; ListFutureDouble results new ArrayList(); for (int i 0; i 10; i) { results.add(exec.submit(task)); } for (FutureDouble result : results) { try { System.out.println(result.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } exec.shutdown(); } }模拟处理异步任务。import java.util.concurrent.*; public class AsynchronousTaskProcessor { private static final ExecutorService exec Executors.newCachedThreadPool(); public static void main(String[] args) { exec.execute(() - { // 执行某些异步任务 System.out.println(Async task started); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Async task completed); }); System.out.println(Main thread continues to execute other operations.); exec.shutdown(); } }居于上方的那个示例里头, 我们运用了 JDK 内部所给予的工具类, 以此来迅速创建线程池。1固定线程数量的线程池核心线程数与最大线程数相等public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable()); }2单个线程数量的线程池public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable())); }3接近无限大线程数量的线程池public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueueRunnable()); }4带定时调度功能的线程池public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }尽管 JDK 预备了能让线程池得以快速创建的办法, 但其实并不建议借助该办法去创建线程池, 原因在于从上面构造线程池所呈现的代码能够瞧出, 这种自定义线程池由于采用了特定方式, 其队列的基本容量在默认状态下表现为无限大, 于实际运用当中若是出现任务数量过多的状况极可能致使内存耗竭而溢出还有另一种线程池, 鉴于其核心线程数具备无尽的特性, 在任务数量过多之际会造成大量线程被创建出来, 如此一来很有可能因机器负载过高而使得服务出现故障导致宕机现象的发生。这也是面试常问的一道八股文大家需要注意。依照上面所分析指出的, 借助这个工具类去创建的线程池, 实际上全都没法契合实际的使用情形, 那么在实际的项目里面, 究竟该怎么去构造线程池, 又该如何合理地设置参数?线程数的设置, 主要是依据业务来决定, 业务存在两种类型, 一种是IO密集型, 另一种是CPU密集型。CPU 密集型, 其指的是任务主要借助来开展大量的计算, 不存在致使线程阻塞的情况。通常而言, 这种情形下的线程数设定为 CPU 核心数加上 1。输入输出密集型: 执行任务之时, 若是需要大量的输入输出操作, 像磁盘输入输出, 网络输入输出这些的话, 或许就会存在大量的阻塞情况因而在输入输出密集型任务当中, 运用多线程能够极大地加快任务的处理速度。通常线程数量设定为二乘以中央处理器核心数量。在Java里头, 被用来获取CPU核心数的那种方法是, .(.).()。通常来讲, 建议自行定义线程工厂, 在构建线程之际, 对线程的名称予以设置, 如此一来, 于查询日志之时, 便能够便捷地知晓究竟是哪一个线程执行了代码。通常而言, 是需要去设置有界队列的大小这种情况的, 举例来说, 在进行构造这个动作的时候, 能够通过传入参数的方式来对队列之中任务数据的大小予以限制, 如此一来, 便不会由于毫无限制地朝着队列里面扔任务这个行为, 进而导致系统出现oom这种状况了。OK我们来通过自定义 改造一下前面使用 的例子。Web服务器常常要处理I/O操作, 像网络I/O这类, 所以它们被视作I/O密集型任务。因而, 我们把线程数设定为2乘以CPU核心数。import java.util.concurrent.*; public class SimpleWebServer { private static final int CPU_COUNT Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE 2 * CPU_COUNT; private static final int MAX_POOL_SIZE 2 * CPU_COUNT 1; private static final ThreadPoolExecutor exec new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000) ); public static void main(String[] args) { while (true) { Runnable request () - System.out.println(Request handled by Thread.currentThread().getName()); exec.execute(request); } } }用于计算的并行计算任务, 不存在I/O阻塞情况, 故而它们属于CPU密集型, 线程数设定为CPU核心数加1。import java.util.*; import java.util.concurrent.*; public class ParallelCalculation { private static final int CPU_COUNT Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE CPU_COUNT 1; private static final int MAX_POOL_SIZE CPU_COUNT * 2; private static final ThreadPoolExecutor exec new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000) ); public static void main(String[] args) { CallableDouble task () - Math.random() * 100; ListFutureDouble results new ArrayList(); for (int i 0; i 10; i) { results.add(exec.submit(task)); } for (FutureDouble result : results) { try { System.out.println(result.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } exec.shutdown(); } }异步任务常常涉及到I/O操作, 像数据库查询或者文件读写这般, 所以它们被看作是I/O密集型任务。因而, 我们把线程数设定为2乘以CPU核心数。import java.util.concurrent.*; public class AsynchronousTaskProcessor { private static final int CPU_COUNT Runtime.getRuntime().availableProcessors(); private static final int CORE_POOL_SIZE 2 * CPU_COUNT; private static final int MAX_POOL_SIZE 2 * CPU_COUNT 2; private static final ThreadPoolExecutor exec new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000) ); public static void main(String[] args) { exec.execute(() - { System.out.println(Async task started); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Async task completed); }); System.out.println(Main thread continues to execute other operations.); exec.shutdown(); } }本文着重讲了线程池的原理同其使用场景, 线程池主要借由阻塞队列达成, 线程池的使用场景主要是异步情形或者多线程处理任务的状况, 线程池的使用能够经由 来迅速创建, 然而不建议采用, 因为 创建的线程池皆存在一些缺陷像无界队列也许致使内存溢出, 无限大的线程数可能使得机器负载过重, 所以在实际项目里, 建议自行定义线程池 , 依据业务场景去合理地设置线程数、队列大小等参数。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价