假如你是一家名叫「并发轩」的餐厅后厨主管。开业第一晚,后厨就乱成了一锅粥:十个厨师(线程)同时抢一口炒锅(共享资源),有人菜炒到一半锅被端走;食材堆在台面上你推我搡;出菜口没人管,菜品越堆越高。你急需一套管理工具——而 Java 里恰好就有一套现成的:java.util.concurrent,江湖人称 JUC。它把锁、并发容器、同步器、线程池全部封装好,就像给后厨配备了灶台通行证、分格冰箱、开工倒计时牌和一支训练有素的厨师团队。这篇文章,我们就从「并发轩」的后厨出发,把这些工具一件件拆开看明白:锁怎么用、队怎么排、冰箱怎么分格、团队怎么调度。学完你再去看高并发 Java 代码,就像老主厨扫一眼后厨——一切秩序井然。
JUC 是 Java 高并发服务的通行证。吃透它,你才能写出线程安全的业务代码、治理好线上并发问题,并在面试中系统性地回答锁与线程池的原理。
目录
可视化演示
通过AQS等待队列、ConcurrentHashMap并发读写和线程池调度流程三个维度,展示Java并发包JUC的核心机制与工作原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
AQS等待队列与状态管理 volatile int state表示同步状态,CAS原子更新 CLH双向队列管理等待线程,Head节点持有锁 state=0(空闲) / state=1(独占) / state>1(重入) AQS是ReentrantLock、CountDownLatch等同步器的基石
JUC整体结构:后厨的工具墙
走进「并发轩」后厨,最先看到的是东墙上一整面工具墙。墙上挂四类东西:灶台通行证(锁)、分格冰箱(并发容器)、开工牌(同步器)和厨师排班表(线程池)。没学过 JUC 的人觉得这面墙眼花缭乱,但老主厨清楚,每一类工具对应一类后厨难题:锁管互斥、容器管存取、同步器管节奏、线程池管人手。
这面墙的学名就是 java.util.concurrent,从 JDK 1.5 开始随 Java 官方发布。它不是某个单一类,而是一整套并发体系:锁家族(ReentrantLock、ReadWriteLock 等)解决资源互斥;并发容器(ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue)解决线程安全的数据存取;同步器(CountDownLatch、CyclicBarrier、Semaphore)解决线程间的节奏编排;线程池(ThreadPoolExecutor)解决线程的创建、复用与回收。这些工具背后,绝大多数都站着一个共同的“规则本”——AQS。
为什么要这么大动干戈?因为多线程的收益不是无限的。当任务中可并行的部分占比为 P 时,理论加速比被阿姆达尔定律锁死:就算把厨师加到一百个,洗锅、切配这些必须串行的环节永远拖后腿。
这也解释了为什么「并发轩」的厨师不是越多越好:备料、洗锅这些串行环节(1-P)锁死了天花板。JUC 的全部价值,就是尽量把 P 做大、把互斥开销降到最低——用锁护住每一口锅,用容器管好每一份食材,让厨师们各司其职,而不是在推搡中内耗。
- 串行时间归一化把任务在单核上执行完的总时间设为 1,作为加速比的基准。
- 可并行部分被分摊占比为 P 的那部分工作可以分给 n 个核同时做,耗时从 P 缩短到 P/n。
- 串行部分无法缩水剩下 1-P 的工作必须按顺序完成,加多少核都不变。
- 总耗时与加速比并行后的总耗时为两者之和,加速比等于 1 除以总耗时。
AQS核心原理:叫号系统的规则本
晚市高峰,十个厨师都想用那口招牌炒锅。与其靠嗓门抢,不如装一个叫号系统:想用锅的先取号,到排队区等着,叫到号才上前。JUC 工具墙后面那本“叫号规则本”,就是 AQS(AbstractQueuedSynchronizer,抽象队列同步器)——几乎所有的显式锁和同步器都照着它实现。
AQS 的核心是两样东西:一个 volatile int 类型的 state 状态字段,和一条基于 CLH 变体的 FIFO 等待队列。state 表示资源被占用的程度:锁是否被持有、信号量还剩几张票;没抢到资源的线程会被封装成节点,挂到队列尾部挂起等待,而不是原地空转。
AQS 用模板方法模式定义骨架:acquire()、release() 规定整体流程,tryAcquire()、tryRelease() 留给子类填充。以 ReentrantLock 为例,加锁就是把 state 从 0 改成 1,而这一步依赖的原子操作是 CAS(Compare And Swap)——比较并交换,由 CPU 指令保证“看一眼、比一比、换一换”整个过程不可被打断。
叫号系统的好处是公平、可预测。CAS 就像一个眼疾手快的服务员:看到号码没变才敢撕票,撕完立刻登记;如果有人抢先,就重新取号。而 CLH 排队区保证了后来者不插队、不踩踏,这正是 ReentrantLock、Semaphore 等一切同步器能正常工作的地基。
- 三个参数各司其职state 是共享状态,expected 是调用前读到的那一版旧值,update 是想写入的新值。
- 比较前置条件只要当前 state 仍然等于 expected,就说明读完之后没人动过它。
- 执行原子交换满足条件就写入 update,整个过程由 CPU 指令保证原子,无法被打断。
- 失败则重试或入队比较失败说明有人抢先改了值,CAS 返回 false,线程重新读取或进入 CLH 队列等待。
ReentrantLock:可重入的灶台通行证
「并发轩」最贵重的是一口百年铁锅,主厨规定谁用必须先领灶台通行证,用完归还。但有个细节:如果一位厨师炒完一道菜,还想用同一口锅接着炖汤,难道要把证还回去重新排队?当然不用。ReentrantLock 的“可重入”就是这个意思——同一个线程可以反复获取同一把锁,不会把自己锁在门外。
ReentrantLock 是 JUC 里最常用的显式锁,实现了 Lock 接口。相比 synchronized,它带来三个新能力:可中断(lockInterruptibly())、可超时(tryLock(timeout))、可公平(构造参数 fair=true)。用法上必须手动加锁、手动解锁,标准姿势是 lock() 之后立刻 try/finally,保证锁一定释放。
可重入的原理藏在 AQS 的 state 里:它不再只是 0/1 两态,而是记录“当前持锁线程重入了多少次”。同一个线程每 lock() 一次,state 加 1;每 unlock() 一次减 1;减到 0,锁才真正释放给排队区的其他线程。
通行证上的“次数”一栏帮了大忙:同一位厨师连续用锅,只需在证上记一笔;只有最后一笔划掉,证上归零,才轮到下一位。相比 synchronized 的“一喊就锁、谁抢到算谁”,ReentrantLock 能排队、能限时、能响应中断,更适合复杂的业务后厨。
- state 兼任重入计数AQS 的 state 在这里不是简单的 0/1,而是当前线程的重入深度。
- 加锁深度加一当前线程每次成功 lock,state 加 1,同时把持锁线程记为当前线程。
- 解锁深度减一每次 unlock 把 state 减 1,只有减到 0 时才真正释放锁并唤醒等待者。
- 可重入的边界state 大于 0 时,只有持锁线程自己才能继续重入,其他线程只能在队列里等待。
Condition机制:等菜坐席与传菜铃
后厨除了炒锅,还有两个“等待区”:等食材的厨师们,和等出菜口空位的传菜员。他们不能干瞪眼,需要有人喊“食材到了!”或“出菜口空了!”。Condition 就是 Lock 配套的“传菜铃”——一个 Lock 可以 new 出多个 Condition,每一条对应一个独立的等待坐席。
synchronized 的 wait/notify 只能在同一个监视器上等待,粒度太粗;而一个 ReentrantLock 可以同时维护 notFull 和 notEmpty 两条队伍,分别管理“生产者等空位”和“消费者等食材”。这正是阻塞队列 ArrayBlockingQueue 的实现基础,也是比 synchronized 更细腻的地方。
调用 condition.await() 的线程会先释放锁,然后进入该 Condition 的等待队列挂起;signal() 唤醒这条队伍里的第一个线程,signalAll() 唤醒全部。被唤醒的线程要重新参与锁竞争,拿到锁之后才从 await() 返回继续执行。注意:await/signal 必须是在持有对应 Lock 的前提下调用,否则会抛 IllegalMonitorStateException。
两条等待坐席互不干扰,是 JUC 比 synchronized 细腻的关键。传菜铃一响,队伍里最前面那位先来后到地起身;等食材的不会去抢出菜口的空位。等的人各等各的,叫的人各叫各的,一来一回之间,后厨的流水线始终保持有条不紊。
- 消费者侧条件缓冲区里没有食材时,消费者线程在 notEmpty 坐席等待,不能取出。
- 生产者侧条件缓冲区已满时,生产者线程在 notFull 坐席等待,不能放入。
- 配对唤醒生产者放入后 signal(notEmpty),消费者取走后 signal(notFull),各叫醒一条队伍的头一位。
并发容器:分格冰箱里的线程安全
食材必须分格存放:蔬菜一格、肉类一格、酱料一格。如果所有食材堆在一个大冷柜里,一个人翻找时其他人只能干等。ConcurrentHashMap 就是这样的“分格冰箱”——把整张哈希表切成很多桶,不同线程操作不同桶时互不干扰,大大提升了并发度。
早期的并发安全方案是 Hashtable,给整张表加一把大锁,所有线程串行访问,效率极低。JDK 7 的 ConcurrentHashMap 引入“分段锁”:把表分成若干段,每段一把锁,只有同一段的操作才竞争。JDK 8 进一步把锁粒度细化到桶(bucket),用 CAS + synchronized 只锁链表的头节点,锁竞争进一步降低——这就是它从“分段锁”演进到“桶锁”的历程。
JDK 8 的 ConcurrentHashMap 底层是“数组 + 链表 + 红黑树”:默认容量 16,链表长度超过 8 且数组长度不小于 64 时树化,扩容阈值由加载因子决定。其他并发容器也各有所长:CopyOnWriteArrayList 适合读多写少,写时复制整份数组;BlockingQueue 系列用锁加条件队列实现生产者消费者管道。
分格冰箱让厨师各取所需,不用再为一格食材排队。但冰箱再大也有装满的时候(扩容阈值),而且“先看再拿”这种两步操作(读-改-写)依然需要额外小心——JUC 提供了 merge、computeIfAbsent 等原子方法,相当于“看一眼、拿走、登记”一气呵成,没人能从中插一脚。
- capacity 桶数量哈希表底层数组长度,默认 16,表示有 16 个桶位。
- loadFactor 加载因子默认 0.75,表示元素占桶位比例达到七成五时触发扩容。
- threshold 扩容阈值元素个数达到 12 就扩容,保证链表不至于过长。
- 扩容翻倍新数组长度为原来的两倍,元素重新散列,桶位更多、查询更快。
同步器工具:倒计时、晨会与门票
后厨除了炒菜,还有三类节奏要管:开业前所有人必须到岗;每天晨会大家要等齐了才开始备菜;热菜区一次只允许两个人进入。这三件事,正好对应同步器三件套——CountDownLatch(倒计时门闩)、CyclicBarrier(循环屏障)、Semaphore(信号量)。
CountDownLatch 像一个倒计时牌:初始化一个计数 N,主线程调用 await() 阻塞,其他线程每完成一件事就 countDown() 一次;计数归零时门闩打开,所有等待者一起放行。它是一次性的,用完即弃。CyclicBarrier 则像晨会集合:N 个线程各自到达后互相等待,全部到齐才一起出发,而且能循环使用——晨会开完,明天还能再开一轮。
Semaphore 像限流门票:构造时给 permit 数量,acquire() 拿一张票,release() 归还一张;没票的线程阻塞排队。它常用来控制并发数,比如数据库连接池最多 N 个连接、接口同时处理的请求数。三者底层都建立在 AQS 的共享模式之上:CountDownLatch 把 state 当剩余计数,CyclicBarrier 则用 ReentrantLock 加 Condition 实现“等齐”。
倒计时牌保证“人齐了才开张”,晨会保证“谁也不落单”,门票保证“场面不失控”。这三种节奏在微服务启动、批处理并发、接口限流中天天上演——理解了它们的语义区别,你才能在后厨里精确地说出“现在该用哪块牌”。
- count 是剩余计数初始化时由构造参数设为 N,底层存储在 AQS 的 state。
- countDown 减少计数每个线程完成一个前置任务就调用一次,计数减 1。
- await 检查并阻塞计数大于 0 时调用方挂起,最后一次 countDown 将计数归零时唤醒所有等待者。
- 一次性与循环的差异CountDownLatch 归零后不能重置;CyclicBarrier 到达后自动重置,可以循环使用。
线程池:主厨的排班表
后厨最考验功力的是排班。每来一桌客人都临时雇一个厨师,成本高、管理乱;只固定三个人,晚市高峰又忙不过来。主厨的解决方案是:养几个固定厨师(核心线程),忙起来再临时招人(最大线程),还忙就让客人排队(任务队列),实在接不下就拒单(拒绝策略)。这就是 ThreadPoolExecutor。
ThreadPoolExecutor 有七个参数,最重要的是四个:corePoolSize(核心线程数,常驻厨师)、maximumPoolSize(最大线程数,最多可招的人数)、workQueue(任务队列,等位区容量)、handler(拒绝策略,接不下怎么办)。执行流程一句话:先核心线程,再任务队列,再扩到最大线程,最后拒绝。
四种拒绝策略各有性格:AbortPolicy 直接抛异常(最有原则);CallerRunsPolicy 让提交任务的线程自己干(最负责);DiscardPolicy 悄悄丢弃(最省事);DiscardOldestPolicy 丢弃队首最旧任务(优化等待)。至于线程数怎么定,业界有一个经验公式:CPU 密集型线程数接近核数;IO 密集型因为线程大量时间在等 IO,可以多设。
排班表是后厨的命脉:核心厨师不轻易辞退(核心线程常驻),临时工空闲太久就辞退(keepAliveTime),排队区满了才扩招。这套机制避免了“每来一桌雇一个厨子”的浪费,也避免了无限招人导致后厨人挤人——线程太多时,光上下文切换就能把 CPU 拖垮。
- N_CPU 是物理核数机器真正能同时并行执行的线程数上限。
- W 是等待耗时线程因 IO、网络等原因阻塞等待的时间。
- C 是计算耗时线程真正占用 CPU 进行计算的时间。
- 等待占比决定倍数W/C 越大说明线程越闲,需要更多线程填补等待;CPU 密集型 W≈0,线程数约等于核数。
AI 应用场景
大模型在线推理服务的并发治理
阶段:模型部署与在线推理服务
解决的问题:大模型推理时显存和计算资源昂贵,请求无限涌入会导致 GPU 内存溢出或排队雪崩。传统做法是“来一个处理一个”,没有上限,服务极易被打垮。用 ThreadPoolExecutor 管理推理线程,配合 Semaphore 做令牌限流,可以把并发推理路数严格控制在 GPU 能承受的范围内。
具体案例:某 LLM 服务用核心线程数等于 GPU 卡数的两倍、最大线程数为四倍的线程池执行 forward 计算,再用 Semaphore(4) 做全局限流——同一时刻最多 4 个推理请求在跑,超出的请求进入队列等待或直接返回 429,GPU 不再 OOM。
AI 特征与模型元数据缓存
阶段:特征工程与在线 Serving
解决的问题:线上推理每个请求都要查特征、查模型版本,直接使用 HashMap 会被并发写破坏结构,每次加锁又太慢。ConcurrentHashMap 提供线程安全的读写且并发度高,非常适合做特征缓存。
具体案例:广告点击率预估服务把用户特征存入 ConcurrentHashMap,key 是用户 ID,value 是特征向量;模型版本列表用 CopyOnWriteArrayList 存储,支持线上无锁热切换——新版本发布时写入新数组,旧数组等读完了自动回收。
训练数据流水线同步
阶段:数据准备与分布式训练
解决的问题:分布式训练前要把多个数据分片下载并预处理,全部就绪才能开训;训练中数据加载(生产者)与 GPU 计算(消费者)速度不匹配,需要缓冲管道。用 CountDownLatch 等所有分片就绪,用 BlockingQueue 做生产消费缓冲。
具体案例:多卡训练启动脚本里,每个数据加载线程完成分片处理后调用 countDown(),主线程 await() 等计数归零后统一拉起训练进程;训练中的数据队列用 ArrayBlockingQueue(1024) 缓存样本,加载线程负责放、GPU 线程负责取,队满自动阻塞、队空自动等待。
Python 演示
"""Mini thread pool: imitate JUC ThreadPoolExecutor scheduling flow. core threads -> task queue -> expand threads -> reject""" import randomimport threadingimport timefrom queue import Queue class MiniThreadPool: """Use four core params to simulate ThreadPoolExecutor""" def __init__(self, core_size, max_size, queue_capacity): self.core_size = core_size self.max_size = max_size self.queue_capacity = queue_capacity self.task_queue = Queue(queue_capacity) self.worker_count = 0 self.rejected_count = 0 self._guard = threading.Lock() def submit(self, task): """Submit a task, follow JUC priority: core -> queue -> expand -> reject""" while True: with self._guard: if self.worker_count < self.core_size: self.worker_count += 1 threading.Thread(target=self._worker_loop, args=(task,)).start() return try: self.task_queue.put_nowait(task) return except Exception: with self._guard: if self.worker_count < self.max_size: self.worker_count += 1 threading.Thread(target=self._worker_loop, args=(task,)).start() return self.rejected_count += 1 print("[reject] queue is full and workers hit max") return def _worker_loop(self, first_task): """Handle first task, then pull more tasks from the queue""" task = first_task while task is not None: task() try: task = self.task_queue.get_nowait() except Exception: task = None with self._guard: self.worker_count -= 1 def cook_task(name): """Create a cooking task with random duration""" def task(): time.sleep(random.uniform(0.02, 0.1)) print(f"chef {name} finished a dish") return task if __name__ == "__main__": pool = MiniThreadPool(core_size=2, max_size=4, queue_capacity=2) for i in range(12): pool.submit(cook_task(i)) time.sleep(2) print(f"done, rejected={pool.rejected_count}")
self.task_queue = Queue(queue_capacity)等位区,对应 JUC 的workQueue阻塞队列,容量满了会抛异常。if self.worker_count < self.core_size:第一步:核心线程没满,直接新开一个厨师干活。self.task_queue.put_nowait(task)第二步:核心满了,任务进队列排队。if self.worker_count < self.max_size:第三步:队列也满了且还没到最大线程数,扩招临时工。self.rejected_count += 1第四步:达到最大线程数,任务被拒绝,对应AbortPolicy。threading.Thread(target=self._worker_loop, args=(task,)).start()新厨师上岗,开始执行第一个任务。task = self.task_queue.get_nowait()干完手里的活,再从队列取新任务,对应线程池 worker 的循环取活。
常见误区
误区synchronized 写法简单还能自动释放锁,ReentrantLock 要手动 unlock 很容易忘记,我项目里根本不用它,不也跑得好好的?
正解synchronized 无法响应中断、无法设置超时、无法公平排队,而且只有一个隐式的等待区。ReentrantLock 提供 tryLock(timeout)、lockInterruptibly()、公平锁和多个 Condition,对“抢锁不能无限等”和“精细控制等待队列”的高并发场景不可或缺。就像灶台通行证可以设有效期、可以排队,而 synchronized 是“谁嗓门大谁先用”,各有用武之地。
为何常见:日常业务中 synchronized 确实够用且更简单,看不到 ReentrantLock 在中间件、连接池、框架源码里的大量应用,容易得出“多余”的结论。
误区ConcurrentHashMap 不是线程安全的吗?那我 `get` 完再 `put`,两步操作合起来不也安全吗?
正解线程安全只保证单个方法内部不会被破坏,但“先读后写”的复合操作在多线程下会出现覆盖丢失。比如两个线程同时 get 到空值,又各自 put 自己算好的结果,其中一个结果就被悄悄覆盖了。正确做法是使用 compute、computeIfAbsent、merge,让“读-算-写”变成原子操作——就像“看冰格里有没有肉、再决定补货”这两步必须一气呵成,中间不许别人插手。
为何常见:把“方法级线程安全”误解成“业务级线程安全”,忽略了复合操作同样需要原子性保护。
误区系统变慢是因为线程不够用,把 `maximumPoolSize` 调到 1000 肯定没错,反正任务多。
正解线程越多,CPU 在保存和恢复线程上下文上的开销越大。当活跃线程数超过 CPU 核数,多出来的线程其实都在排队抢 CPU。CPU 密集型任务线程数约等于核数;IO 密集型按公式 核数 × (1 + W/C) 估算。盲目加大线程池只会让吞吐量下降,就像后厨塞进一百个厨师,光互相躲人就累得半死,炒菜效率反而更低。
为何常见:直觉上“人多好办事”,忽略了上下文切换成本和共享资源(CPU、内存、锁)的物理极限。
误区这俩都是让线程“等一等”再继续,用哪个都一样吧?我项目里混着用也没出过问题。
正解CountDownLatch 是“倒计时门闩”,N 个线程各自 countDown,等待者等到计数归零才放行,一次性使用;CyclicBarrier 是“集合屏障”,N 个线程互相等待,全部到齐后一起出发,且可以循环使用。两者的语义完全不同:一个是“我等你把活干完”,一个是“我们互相等齐再出发”。选错了,代码的并发行为会和预期相差甚远,且难以排查。
为何常见:两者都在做“等待”,表象相似,底层实现也不同(Latch 基于 AQS 共享模式,Barrier 基于 ReentrantLock + Condition),不深入原理就容易混淆。
面试题
Q1. ReentrantLock 的“可重入”是指什么?
- A.同一个线程可以重复获取同一把锁← 正确答案
- B.多个线程可以同时获取同一把锁
- C.锁可以被不同线程交替持有
- D.一个线程只能持有一把锁
答案:A。可重入的意思是持锁线程自己可以再次进入临界区而不被自己阻塞,A 正确。B 是共享锁的语义,C 是普通互斥,D 与重入无关。就像同一位厨师可以凭通行证连续用同一口锅,不用先还证再排队。
Q2. 关于 AQS(AbstractQueuedSynchronizer),下列说法正确的有?
- A.用 volatile 状态字段记录锁状态← 正确答案
- B.用 CLH 队列管理所有等待线程← 正确答案
- C.同时支持独占与共享两种模式← 正确答案
- D.所有等待线程均自旋且不阻塞
答案:ABC。AQS 核心就是 volatile int state 加 CLH 变体等待队列,支持独占(锁)和共享(信号量、门闩)两种模式,ABC 正确。等待线程获取锁失败后会挂起(park)而不是一直自旋空转,D 错误。AQS 就像叫号系统的规则本:排队、叫号、放行都有规矩。
Q3. ThreadPoolExecutor 提交任务后,正确的执行顺序是?
- A.核心线程、任务队列、扩增线程、拒绝← 正确答案
- B.扩增线程、任务队列、核心线程、拒绝
- C.任务队列、核心线程、扩增线程、拒绝
- D.核心线程、扩增线程、任务队列、拒绝
答案:A。标准顺序是:先尝试核心线程,核心满了任务进队列,队列满了才扩增线程到最大值,达到最大值后执行拒绝策略。这就像后厨先让固定厨师干,忙不过来就让客人排队,队也排不下才临时招人,招满就只能拒单。
Q4. 关于 JDK 8 的 ConcurrentHashMap,下列说法正确的有?
- A.采用 CAS 配合 synchronized 保证并发安全← 正确答案
- B.沿用了老的分段锁设计
- C.底层为数组加链表与红黑树← 正确答案
- D.所有数据共享同一把大锁
答案:AC。JDK 8 用 CAS 处理无竞争的操作,用 synchronized 锁桶的头节点,底层是数组加链表加红黑树,AC 正确。分段锁是 JDK 7 的设计,JDK 8 已放弃,B 错误;所有数据共享一把大锁是 Hashtable 的做法,D 错误。就像冰箱从“几个大抽屉各上一把锁”进化到“每个小格子都能单独锁”。
Q5. CountDownLatch 的核心用途是?
- A.倒计数到零一次性放行所有等待者← 正确答案
- B.每轮任务完成后计数自动重置归零
- C.让多个线程互相等待集合后再出发
- D.限制同时访问共享资源的线程数量
答案:A。CountDownLatch 是倒计时门闩,计数归零后一次性放行所有 await 的线程,且不能重置,A 正确。B 是循环语义,属于 CyclicBarrier;C 是屏障的互相等待;D 是 Semaphore 的限流语义。
Q6. CyclicBarrier 与 CountDownLatch 最大的区别是?
- A.屏障可以重复使用且线程间互相等待← 正确答案
- B.门闩可以重复使用且线程间互相等待
- C.两者功能与实现原理上完全相同
- D.屏障只能使用一次且不能循环重置
答案:A。CyclicBarrier 的屏障在到达后自动重置,可循环使用,且所有参与线程互相等待到齐再出发;CountDownLatch 是别人干活、你等计数,一次性。A 正确,BCD 均描述错误。就像晨会可以每天开(屏障),而开业倒计时牌只倒计时一次(门闩)。
Q7. 关于 Condition 的 await 方法,下列说法正确的是?
- A.await 类似于 wait 且可配多个条件队列← 正确答案
- B.await 与 wait 之间没有任何对应关系
- C.await 只能与 synchronized 搭配使用
- D.wait 内部拥有多个独立的等待队列
答案:A。Condition.await() 对应 Object.wait(),但一个 Lock 可以创建多个 Condition,各自管理独立的等待队列,A 正确。await 必须搭配 Lock 而非 synchronized,C 错误;wait 只有隐式的一个等待队列,D 错误。
Q8. 下列哪些是 ThreadPoolExecutor 常见的拒绝策略?
- A.抛出异常直接放弃该任务← 正确答案
- B.由提交任务的线程自行执行← 正确答案
- C.丢弃队列中最旧的一个任务← 正确答案
- D.无限扩大线程数量继续执行
答案:ABC。AbortPolicy 抛异常、CallerRunsPolicy 由提交者执行、DiscardOldestPolicy 丢弃队首最旧任务,ABC 都是标准拒绝策略。D 不符合线程池设计——maximumPoolSize 是硬上限,不会无限扩招。就像后厨满员时:要么拒单、要么让点菜服务员自己炒、要么扔掉最旧的单子,但绝不会无限塞人。
Q9. 对于 IO 密集型任务的线程池,核心线程数通常如何估算?
- A.直接等于 CPU 的核心数量
- B.等于 CPU 核心数的两倍
- C.按核数乘一加等待与计算比← 正确答案
- D.线程数越多性能就一定越好
答案:C。经验公式是 N_threads = N_CPU × (1 + W/C),IO 密集型任务 W 远大于 C,所以线程数要明显大于核数,C 正确。A 适合 CPU 密集型;B 是一个粗略经验值,不是“通常估算”的标准答案;D 忽略了上下文切换开销,错误。
Q10. 关于 ReentrantLock 的公平锁与非公平锁,下列说法正确的有?
- A.非公平锁的吞吐通常更高一些← 正确答案
- B.公平锁严格按排队的顺序获锁← 正确答案
- C.非公平锁可能导致线程饥饿← 正确答案
- D.公平锁能完全避免线程切换
答案:ABC。非公平锁允许新线程“插队”竞争,减少唤醒开销,吞吐通常更高,A 正确;公平锁按 CLH 队列顺序获取,B 正确;非公平锁极端情况下后到者反复插队,老线程可能长期拿不到锁,C 正确。公平锁只是排队更公平,唤醒等待线程依然会发生上下文切换,D 错误。就像排队买票:允许插队时柜台利用率高,但老实排队的人可能一直轮不上。