ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多线程面试题系统梳理:线程池、锁、协程与断点续传实战

多线程面试题系统梳理:线程池、锁、协程与断点续传实战 多线程面试题这几年被问得越来越刁钻早年间背几句线程是 CPU 调度的最小单位就能糊弄过去现在面试官张口就是你这个线程池为什么用无界队列、volatile 在双重检查锁定里到底挡住了什么、线上 CPU 打满你怎么查。我自己既做过面试官也被人面过不少回最直观的感受是多线程这块真正拉开差距的不是你知道多少名词而是你能不能在一分钟内把某个方案的取舍代价讲清楚。下面这些内容是我把近几年被问到的、问别人的、以及带人复盘过的题目做的一次系统整理覆盖 Java、Python、C、C#、Qt、Flutter 几大技术栈也包含断点续传下载和 gdb 调试这类偏实战的场景题。适合准备中高级岗位的人也适合工作几年但一直没系统梳理过并发知识的人拿来查漏补缺。1. 面试官抛出多线程问题时脑子里其实在算什么账1.1 一道题背后通常藏着三层追问很多人以为面试官问进程和线程的区别就是想听标准答案其实那只是第一层。三个层次大致是这样第一层是概念层考你能不能把基本定义说清楚线程状态有哪些、线程和进程怎么划分、什么是上下文切换。这一层筛掉的是完全没准备的人。第二层是机制层考你知不知道底层发生了什么。比如你说用锁保证线程安全他会追问锁住的是什么、加锁之后其他线程在哪里等、等待的线程被唤醒之后为什么要重新抢锁。你说用 volatile 保证可见性他会问可见性是怎么实现的、写屏障和读屏障分别做了什么。第三层是工程判断层这是真正区分层级的地方。他关心的是你知不知道用并发是有代价的什么场景下不该用出了问题怎么定位。典型问法是你说多线程下载快那开多少线程合适服务器不支持 Range 怎么办分片写同一个文件怎么保证不串我见过不少候选人第一层答得滴水不漏第二层勉强能接到了第三层就开始绕。而中高级岗位的评估权重恰恰在第三层。1.2 只会背概念的人一般卡在哪个环节卡点通常在两个地方。一个是对**为什么缺少解释**。比如被问到为什么要用线程池回答因为创建线程开销大这话没错但太浅。面试官想听的是创建线程到底贵在哪内核态栈分配、系统调用、调度器注册、线程池除了省创建开销还解决了什么限流、复用、统一监控、任务排队、以及它引入了什么新问题队列积压、线程泄漏、上下文切换。另一个是对参数的量化过程说不出来。问线程池核心线程数怎么定答CPU 密集就 N1IO 密集就 2N这是背书。稍微认真一点的面试官会继续问你的机器几核任务里 IO 占多少2N 这个系数是怎么来的这时候能写出N * (1 等待时间 / 计算时间)并且能解释每项含义的人一下子就区分开了。提示多线程题的答题节奏建议是先给结论再给理由最后主动补一句边界条件。主动交边界等于把面试官的下一发子弹提前卸掉。1.3 一个真实的追问链路示例我印象最深的一次是候选人简历里写了使用多线程优化导出功能性能提升 3 倍。链路大概是这样的为什么要用多线程——单线程导出 30 万条要 40 秒。瓶颈在哪——主要是数据库查询和写 Excel。那数据库连接池够吗——愣了一下说应该够。线程数设了多少——CPU 核数乘 2。机器几核——8 核。16 个线程同时查库连接池最大连接数是 10会发生什么——这里就答不上来了。这个例子说明一个事多线程从来不是孤立的它一定和你系统里的其他资源连接池、文件句柄、内存耦合。能把这条链讲通的人面试通过率会高很多。2. 进程、线程、协程的三方对照先把地基打牢2.1 地址空间与调度单位进程线程最本质的分界线把这三个东西分清楚最简单的抓手是看两件事谁分配资源、谁被调度。进程是资源分配的基本单位。每个进程有独立的虚拟地址空间、独立的页表、独立的文件描述符表。这意味着两个进程之间默认看不到对方的内存想通信必须走内核提供的通道管道、消息队列、共享内存、Socket。也意味着进程切换的代价高——不仅寄存器要换页表基址寄存器要换TLB 大概率要失效缓存局部性也会被破坏。线程是调度的基本单位。同一个进程内的线程共享地址空间、共享堆、共享全局变量和文件描述符但各自有独立的栈和寄存器上下文。所以线程间通信极其方便直接读写同一个变量就行——而这恰恰是所有并发 bug 的源头。线程切换不需要换页表比进程切换便宜一个量级但依然要陷入内核态除非用的是用户态线程实现。协程是用户态的调度单元。它的切换完全在用户空间完成不进入内核成本可以低到几十纳秒级别。代价是操作系统根本不知道协程的存在一个协程阻塞了底层线程同一线程上的其他协程全都得等着。所以协程必须配合非阻塞 IO 才有意义而且一个线程上跑多少个协程需要应用自己控制。维度进程线程协程地址空间独立共享进程共享线程切换成本高页表、TLB中内核态切换低用户态跳转通信方式管道、共享内存、Socket共享变量 同步原语共享变量单线程内无竞争崩溃影响范围仅自身拖垮整个进程拖垮所在线程典型应用服务隔离、多核计算IO 密集、共享数据高并发网络服务2.2 多进程还是多线程按任务类型做选择这个问题几乎每场面试都会以某种形式出现判断逻辑其实很清晰。CPU 密集型任务优先多进程尤其在 Python 这种有全局解释器锁的语言里。原因是多线程在 CPython 下根本压不满多核加线程只是加切换开销。C、Java 这类没有 GIL 的语言多线程也能吃满多核那就看数据共享的密度。IO 密集型任务优先多线程或协程。因为线程大部分时间在等 IOCPU 是空闲的这时候多开几个线程能显著提高吞吐。协程在这个场景下更划算因为它把等待这件事的成本压得更低同样一台机器能扛的连接数高一个数量级。需要强隔离的场景优先多进程。最典型的就是浏览器一个标签页崩了不应该带着整个浏览器一起死。反过来如果一个崩溃就全完蛋比如某些计算任务多进程反而增加复杂度。另外多进程还有一个隐性好处方便做资源限额比如限制某个子进程最多用 2G 内存。共享大量数据的场景优先多线程。多进程之间共享数据要么复制要么走共享内存复制成本高共享内存又要自己处理同步代码复杂度陡增。如果一份数据要被多个执行单元反复读写多线程是更自然的选择。2.3 协程为什么在高并发 IO 场景里更占便宜很多人对协程的理解停留在比线程更轻但轻在哪说不清楚。关键差别在调度时机。线程的调度是抢占式的操作系统在你毫不知情的时候把 CPU 抢走所以你必须用锁来保护临界区还要考虑各种时序问题。协程的调度是协作式的只有在协程主动让出await / yield的时候才会切换切换点是你自己写在代码里的。这带来两个直接好处一是单线程内的协程之间不存在真正的同时执行很多场景下根本不需要锁二是切换点明确调试和推理都容易得多。代价也很实在。协程代码对阻塞零容忍一个没注意到的同步阻塞调用就能把整个事件循环卡住。Python 里常见的翻车方式是在 async 函数里调了一个同步的数据库驱动或者requests事件循环直接被冻住所有并发请求一起变慢。所以协程世界里有个不成文的规矩整个调用链要么全是异步要么把同步部分丢进线程池跑。3. 线程安全与锁淘汰率最高的一类题3.1 竞态条件的成因与手工复现线程安全问题的根源可以归结为三个词原子性、可见性、有序性。count这行代码看着像一步操作实际至少是三步从内存读到寄存器、寄存器加一、写回内存。两个线程交叉执行这三步就会丢更新。这就是原子性问题。可见性问题来自 CPU 的多级缓存。线程 A 改了变量写在自己的缓存里线程 B 读的是自己缓存里的旧值两边看到的不是同一个东西。Java 通过 JMM 这套抽象来屏蔽底层差异规定线程对共享变量的操作必须经过主内存volatile 和锁则负责在特定点插入内存屏障强制刷新。有序性问题来自编译器和 CPU 的指令重排优化。只要不改变单线程的语义编译器就可以调整指令顺序CPU 也会乱序执行。在多线程环境下这种重排会导致意想不到的结果最经典的就是双重检查锁定里先分配内存、再把引用指向内存、最后初始化对象这个顺序被打乱导致另一个线程拿到一个还没初始化完的对象。解决办法就是在那个字段上加强volatile。想手工复现丢更新很简单开十个线程各做十万次自增最后统计结果大概率小于一百万。这个实验我在带新人时必让他们跑一遍比讲十遍原理管用。3.2 从 synchronized 到 CAS各自解决什么问题Java 里的同步手段可以按解决哪种问题来归类。synchronized解决的是互斥 可见性。它修饰实例方法锁当前对象修饰静态方法锁 Class 对象修饰代码块锁指定对象。它的好处是自动释放抛异常也会释放JVM 还做了锁升级优化从无锁到偏向锁JDK 15 之后默认关闭到轻量级锁CAS 自旋再到重量级锁操作系统互斥量。面试里能讲清这条升级路径和各自的适用场景无竞争、短临界区、长临界区就已经超过大部分人了。ReentrantLock提供的是比 synchronized 更细的控制可以设置公平锁、可以带超时地获取、可以被中断、可以用Condition做多个等待队列synchronized 只有一个 wait set。什么时候用它需要超时避免死等、需要公平性、需要按条件精确唤醒的时候。注意一定要写在try里、finally里释放否则一个异常就把锁永久泄漏了。volatile只解决可见性和有序性不解决原子性。它的典型使用场景是两个一是状态标志位一个线程写、多个线程读二是双重检查锁定的单例字段。很多人栽在把它当成轻量级锁来用。CASCompare And Swap是无锁编程的基石底层是一条 CPU 原子指令。AtomicInteger、AtomicReference这些类都是基于它实现的。它的三个问题是ABA 问题用版本号或时间戳解决比如AtomicStampedReference、自旋开销高竞争下自旋白费 CPU、只能保证单个变量的原子性多个变量要么合并成一个对象要么用锁。3.3 死锁四个必要条件与线上定位手段死锁的四条件背起来容易讲透不容易互斥资源同一时间只能被一个线程持有。请求与保持持有资源的同时还去申请别的资源。不可剥夺拿到的资源在释放前不能被强行抢走。循环等待存在一个线程到资源的环形等待链。四个条件同时成立才会死锁破坏任意一个就能避免。工程里最实用的是破坏循环等待给所有锁定一个全局顺序所有线程都按这个顺序加锁。比如转账场景里永远按账户 ID 大小顺序加锁就不会出现 A 等 B、B 等 A 的环形。定位手段分语言看。Java 线上直接jstack pid如果发生死锁输出里会明确打印Found one Java-level deadlock并列出涉及的两个线程和它们卡在哪一行。C/C 环境下用 gdb attach 上去thread apply all bt把全部线程堆栈打出来人肉找谁在等谁的锁。Qt 程序类似可以用 gdb 看QMutex::lock的调用栈。比定位更重要的是预防性的可观测性线程池的ThreadFactory里给线程起个有意义的名字比如order-export-1锁等待加上超时和日志。我见过太多线上事故jstack打出来全是pool-1-thread-37光对齐哪个线程在干什么就得花半小时。3.4 几个隐蔽但高频的坑SimpleDateFormat是线程不安全的内部有个可变的Calendar字段多线程共用会出现解析结果错乱甚至抛异常。正确做法是每个线程一个实例配合ThreadLocal或者直接用DateTimeFormatter。ThreadLocal在线程池里必须手动清理。池里的线程是复用的上一个请求放进去的值如果不清下一个请求可能读到脏数据更严重的是如果值是对象引用会一直挂在池线程上导致内存泄漏。规范写法是try { ... } finally { threadLocal.remove(); }。HashMap在并发写的时候会出问题。Java 7 的头插法在并发扩容时可能形成环形链表导致 CPU 打满Java 8 改成尾插法避免了死循环但依然可能丢数据、size 不准。并发场景老老实实用ConcurrentHashMap。还有伪共享这个偏底层的坑两个线程频繁写同一缓存行里的不同变量会导致缓存行在两个核之间反复失效性能比单线程还差。Java 里可以用Contended做缓存行填充C 里手动 padding 到 64 字节。4. 同一道题在 Java、Python、C、C#、Qt、Flutter 里的问法差异4.1 Java 线JMM、volatile 与线程池参数Java 的并发面试基本围绕 JMM 展开。happens-before 规则要能举出几个典型的程序顺序规则、监视器锁规则解锁先于后续加锁、volatile 变量规则写先于后续读、线程启动规则start 先于线程内所有操作、线程终止规则线程内所有操作先于 join 返回。线程池那套参数是必考的核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。执行流程要能画出来任务进来先看核心线程满没满没满直接建线程满了就入队队列满了再看最大线程数还没到就建非核心线程到了就执行拒绝策略。这个顺序有个反直觉的点——先入队再扩容很多候选人答成先扩容一问就露馅。再往上会问到CompletableFuture的组合能力、ForkJoinPool的工作窃取、以及 JDK 21 正式落地的虚拟线程。虚拟线程这块能说清它的定位高并发 IO 场景下把线程的成本降下来让一个请求一个线程这种简单模型重新可用和限制CPU 密集任务没有收益、synchronized块里阻塞会钉住载体线程就足够了。4.2 Python 线GIL 之下的取舍Python 的并发问题绕不开 GIL。它的本质是 CPython 解释器为了保证内部数据结构安全而加的一把全局锁同一时刻只有一个线程在执行 Python 字节码。所以CPU 密集任务用多线程几乎没有加速甚至因为切换开销更慢要用multiprocessing。IO 密集任务多线程有效因为执行 IO 时线程会释放 GIL。想真正压满多核又不想多进程可以考虑用 C 扩展在计算时释放 GIL或者用 numpy 这类底层已经释放 GIL 的库。有个细节常被问GIL 之下count 1是不是就安全了答案是不安全。虽然字节码执行不会被打断但对应多条字节码线程可能在中间被切换。想验证可以起十个线程各加十万次结果基本不会是整数。另外值得提一句近几个版本 Python 在推进 free-threaded 构建PEP 703但生态兼容性还在演进中现阶段选型时不要想当然认为它就是默认行为。4.3 C 与 Qt 线内存序和线程亲和性C 的并发面试会更底层一些。std::thread的启动、join和detach的区别、为什么detach之后要特别小心对象可能在线程还在跑的时候就析构了。RAII 风格的锁管理是基本要求std::lock_guard用于简单加锁std::unique_lock支持延迟加锁、手动解锁和配合条件变量。条件变量必须配while循环判断谓词防虚假唤醒。std::atomic的memory_order是高级题。默认的seq_cst最安全也最慢acquire/release配对使用可以在保证正确性的前提下省掉一些屏障开销relaxed只保证原子性不保证顺序。能结合实际场景比如自旋锁实现、无锁队列讲清楚为什么选某个内存序是很强的加分项。Qt 这边核心考点是线程亲和性。QObject属于创建它的那个线程跨线程直接调用它的方法是不安全的。正确做法是用信号槽跨线程连接默认走QueuedConnection参数会被拷贝到接收者线程的事件循环里执行。QThread的两种用法也要分清继承QThread重写run是老写法moveToThread把工作对象挪到新线程是推荐写法。GUI 操作只能在主线程做这条铁律不能破。共享资源用QMutex、QReadWriteLock任务型并发用QThreadPool配QRunnable或者QtConcurrent::run。4.4 C# 与 Flutter 线async/await 和 Isolate 的真相C# 里最大的误解是async/await 会开新线程。实际上async方法在遇到await之前的代码是在调用线程上同步执行的await之后是否换线程取决于有没有SynchronizationContext。它本质上是编译器生成的状态机把回调写成了线性代码的样子。真正要开线程得用Task.Run。面试里常问ConfigureAwait(false)的作用告诉延续任务不需要回到原来的上下文在库代码里用它能让实现更轻、更不容易死锁。同步原语方面lock是Monitor的语法糖Interlocked提供原子操作ConcurrentQueue、ConcurrentDictionary这些并发集合在内部做了细粒度锁定或无锁优化比自己在普通集合外面包一把大锁高效得多。Flutter 这边的概念差异更大。Dart 是单线程事件循环模型代码跑在主 isolate 上异步任务是靠事件队列和微任务队列驱动的不是靠多线程。要真正并行计算得用Isolate或者compute()函数。Isolate 之间不共享内存只能通过SendPort传消息所以压根不存在锁的问题但也意味着大数据传输要考虑拷贝成本能用TransferableTypedData就尽量用它。判断标准很简单涉及大量计算、可能阻塞 UI 的丢给 Isolate只是等网络或磁盘 IO 的用 async/await 就够了。5. 线程池的参数量化与并发容器选型5.1 核心线程数到底怎么算先分清任务类型。CPU 密集型线程大部分时间在算上下文切换是纯损耗。经验值是核数加一多出来的那个一是为了在某线程偶发缺页时顶上。核数是 8 就设 8 到 9。设成 32 只会让切换开销把收益吃掉。IO 密集型线程大部分时间在等可以多设。公式是线程数 核数 × 目标利用率 × (1 等待时间 / 计算时间)举个具体例子。8 核机器任务平均计算 10ms、等待 IO 50ms期望 CPU 利用率 80%线程数 8 × 0.8 × (1 50 / 10) 8 × 0.8 × 6 38.4 ≈ 38这个 38 是上限而不是目标值实际还要看下游能扛多少。如果数据库连接池只有 20 个连接你开 38 个线程去查库多出来的 18 个只是堵在连接池上白白占内存。所以我一般会取公式算出来的值和下游资源上限里小的那个。生产环境的调参流程我建议是这样的先按公式估一个初始值压测看吞吐和响应时间曲线再逐步调整。注意看两个拐点一个是吞吐不再上升的点一个是响应时间开始陡增的点最优点通常在这两个点之间。5.2 阻塞队列和拒绝策略的组合拳队列选型直接影响线程池的行为这是面试里的高频加分点。队列类型特点适用场景风险ArrayBlockingQueue有界数组读写同一把锁需要严格背压容量设小了拒绝率飙升LinkedBlockingQueue链表默认容量接近无界任务量波动大无界时可能积压到 OOMSynchronousQueue不存储元素直接移交高吞吐、任务短小必须配大 maxPoolSizePriorityBlockingQueue按优先级出队有分级要求的任务无界低优先级可能饿死DelayQueue延迟出队定时重试、超时处理容量规划要另做这里最常见的坑是图省事用Executors.newFixedThreadPool(n)它内部用的是无界LinkedBlockingQueue。队列永远不满最大线程数就永远用不上任务全堆在队列里内存一路涨到 OOM。生产代码我强烈建议手动new ThreadPoolExecutor所有参数显式写出来一眼能看到边界在哪。拒绝策略有四种默认实现AbortPolicy直接抛RejectedExecutionException。默认策略适合希望快速失败的场景。CallerRunsPolicy让提交任务的线程自己跑。天然形成背压任务提交方被拖慢队列就不会继续涨。用得多但要注意如果提交方是 IO 线程或者定时任务线程可能会把整个调度卡住。DiscardPolicy静默丢弃。除非任务本身无所谓否则别用出了问题连日志都没有。DiscardOldestPolicy丢队列里最老的。适合新数据比老数据有价值的场景比如实时行情。实际项目里更常见的是自定义策略打到日志里、上报监控、落盘到本地队列稍后重放。我一般会在自定义策略里记下被拒任务的关键标识出问题时能查到是哪批请求被丢了。线程池的监控也不能少。定期采集getActiveCount()、getQueue().size()、getCompletedTaskCount()配合自定义ThreadFactory给线程命名出问题的时候排查效率能差出好几倍。还可以重写beforeExecute和afterExecute在任务执行前后埋点统计每个任务的耗时分布。6. 场景题最容易翻车的两个方向6.1 HTTP 断点续传与多线程分片下载这是把多线程和网络协议结合起来的经典场景能讲清说明你对 HTTP 的理解到位。第一步是探测服务端能力。可以先发一个HEAD请求看响应里有没有Accept-Ranges: bytes和Content-Length。有些服务端不支持 HEAD那就发一个Range: bytes0-0的 GET期望返回 206。第二步是分片。假设文件 100MB想开 4 个线程每片 25MB区间就是0-26214399、26214400-52428799以此类推。这里有两个容易算错的地方区间是闭区间长度是end - start 1最后一片的end要按实际文件大小截断不能直接按等分算否则会超出范围拿到 416。第三步是并发下载和写入。有两种写法一是每片下载到自己独立的临时文件全部完成后按顺序合并二是预先创建好目标文件并设置到最终大小每片用随机写入定位到自己的偏移。Java 用RandomAccessFile.seek()Python 用f.seek()加rb模式C 用pwrite()。第二种更省空间也省一次拷贝但要注意每个线程要用独立的文件句柄别多个线程共用一个RandomAccessFile对象虽然它是线程安全的但会互相干扰文件指针。第四步是断点记录。每个分片记录自己已经写到哪个偏移定期持久化到一个进度文件里JSON 或简单的文本都行。重启后读进度文件只把没完成的部分重新请求。第五步是一致性校验。请求头带上If-Range: ETag如果文件在服务端变了服务端会返回 200 全量内容而不是 206这时候就得放弃续传重来。否则你拼出来的文件会是新旧内容的混合体而且校验和还会对不上。几个实践中踩出来的注意点并发数不要一上来就开几十个。很多服务端和 CDN 会对单 IP 的连接数或总带宽限流开太多反而更慢还可能被临时拒绝。一般 4 到 8 个线程是比较舒服的区间。必须加重试和超时。分片请求失败很常见失败一次就整个任务失败体验太差。每个分片配 2 到 3 次重试指数退避。服务端不支持 Range 时要能优雅降级。检测到响应是 200 而不是 206就老老实实单线程下载别硬刚。磁盘写入可能成为瓶颈。如果同时开 8 个线程写同一块盘IO 打满的话下载速度反而下降这时候要考虑减少并发或者用缓冲批量写。浏览器上那些多线程下载加速类插件底层逻辑就是上面这套。理解了协议层的东西工具对你来说就是透明的。6.2 gdb 调试多线程从卡死到定位C/C 项目里排查多线程问题gdb 是绕不开的。编译时要带上-g保留调试符号链接时要带-pthread。最常用的几条命令命令作用info threads列出所有线程及当前栈帧位置thread n切换到第 n 号线程thread apply all bt打印所有线程的完整调用栈set scheduler-locking off单步时不锁定其他线程默认set scheduler-locking on单步时锁定当前线程其他线程暂停break file:line在指定位置下断点break func if x 5条件断点watch var监视变量变化排查程序卡死不动这个场景标准动作是 attach 上去然后thread apply all bt。拿到全量堆栈之后重点看有没有这样的形态线程 A 卡在等锁 M1持有 M2线程 B 卡在等锁 M2持有 M1。这就是经典的死锁。如果是所有工作线程都在等条件变量而没有任何线程会去唤醒它们那就是逻辑上的漏唤醒。单独调试某个线程时scheduler-locking这个设置很关键。默认off的情况下你单步走一步操作系统可能把 CPU 调度给别的线程虚拟机由它们继续跑你看到的变量值莫名其妙就变了很容易误判。需要专心看一个线程的行为时设成on但记住它只对单步生效continue还是会放开。还有一点如果是跑了一段时间才崩的别指望现场还在。提前开 core dumpulimit -c unlimited配合内核的 core_pattern或者用gcore pid手动抓一份。拿到 core 文件之后gdb 可执行文件 core 文件就能离线分析所有线程的堆栈、局部变量、内存内容都在里面。这个能力在定位线上偶发问题时价值极高。7. 高频题速查表和现场答题节奏7.1 手写交替打印的标准写法两个线程交替打印 1 到 100是出现频率极高的手写题。用 Java 的wait/notify写一版public class AlternatePrint { private static final Object LOCK new Object(); private static int count 1; private static final int MAX 100; public static void main(String[] args) { Thread odd new Thread(new Printer(true), odd); Thread even new Thread(new Printer(false), even); odd.start(); even.start(); } static class Printer implements Runnable { private final boolean printOdd; Printer(boolean printOdd) { this.printOdd printOdd; } Override public void run() { synchronized (LOCK) { while (count MAX) { boolean myTurn (count % 2 1) printOdd; if (myTurn) { System.out.println(Thread.currentThread().getName() - count); count; LOCK.notifyAll(); } else { try { LOCK.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } } } } } }几个面试官一定会盯的细节为什么用while而不是if。因为存在虚假唤醒线程可能在没有被notify的情况下从wait返回。用if判断一次就往下走会在不该打印的时候打印。必须用while重新检查条件。为什么用notifyAll而不是notify。两个线程在同一个锁对象上等待用notify唤醒的是哪一个不确定。万一唤醒了不该醒的那个它检查条件不满足又回去等真正该跑的那个就永远醒不过来了。用notifyAll保证两个都能醒来重新竞争。当然如果用了ReentrantLock的两个Condition就可以精确唤醒对方signal()就够。为什么 wait 要放在 try-catch 里。wait会抛InterruptedException而且这里有个原则捕获到中断之后要么往上抛要么恢复中断标记Thread.currentThread().interrupt()不要静默吞掉。如果面试官要求换一种实现可以给ReentrantLock加两个Condition的版本或者用Semaphore互相发信号再或者用AtomicInteger配 CAS 自旋。能写出两种以上并说清各自优劣这道题基本满分。7.2 被追问时的应对顺序最后分享一套我自己在面试里验证过有效的答题节奏。先给一句话结论。线程池的核心线程数CPU 密集用核数加一IO 密集用公式算但最终取公式值和下游资源上限的较小值。这一句就把答案框定了。再给判断依据。把公式写出来把每一项的含义解释清楚顺便说一句为什么 CPU 密集场景加线程反而更慢切换开销。主动补边界。如果下游是数据库还要看连接池大小如果是调用外部接口还要看对方的限流策略。最后落到排查手段。上线之后我会看活跃线程数、队列深度和任务耗时分位数如果队列持续增长就说明线程数不够或者下游变慢了。这一句往往比前面所有的更能加分因为它证明你不只是会写代码还知道怎么保证它稳定运行。遇到真的不会的问题我的建议是别硬编。可以说这块我没在生产环境里用过凭我的理解应该是……但我不确定如果方便的话想听听您的答案。诚实加上一点推理比胡说要好得多——面试官问的很多问题本来就是奔着你的知识边界去的看清边界本身就是一种能力。另外提醒一句多线程的题很容易被追问成连环题回答的时候尽量把我知道的和我推测的分开说别把推测当结论讲。我见过候选人因为一个不确定的细节答得太肯定被连续追了三个问题最后前面答对的部分都被带偏了。
返回列表