美团二面:聊聊线程池设计与原理,由表及里趣味解析

您所在的位置:网站首页 电脑版免费大型游戏有哪些游戏 美团二面:聊聊线程池设计与原理,由表及里趣味解析

美团二面:聊聊线程池设计与原理,由表及里趣味解析

2023-04-06 16:31| 来源: 网络整理| 查看: 265

关于线程池,无论是在实际的项目开发还是面试,它都是并发编程中当之无愧的重中之重。因此,掌握线程池是每个Java开发者的必备技能。

本文将从线程池的应用场景和设计原理出发,先带大家手撸一个线程池,在理解线程池的内部构造后,再深入剖析Java中的线程池。全文大约2.5万字,篇幅较长,在阅读时建议先看目录再看内容

一、为什么要使用线程池

在前面系列文章的学习中,你已然知道多线程可以加速任务的处理、提高系统的吞吐量。那么,是否我们因此就可以频繁地创建新的线程呢?答案是否定的。频繁地繁创建和启用新的线程不仅代价昂贵,而且无限增加的线程势必也会造成管理成本的急剧上升。因此,为了平衡多线程的收益和成本,线程池诞生了

1. 线程池的使用场景

生产者与消费者问题是线程池的典型应用场景。当你有源源不断的任务需要处理时,为了提高任务的处理速度,你需要创建多个线程。那么,问题来了,如何管理这些任务和多线程呢?答案是:线程池

线程池的池化(Pooling)原理的应用并不局限于Java中,在MySQL和诸多的分布式中间件系统中都有着广泛的应用。当我们链接数据库的时候,对链接的管理用的是线程池;当我们使用Tomcat时,对请求链接的管理用的也是线程池。所以,当你有批量的任务需要多线程处理时,那么基本上你就需要使用线程池

2. 线程池的使用好处

线程池的好处主要体现在三个方面:系统资源任务处理速度相关的复杂度管理,主要表现在:

降低系统的资源开销:通过复用线程池中的工作线程,避免频繁创建新的线程,可以有效降低系统资源的开销;提高任务的执行速度:新任务达到时,无需创建新的线程,直接将任务交由已经存在的线程进行处理,可以有效提高任务的执行速度;有效管理任务和工作线程:线程池内提供了任务管理和工作线程管理的机制。

为什么说创建线程是昂贵的

现在你已经知道,频繁地创建新线程需要付出额外的代价,所以我们使用了线程池。那么,创建一个新的线程的代价究竟是怎样的呢?可以参考以下几点:

创建线程时,JVM必须为线程堆栈分配和初始化一大块内存。每个线程方法的调用栈帧都会存储到这里,包括局部变量、返回值和常量池等;在创建和注册本机线程时,需要和宿主机发生系统调用;需要创建、初始化描述符,并将其添加到 JVM 内部数据结构中。

另外,从某种意义上说,只要线程还活着,它就会占用资源,这不仅昂贵,而且浪费。 例如 ,线程堆栈、访问堆栈的可达对象、JVM 线程描述符、操作系统本机线程描述符等等,在线程活着的时候,这些资源都会持续占据。

虽然不同的Java平台在创建线程时的代价可能有所差异,但总体来说,都不便宜。

3. 线程池的核心组成

一个完整的线程池,应该包含以下几个核心部分:

任务提交:提供接口接收任务的提交;任务管理:选择合适的队列对提交的任务进行管理,包括对拒绝策略的设置;任务执行:由工作线程来执行提交的任务;线程池管理:包括基本参数设置、任务监控、工作线程管理等。

二、如何手工制作线程池

通过第一部分的阅读,现在你已经了解了线程池的作用及它的核心组成。为了更深刻地理解线程池的组成,在这一部分我们通过简单的四步来手工制作一个简单的线程池。当然,麻雀虽小,五脏俱全。如果你能手工自制线程池之后,那么在理解后续的Java中的线程池时,将会易如反掌。

1. 线程池设计和制作

第一步:定义一个王者线程池:TheKingThreadPool,它是这次手工制作中名副其实的主角儿。在这个线程池中,包含了任务队列管理、工作线程管理,并提供了可以指定队列类型的构造参数,以及任务提交入口和线程池关闭接口。你看,虽然它看起来似乎很迷你,但是线程池的核心组件都已经具备了,甚至在它的基础上,你完全可以把它扩展成更为成熟的线程池。

/** * 王者线程池 */ public class TheKingThreadPool { private final BlockingQueue taskQueue; private final List workers = new ArrayList(); private ThreadPoolStatus status; /** * 初始化构建线程池 * * @param worksNumber 线程池中的工作线程数量 * @param taskQueue 任务队列 */ public TheKingThreadPool(int worksNumber, BlockingQueue taskQueue) { this.taskQueue = taskQueue; status = ThreadPoolStatus.RUNNING; for (int i = 0; i < worksNumber; i++) { workers.add(new Worker("Worker" + i, taskQueue)); } for (Worker worker : workers) { Thread workThread = new Thread(worker); workThread.setName(worker.getName()); workThread.start(); } } /** * 提交任务 * * @param task 待执行的任务 */ public synchronized void execute(Task task) { if (!this.status.isRunning()) { throw new IllegalStateException("线程池非运行状态,停止接单啦~"); } this.taskQueue.offer(task); } /** * 等待所有任务执行结束 */ public synchronized void waitUntilAllTasksFinished() { while (this.taskQueue.size() > 0) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 关闭线程池 */ public synchronized void shutdown() { this.status = ThreadPoolStatus.SHUTDOWN; } /** * 停止线程池 */ public synchronized void stop() { this.status = ThreadPoolStatus.SHUTDOWN; for (Worker worker : workers) { worker.doStop(); } } }

第二步:设计并制作工作线程。工作线程是干活的线程,将负责处理提交到线程池中的任务,我们把它叫做Worker。其实,这里的Worker的定义和Java线程池中的Worker已经很像了,它继承了Runnable接口并封装了Thread. 在构造Worker时,可以设定它的名字,并传入任务队列。当Worker启动后,它将会从任务队列中获取任务并执行。此外,它还提供了Stop方法,用以响应线程池的状态变化。

/** * 线程池中用于执行任务的线程 */ public class Worker implements Runnable { private final String name; private Thread thread = null; private final BlockingQueue taskQueue; private boolean isStopped = false; private AtomicInteger counter = new AtomicInteger(); public Worker(String name, BlockingQueue queue) { this.name = name; taskQueue = queue; } public void run() { this.thread = Thread.currentThread(); while (!isStopped()) { try { Task task = taskQueue.poll(5L, TimeUnit.SECONDS); if (task != null) { note(this.thread.getName(), ":获取到新的任务->", task.getTaskDesc()); task.run(); counter.getAndIncrement(); } } catch (Exception ignored) { } } note(this.thread.getName(), ":已结束工作,执行任务数量:" + counter.get()); } public synchronized void doStop() { isStopped = true; if (thread != null) { this.thread.interrupt(); } } public synchronized boolean isStopped() { return isStopped; } public String getName() { return name; } }

第三步:设计并制作任务。任务是可以可执行的对象,因此我们直接继承Runnable接口就行。其实,直接使用Runnable接口也是可以的,只不过为了让示例更加清楚,我们给Task加了任务描述的方法。

/** * 任务 */ public interface Task extends Runnable { String getTaskDesc(); }

第四步:设计线程池的状态。线程池作为一个运行框架,它必然会有一系列的状态,比如运行中、停止、关闭等。

public enum ThreadPoolStatus { RUNNING(), SHUTDOWN(), STOP(), TIDYING(), TERMINATED(); ThreadPoolStatus() { } public boolean isRunning() { return ThreadPoolStatus.RUNNING.equals(this); } }

以上四个步骤完成后,一个简易的线程池就已经制作完毕。你看,如果你从以上几点入手来理解线程池的源码的话,是不是要简单多了?Java中的线程池的核心组成也是如此,只不过在细节处理等方面更多全面且丰富。

2. 运行线程池

现在,我们的王者线程池已经制作好。接下来,我们通过一个场景来运行它,看看它的效果如何。

试验场景:峡谷森林中,铠、兰陵王和典韦等负责打野,而安其拉、貂蝉和大乔等美女负责对狩猎到的野怪进行烧烤,一场欢快的峡谷烧烤节正在进行中

在这个场景中,铠和兰陵王他们负责提交任务,而貂蝉和大乔她们则负责处理任务。

在下面的实现代码中,我们通过上述设计的TheKingThreadPool来定义个线程池,wildMonsters中的野怪表示待提交的任务,并安排3个工作线程来执行任务。在示例代码的末尾,当所有任务执行结束后,关闭线程池。

public static void main(String[] args) { TheKingThreadPool theKingThreadPool = new TheKingThreadPool(3, new ArrayBlockingQueue(10)); String[] wildMonsters = {"棕熊", "野鸡", "灰狼", "野兔", "狐狸", "小鹿", "小花豹", "野猪"}; for (String wildMonsterName : wildMonsters) { theKingThreadPool.execute(new Task() { public String getTaskDesc() { return wildMonsterName; } public void run() { System.out.println(Thread.currentThread().getName() + ":" + wildMonsterName + "已经烤好"); } }); } theKingThreadPool.waitUntilAllTasksFinished(); theKingThreadPool.stop(); }

王者线程池运行结果如下:

Worker0:获取到新的任务->灰狼 Worker1:获取到新的任务->野鸡 Worker1:野鸡已经烤好 Worker2:获取到新的任务->棕熊 Worker2:棕熊已经烤好 Worker1:获取到新的任务->野兔 Worker1:野兔已经烤好 Worker0:灰狼已经烤好 Worker1:获取到新的任务->小鹿 Worker1:小鹿已经烤好 Worker2:获取到新的任务->狐狸 Worker2:狐狸已经烤好 Worker1:获取到新的任务->野猪 Worker1:野猪已经烤好 Worker0:获取到新的任务->小花豹 Worker0:小花豹已经烤好 Worker0:已结束工作,执行任务数量:2 Worker2:已结束工作,执行任务数量:2 Worker1:已结束工作,执行任务数量:4 Process finished with exit code 0

从结果中可以看到,效果完全符合预期。所有的任务都已经提交完毕,并且都被正确执行。此外,通过线程池的任务统计,可以看到任务并不是均匀分配,Worker1执行了4个任务,而Worker0和Worker2均只执行了2个任务,这也是线程池中的正常现象。

三、透彻理解Java中的线程池

在手工制作线程线程池之后,再来理解Java中的线程池就相对要容易很多。当然,相比于王者线程池,Java中的线程池(ThreadPoolExecutor)的实现要复杂很多。所以,理解时应当遵循一定的结构和脉络,把握住线程池的核心要点,眉毛胡子一把抓、理不清层次会导致你无法有效理解它的设计内涵,进而导致你无法正确掌握它。

总体来说,Java中的线程池的设计核心都是围绕“任务”进行,可以通过一个框架两大核心三大过程概括。理解了这三个重要概念,基本上你已经能从相对抽象的层面理解了线程池。

一个框架:即线程池的整体设计存在一个框架,而不是杂乱无章的组成。所以,在学习线程池时,首先要能从立体上感知到这个框架的存在,而不要陷于凌乱的细节中;两大核心:在线程池的整个框架中,围绕任务执行这件事,存在两大核心:任务的管理任务的执行,对应的也就是任务队列和用于执行任务的工作线程任务队列工作线程是框架得以有效运转的关键部件;三大过程:前面说过,线程池的整体设计都是围绕任务展开,所以框架内可以分为任务提交任务管理任务执行三大过程。

从类比的角度讲,你可以把框架看作是一个生产车间。在这个车间里,有一条流水线,任务队列工作线程是这条流水线的两大关键组成。而在流水线运作的过程中,就会涉及任务提交任务管理任务执行等不同的过程。

下面这幅图,将帮助你立体地感知线程池的整体设计,建议你收藏。在这幅图中,清楚地展示了线程池整个框架的工作流程和核心部件,接下来的文章也将围绕这幅图展开。

1. 线程池框架设计概览

从源码层面看,理解Java中的线程池,要从下面这四兄弟的概念和关系入手,这四个概念务必了然于心。

Executor:作为线程池的最顶层接口,Executor的接口在设计上,实现了任务提交任务执行之间的解耦,这是它存在的意义。在Executor中,只定义了一个方法void execute(Runnable command),用于执行提交的可运行的任务。注意,你看它这个方法的参数干脆就叫command,也就是“命令”,意在表明所提交的不是一个静止的对象,而是可运行的命令。并且,这个命令将在未来的某一时刻执行,具体由哪个线程来执行也是不确定的;ExecutorService:继承了Executor的接口,并在此基础上提供可以管理服务执行结果(Futrue) 的能力。ExecutorService所提供的submit方法可以返回任务的执行结果,而shutdown方法则可以用于关闭服务。相比起来,Executor只具备单一的执行能力,而ExecutorService则不仅具有执行能力,还提供了简单的服务管理能力AbstractExecutorService:作为ExecutorService的简单实现,该类通过RunnableFuture和newTaskFor实现了submit、invokeAny和invokeAll等方法;ThreadPoolExecutor:该类是线程池的最终实现类,实现了Executor和ExecutorService中定义的能力,并丰富了AbstractExecutorService中的实现。在ThreadPoolExecutor中,定义了任务管理策略和线程池管理能力,相关能力的实现细节将是我们下文所要讲解的核心所在。

如果你觉得还是不太能直观地感受四兄弟的差异,那么你可以放大查看下面这幅高清图示。看的时候,要格外注意它们各自方法的不同,方法的不同意味着它们的能力不同

而对于线程池总体的执行过程,下面这幅图也建议你收藏。这幅图虽然简明,但完整展示了从任务提交到任务执行的整个过程。这个执行过程往往也是面试中的高频面试题,务必掌握。

(1)线程池的核心属性

线程池中的一些核心属性选取如下,对于其中个别属性会做特别说明。

// 线程池控制相关的主要变量 // 这个变量很神奇,下文后专门陈述,请特别留意 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 待处理的任务队列 private final BlockingQueue < Runnable > workQueue; // 工作线程集合 private final HashSet < Worker > workers = new HashSet < Worker > (); // 创建线程所用到的线程工厂 private volatile ThreadFactory threadFactory; // 拒绝策略 private volatile RejectedExecutionHandler handler; // 核心线程数 private volatile int corePoolSize; // 最大线程数 private volatile int maximumPoolSize; // 空闲线程的保活时长 private volatile long keepAliveTime; // 线程池变更的主要控制锁,在工作线程数、变更线程池状态等场景下都会用到 private final ReentrantLock mainLock = new ReentrantLock();

关于ctl字段的特别说明

在ThreadPoolExecutor的多个核心字段中,其他字段可能都比较好理解,但是ctl要单独拎出来做些解释。

顾名思义,ctl这个字段用于对线程池的控制。它的设计比较有趣,用一个字段却表示了两层含义,也就是这个字段实际是两个字段的合体:

runState:线程池的运行状态(高3位);workerCount:工作线程数量(第29位)。

这两个字段的值相互独立,互不影响。那为何要用这种设计呢?这是因为,在线程池中这两个字段几乎总是如影相随,如果不用一个字段来表示的话,那么就需要通过锁的机制来控制两个字段的一致性。不得不说,这个字段设计上还是比较巧妙的。

在线程池中,也提供了一些方法可以方便地获取线程池的状态和工作线程数量,它们都是通过对ctl进行位运算得来。

/** 计算当前线程池的状态 */ private static int runStateOf(int c) { return c & ~CAPACITY; } /** 计算当前工作线程数 */ private static int workerCountOf(int c) { return c & CAPACITY; } /** 初始化ctl变量 */ private static int ctlOf(int rs, int wc) { return rs | wc; }

关于位运算,这里补充一点说明,如果你对位运算有点迷糊的话可以看看,如果你对它比较熟悉则可以直接跳过。

假设A=15,二进制是1111;B=6,二进制是110.运算符名称描述示例&按位与如果相对应位都是1,则结果为1,否则为0(A&B),得到6,即110~按位非按位取反运算符翻转操作数的每一位,即0变成1,1变成0。(〜A)得到-16,即11111111111111111111111111110000|按位或如果相对应位都是 0,则结果为 0,否则为 1(A | B)得到15,即 1111(2)线程池的核心构造器

ThreadPoolExecutor有四个构造器,其中一个是核心构造器。你可以根据需要,按需使用这些构造器。

核心构造器之一:相对较为常用的一个构造器,你可以指定核心线程数、最大线程数、线程保活时间和任务队列类型。public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue < Runnable > workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }核心构造器之二:相比于第一个构造器,你可以在这个构造器中指定ThreadFactory. 通过ThreadFactory,你可以指定线程名称、分组等个性化信息。public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue < Runnable > workQueue, ThreadFactory threadFactory) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler); }核心构造器之三:这个构造器的要点在于,你可以指定拒绝策略。关于任务队列的拒绝策略,下文有详细介绍。public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue < Runnable > workQueue, RejectedExecutionHandler handler) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler); }核心构造器之四:这个构造器是ThreadPoolExecutor的核心构造器,提供了较为全面的参数设置,上述的三个构造器都是基于它实现。public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue < Runnable > workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3