← 全部材料 Java 并发
JAVA 并发 · 15 个高频考点 · JDK 17 / 21

并发的题都出在「以为锁住了」

并发不难在概念,难在看上去对了其实没锁住:锁错了对象、volatile 当原子用、线程池队列无界、ThreadLocal 忘 remove。每一节都给一段能真跑出问题的代码,输出就是证据。

01先跑出问题再讲原理每个结论都配一段会出错的代码,丢了多少条、卡在哪里,输出里看得见。
02盯住「锁的对象」synchronized 锁谁、volatile 不锁什么、AQS 的 state 是什么,答得清这三点就赢一半。
03和前端经验对上单线程没有锁,但有并发控制:乐观更新、版本号冲突、Worker 里的 Atomics。
1

start 和 run 差一行,差一整个线程★

run 就是普通方法,start 才向系统要线程
很多人第一次写线程就踩这个坑:直接调 t.run(),代码照样跑,但跑在 main 线程里,一点并发都没有。start() 做的是另一件事——告诉 JVM「给我开一个新线程」,新线程起来后由它去回调 run()。线程对象只是一个句柄,只能启动一次。
输出调用 t.run() —— 就是一次普通方法调用 里面跑的线程: main 外面当前线程: main 调用 t.start() —— 才真的开新线程 里面跑的线程: worker-2 外面当前线程: main 同一个线程对象再 start 一次? 抛 IllegalThreadStateException:线程只能启动一次
面试怎么答start 才是真的启动线程,run 只是普通方法调用。start 里由 JVM 去申请内核线程、分配栈,再把状态改成 RUNNABLE 交给调度器,这些 run 都不做,所以直接调 run 会跑在当前线程里,写成并发其实是串行。另外线程只能启动一次,第二次会抛 IllegalThreadStateException。实际项目里我不用裸 Thread,统一走线程池,复用和监控都好做。
2

synchronized 锁的到底是哪个对象★

锁永远挂在对象上,写法决定挂哪个
synchronized 只有三种写法,锁的都是对象:修饰实例方法锁 this,修饰静态方法锁 类名.class,同步代码块锁括号里那个对象。所以「两个线程调同一个对象的同步方法会互斥、调两个不同对象的同步方法不会互斥」——因为根本是两把锁。
输出同一个对象,锁 this -> shared = 2 (期望 2) 两个对象,还是锁 this -> shared = 1 (期望 2,丢了更新) 静态方法,锁 Class -> shared = 2 (期望 2)
面试怎么答synchronized 锁的都是对象,只是写法决定锁哪个:实例方法锁 this,静态方法锁 Class 对象,同步块锁括号里的对象。所以两个线程操作不同实例时,各自的同步方法并不互斥,这是最常见的「加了锁却没锁住」。底层靠对象头的 Mark Word 记录锁状态,JDK 6 之后有无锁到偏向、轻量、重量级的升级,升级不可逆,目的是没竞争时别付系统调用代价。还有一点要避开:不要锁 String 字面量和 Integer 缓存对象,它们是全 JVM 共享的。
3

volatile 保证可见性,但不保证原子性★

一个 i++ 就把它拆穿了
volatile 干两件事:写完之后立刻刷到主内存、读之前必须从主内存拉最新的;顺便禁止指令重排。它不保证原子性——i++ 编译出来是「读、加一、写回」三条指令,volatile 只保证每一步看到的是最新值,不保证这中间没人插队。
输出(每次跑数字不同,但一定明显小于 400000)期望值 : 400000 volatile int++ : 177869 <- 丢了更新 AtomicInteger : 400000 <- 一次不少 少了多少个 : 222131
面试怎么答volatile 保证可见性和有序性,不保证原子性。可见性是写完立刻刷主内存、读之前必须重新拉;有序性是插入内存屏障,禁止指令重排。i++ 是读、加、写三条指令,volatile 只能保证每条指令拿到最新值,保证不了这三条不被打断,所以 4 个线程各加 10 万次,最后明显小于 40 万。要原子性得用 AtomicInteger 的 CAS 或者加锁。volatile 正确用法是状态标志和双重检查锁里的安全发布。
4

线程池七个参数,和那条容易背错的执行顺序★

核心满了先排队,队列满了才加班
提交一个任务,线程池按这个顺序处理:核心线程没满 → 建核心线程;核心满了 → 进队列;队列满了 → 建到最大线程数;还满 → 走拒绝策略。注意队列在「加线程」之前,这跟直觉相反,也是很多参数配了没效果的原因:队列无界时最大线程数永远用不上。
输出(任务执行行的先后会变,结构不会变) 跑任务01 -> w-1 提交01 成功 | 线程数 1 | 队列 0 提交02 成功 | 线程数 2 | 队列 0 提交03 成功 | 线程数 2 | 队列 1 提交04 成功 | 线程数 2 | 队列 2 提交05 成功 | 线程数 3 | 队列 2 提交06 成功 | 线程数 4 | 队列 2 跑任务02 -> w-2 跑任务05 -> w-3 跑任务06 -> w-4 提交07 被拒绝 | 线程数 4 | 队列 2 跑任务03 -> w-4 跑任务04 -> w-2 全部结束,一共创建了 4 个线程
面试怎么答七个参数是核心数、最大数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。执行顺序是核心线程满了进队列,队列满了才扩到最大线程数,再满就走拒绝策略——队列在扩线程之前,这一点最容易答错。所以如果给了一个无界队列,maximumPoolSize 永远不会生效。拒绝策略默认 AbortPolicy 抛异常,要背压就用 CallerRunsPolicy。另外 execute 抛的异常会打到线程的未捕获处理器,submit 会把它封进 Future,不 get 就看不到。
5

为什么规约禁止用 Executors 建线程池

它给的两个默认值都是无界的
Executors 的四个工厂方法,把两个最危险的参数都设成了无界:队列无界会让任务无限堆积到 OOM,最大线程数无界会无限创建线程拖死机器。所以规约要求手动 new ThreadPoolExecutor,把队列容量和线程数都写成有界的确定值。
输出newFixedThreadPool(2) 队列实现 LinkedBlockingQueue 队列剩余容量 2147483647 <- Integer.MAX_VALUE,等于无界 最大线程数 2 <- 队列永不慢,最大线程数永远用不上 newCachedThreadPool() 队列实现 SynchronousQueue <- 不存任务,直接交给线程 核心线程数 0 最大线程数 2147483647 <- 约 20 亿,来多少建多少 newScheduledThreadPool(1) 队列实现 DelayedWorkQueue(按延迟时间排序的堆) 队列剩余容量 2147483647 newSingleThreadExecutor() 返回类型 FinalizableDelegatedExecutorService <- 被包了一层,参数改不了,也查不到队列

会出事的地方

  • newFixedThreadPool:队列是 LinkedBlockingQueue,默认容量 Integer.MAX_VALUE。下游一慢,任务无限入队,每个任务还带着上下文对象,堆就慢慢涨没了——这是线上最典型的 OOM 来源之一。
  • newCachedThreadPool:最大线程数 Integer.MAX_VALUE,队列是 SynchronousQueue(不存任务)。一下涌进来一万个请求就建一万个线程,每个线程默认 1MB 栈,直接 unable to create native thread。
  • newSingleThreadExecutor:返回的是 FinalizableDelegatedExecutorService,一层包装类,你既强转不出 ThreadPoolExecutor,也没法在运行期调参数看队列。

正确写法

  • new ThreadPoolExecutor(8, 8, 0L, MILLISECONDS, new ArrayBlockingQueue<>(200), namedFactory, new AbortPolicy()):核心、最大、队列三个数都是确定的,出问题能算出来。
  • 给每个池起名,比如 order-、pay- 前缀。jstack 里一眼看出哪个池堵了,比翻代码快十倍。
  • 把池交给 Spring 管理,暴露 activeCount、queueSize、completedTaskCount 到监控,队列持续上涨就先扩容或限流。
  • 真正要做「一个请求一个线程」且能接受几万个并发时,才考虑虚拟线程(第 15 节),它不需要池化。
面试怎么答因为 Executors 的工厂方法把两个最关键的参数都设成了无界:newFixedThreadPool 和 newSingleThreadExecutor 用默认容量的 LinkedBlockingQueue,等于 Integer.MAX_VALUE,任务堆积就打爆堆;newCachedThreadPool 的 maximumPoolSize 也是 Integer.MAX_VALUE,来多少请求建多少线程,直接报 unable to create native thread。所以我都是手动 new ThreadPoolExecutor,核心数、最大数、有界队列、线程工厂、拒绝策略全写死,再挂监控看队列长度和活跃线程数。
6

核心线程数到底设多少

公式给起点,压测给答案
面试要答三层:CPU 密集取核数 + 1(多出来那个用来顶上偶发的页缺失);IO 密集取核数 × 2 起;更准确的是按等待比例 N × (1 + 等待时间/计算时间)。但公式只是起点,真正的值是压测出来的。
输出本机可用核数: 16 CPU 密集 N + 1 = 17 IO 密集 N × 2 = 32 等待占比 50% = 32 等待占比 90% W/C = 9 = 160 等待占比 99% W/C = 99 = 1600 堆上限: 8148 MB 提示: 线程数再大也压不住数据库连接池,先看下游上限
面试怎么答先给公式再讲怎么定。CPU 密集取核数加一,IO 密集取核数乘二,更准确是按等待比例算,核数乘一加等待比计算,等待 90% 就是十倍核数。但这个数字只是起点,实际要压测:固定 QPS 加线程,看吞吐的拐点和 RT 的上升。还有两个约束比公式重要:一是不能超过下游能力,比如数据库连接池,线程再多也只是排在连接池外面等;二是容器里要确认真实核数,JDK 8 早期在 K8s 里会读到宿主机核数,得用 ActiveProcessorCount 显式指定。
7

AQS:一个 state 加一条队列★

把「抢不到就排队」这件事标准化
AQS 的骨架就两样东西:一个 volatile int state 表示资源被占用的次数,一个双向链表(CLH 队列的变种)放抢不到的线程。子类只需要实现「怎么算抢到」和「怎么算释放」,入队、挂起、唤醒全在父类里做好了。整个 JUC 的锁、信号量、倒计时门闩都是这一套。
输出(t1/t2 谁先进队列不固定)main 先拿到锁,state 由 AQS 的 CAS 从 0 改成 1 t2 想拿锁 -> 进同步队列 t1 想拿锁 -> 进同步队列 此刻排队线程数: 2,队列非空: true 排队中的线程状态: WAITING / WAITING (AQS 用 LockSupport.park 挂起,所以是 WAITING) main 释放锁,AQS 唤醒队头 t2 拿到锁 t1 拿到锁 全部跑完
面试怎么答AQS 的核心是一个 volatile 的 int state 加一条双向的等待队列。子类只实现 tryAcquire 和 tryRelease 来定义什么叫抢到、什么叫释放,排队、park 挂起、唤醒重试这些模板逻辑都在父类。state 的语义完全由子类决定:ReentrantLock 里是重入次数,Semaphore 里是许可数,CountDownLatch 里是剩余计数,读写锁把高低 16 位分别给读和写。这样一套队列就支撑了整个 JUC 的同步器,这也是它能被拿来当面试题的原因。
8

ReentrantLock 比 synchronized 多了什么

功能多,代价是要自己放锁
性能上 1.6 之后两者差不多,synchronized 还有锁升级和锁消除优化,别再答「ReentrantLock 更快」。真正的差别是四个能力:等锁可中断、等锁可超时、可选公平、一个锁挂多条 Condition。代价是必须手动 unlock,忘了就是永久死锁。
输出tryLock(200ms) 拿到锁: false,等了 215ms 就放弃 子线程在等锁时被打断,干净退出,没拿到锁 同一线程连拿两次,holdCount = 2 放完两次,holdCount = 0,isLocked = false 公平锁按到达顺序拿到: 1 2 3 4
面试怎么答性能不是理由,JDK 6 之后两者基本持平,synchronized 还有偏向锁和锁消除。ReentrantLock 的价值在四个能力:tryLock 超时、lockInterruptibly 可中断、可指定公平锁、一个锁挂多个 Condition 做精确唤醒。典型场景是需要超时降级的、需要优雅停机响应中断的、需要按条件分开唤醒的生产者消费者。代价是必须 try/finally 里 unlock,忘了就永久死锁,所以我默认还是用 synchronized,只有确实需要这些能力才换。
9

ThreadLocal 的内存泄漏,和线程池里的脏数据★

两件事,都跟线程被复用有关
每个线程内部挂着一个 ThreadLocalMap,key 是 ThreadLocal 对象的弱引用,value 是强引用。所以 key 会被 GC 回收(变成 null),value 却因为被 Entry 强引用着收不掉——这条 entry 就永久占着内存,直到线程结束或者下次调用 set/get/remove 时被顺带清理。线程池里线程不死,所以「永久」就是真的永久。
输出任务2 读到: 用户A的登录态 <- 脏数据,线程是复用的 remove 之后任务4 读到: null ThreadLocalMap 数组长度: 16 key 还活着的 entry: 1 key 已被回收、value 还占着的 entry: 1,白占 65536 字节(这块要等线程死了才能收回)
面试怎么答ThreadLocal 每个线程内部有一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是强引用。key 被 GC 回收后 entry 的 key 变 null,但 value 还被 Entry 强引用着,收不掉,这就是泄漏的来源。清理动作只在 set/get/remove 的时候顺带做,线程池里线程不死、又不碰这个 key,value 就一直留着。所以第一是用完在 finally 里 remove,第二是别把大对象往里放。还有个更常见的问题:线程复用时忘清导致读到上一次请求的上下文,那是串号,比内存泄漏更严重。
10

ConcurrentHashMap 1.8 为什么扔掉分段锁★

锁的对象从一个段变成桶的头节点
1.7 是 16 个 Segment,每个继承 ReentrantLock,锁的粒度是一段;1.8 改成桶级别 + CAS:桶为空时用 CAS 直接放,桶不为空才 synchronized 锁住这个桶的头节点。锁的粒度细了一个量级,并发度从固定 16 变成跟桶数一样。
输出(第一行的数字每次不同,但一定远小于 80000)HashMap get + put 计数,期望 80000,实际 21835 <- 丢了 CHM merge 计数,期望 80000,实际 80000 <- 一次不少 CHM 批量 put,期望 80000 条,实际 80000 条 chm.put("k", null) 抛 NullPointerException:并发下 null 无法区分「没有」和「有但为空」 compute 后 apple = 2,pear 还是 9(锁的是桶的头节点,不是整张表) 遍历中删掉了 c,没抛 ConcurrentModificationException,剩余 0 条
面试怎么答1.7 用的是 Segment 分段锁,默认 16 个段,每个段继承 ReentrantLock,锁的粒度是一段,并发度固定 16。1.8 换成 CAS 加 synchronized 锁桶头节点:桶为空直接 CAS 放进去,桶不为空只锁这个桶的头,粒度细了一个量级,并发度跟着桶数走。数据结构也从 Segment 数组加 HashEntry 改成 Node 数组加链表和红黑树。size 用 baseCount 加 CounterCell 分散统计,所以是估算值。迭代器是弱一致的,遍历时可以改,但不保证看到新值。还有一点,它不允许 null 键值,因为并发下没法区分「没有」和「值为空」。
11

CAS 与 ABA:看起来没变,其实变过

无锁的代价是「只看值」
CAS 是「我认为现在是 A,如果确实是 A 就改成 B」,一条 CPU 指令完成比较和替换,不需要锁。它的漏洞叫 ABA:值从 A 变成 B 又变回 A,CAS 认为什么都没发生。带版本号的 AtomicStampedReference 就是给值配一个版本号,一起比。
输出[无版本号] A 读到余额 100,准备扣 30 [无版本号] B 已经把 100 取走又存回,余额看着还是 100 [无版本号] A 的 CAS 成功? true,余额 70 <- 中间的变化被完全掩盖 [带版本号] A 读到余额 100,版本 1 [带版本号] B 取走,版本变成 2 [带版本号] B 存回,版本变成 3,余额又看着是 100 [带版本号] A 的 CAS 成功? false <- 版本对不上,被拦住了 [带版本号] 余额仍然是 100
面试怎么答CAS 是一条 CPU 指令做到比较并替换,无锁但要自旋。ABA 的问题是只看当前值:如果被改成了别的值又改回来,CAS 就发现不了,中间发生的事被完全掩盖。可以用 AtomicStampedReference 给值配一个版本号一起比,或者用 AtomicMarkableReference 记个布尔标记。CAS 还有两个局限,一是竞争激烈时空转烧 CPU,二是只能保证一个变量的原子性,多个变量就得封装成一个对象或者加锁。所以高并发累加我会用 LongAdder,它是分散到多个 Cell 再求和。
12

死锁:从四个条件到 jstack 里那三行★

不只是理论,能当场复现并检测出来
死锁的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。破坏任意一个就行,实践中最容易的是破坏「循环等待」——给所有锁排一个全局顺序,永远按这个顺序拿。出问题时的定位手段是 jstack,它会把死锁线程、各持哪把、在等哪把直接打出来。
输出检测到死锁线程数: 2 线程 业务线程-1 状态 BLOCKED 卡在: java.lang.Object@37a71e93 等这把锁,被 业务线程-2 持有 线程 业务线程-2 状态 BLOCKED 卡在: java.lang.Object@4783da3f 等这把锁,被 业务线程-1 持有 两把锁的获取顺序相反 = 环路等待,死锁的四个必要条件全齐
面试怎么答死锁要同时满足互斥、持有并等待、不可剥夺、循环等待,破坏任意一个就能避免,工程上最常用的是给所有锁定一个全局顺序,永远按顺序获取,直接掐掉循环等待。定位靠 jstack,输出里会直接写 Found one Java-level deadlock,然后把每个线程持有什么锁、在等谁持有的锁列出来;代码里也能用 ThreadMXBean 的 findDeadlockedThreads 做到监控里。另外要区分开,线上线程全卡住更常见的原因不是死锁,而是连接池或线程池被占满造成的相互等待,jstack 里没有 deadlock 字样但状态一样是 WAITING。
13

六个线程状态,和 wait 与 sleep 的三处不同

为什么没有 RUNNING
JVM 把线程状态分成 6 个:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。没有 RUNNING——因为分不出「正在跑」和「就绪排队等 CPU」,JVM 把它们合并成 RUNNABLE。
输出刚 new 出来 : NEW 在跑 while(true) : RUNNABLE 在 Thread.sleep : TIMED_WAITING 在等 synchronized : BLOCKED 在 Object.wait : WAITING wait 被唤醒后跑完 : TERMINATED

sleep 和 wait 的差别

  • 锁:sleep 不释放持有的锁;wait 会释放锁,所以别人才能进来改条件。这是最常考的一条。
  • 归属:sleep 是 Thread 的静态方法,wait 是 Object 的方法。后者必须在持有该对象 monitor 的同步块里调,否则抛 IllegalMonitorStateException。
  • 唤醒:sleep 到点自己醒;wait 必须靠别人 notify/notifyAll(或带超时的 wait(ms))。没人通知就是永久 WAITING。

与其容易混的还有

  • join():等另一个线程结束,本质是 while (isAlive()) wait(0),所以状态也是 WAITING。
  • park()/unpark():LockSupport 的底层原语,不要求持锁,可以提前 unpark(发许可)。AQS 内部就用它,jstack 里栈顶是 Unsafe.park。
  • BLOCKED 只对应「等 synchronized 的 monitor」;等 ReentrantLock 时状态是 WAITING。这一点很多人答错。
面试怎么答六个状态是 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。没有 RUNNING,因为 JVM 不区分正在 CPU 上跑和就绪等调度,统称 RUNNABLE。BLOCKED 只用于等 synchronized 的 monitor,等 ReentrantLock 是 WAITING,因为它底层是 LockSupport.park。sleep 和 wait 三点不同:sleep 不释放锁、wait 释放锁;sleep 属于 Thread,wait 属于 Object 且必须在同步块里;sleep 到点自己醒,wait 要靠 notify。
14

CompletableFuture:把回调编排成链

thenApply 和 thenCompose 别搞反
thenApply 是把结果映射成另一个值(里面返回的是值),thenCompose 是把结果摊平成另一个异步任务(里面返回的是 CompletableFuture)。搞反了会得到 CompletableFuture<CompletableFuture<T>>,然后发现拿不到值。
输出thenCombine 合并两个异步结果: 108 没出错时 exceptionally 不碰: 正常数据 出错时走到兜底 : 兜底数据,原因: 下游超时 三个 300ms 的调用:串行 908ms,并行 313ms join 抛 CompletionException,真正原因在 cause: ArithmeticException
面试怎么答CompletableFuture 的核心是把异步回调串成链。thenApply 是同步映射,返回普通值;thenCompose 是摊平嵌套的异步任务,返回 CompletableFuture;搞反会拿到双层嵌套。合并用 thenCombine,等全部完成用 allOf,失败兜底用 exceptionally 或 handle。要注意两点:默认线程池是 ForkJoinPool.commonPool,并行度是核数减一,做 IO 必须换成自己的池,否则会互相拖累;还有 join 抛的是 CompletionException,get 抛的是 ExecutionException,真正原因都在 cause 里。
15

虚拟线程:不是更快,是更省★

JDK 21 正式版,这段真跑过
一个平台线程对应一个 OS 线程,创建成本高(默认 1MB 栈),所以只能开几千个,才有了「线程池」这种复用套路。虚拟线程是 JVM 自己实现的轻量线程,多个虚拟线程复用少量 OS 线程(叫载体线程);它一阻塞就把载体线程让出去,所以可以放心地开几十万个。价值在「省」——省内存、省线程池调参,不在「快」。
输出(JDK 21.0.12 真跑,具体毫秒数每次有波动)任务数 20000,每个任务 sleep 100ms 模拟一次下游调用 平台线程池(200 个线程): 10664 ms 虚拟线程(每任务一个) : 424 ms 虚拟线程是不是虚拟的: true,名字 demo-vt 提醒: 虚拟线程不要池化,ThreadLocal 也别往里塞大对象
面试怎么答虚拟线程是 JVM 层面的轻量线程,多个虚拟线程复用小量平台线程当载体,阻塞时自动让出载体,所以能开到几十万。它的价值是省,不是快:平台线程一个栈默认 1MB,两万个就开不出来,虚拟线程的栈在堆上按需增长。我实测过两万个任务各 sleep 100ms,200 个平台线程要 11 秒,虚拟线程 1.1 秒。要注意三点:不要池化,它的优势就是便宜不用复用;JDK 21 里在 synchronized 块内阻塞会钉住载体线程,退化成平台线程,这个到 JDK 24 才解决;它还只适合 IO 密集,CPU 密集用它没收益。
16

速答清单

42 条,考前扫一遍
这一节没有新知识,只有能直接说出口的短答案。盖住右边先自己答,答不上来的做个记号,第二轮只看记号。
01start() 和 run() 的区别?
start 才向系统申请线程并由新线程回调 run;直接调 run 是普通方法调用,跑在当前线程。
02线程能启动两次吗?
不能。第二次抛 IllegalThreadStateException,因为线程的栈和生命周期绑在 OS 线程上。
03Callable 和 Runnable 的区别?
Callable 有返回值、能抛受检异常,但要包 FutureTask 才能交给 Thread。
04synchronized 修饰静态方法锁的是什么?
锁 SyncLock.class 这个 Class 对象,全类共用一把,和实例锁互不影响。
05两个线程调不同对象的同步方法会互斥吗?
不会。锁的是各自的 this,是两把锁,各跑各的。
06锁升级的顺序?
无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(挂 monitor 排队),不可逆。
07为什么不要锁 String 字面量?
常量池里全 JVM 一份,不相干的代码锁同一个对象会互相干扰。
08volatile 保证原子性吗?
不保证。i++ 是读、加、写三条指令,volatile 只保证每条看到最新值。
09volatile 的两个语义?
可见性(写立刻刷主内存、读必须重新拉)和禁止指令重排(插内存屏障)。
10双重检查锁单例为什么加 volatile?
防止「引用已赋值但对象没构造完」的半成品被别的线程拿去用。
11线程池七个参数?
核心数、最大数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。
12线程池的执行顺序?
核心满了进队列,队列满了才扩到最大线程数,再满走拒绝策略。队列在扩线程之前。
13为什么 maximumPoolSize 常常不生效?
因为队列是无界的,永远排得进去,永远到不了「队列满」。
14四种拒绝策略?
AbortPolicy 抛异常、CallerRunsPolicy 交给提交线程跑(背压)、DiscardPolicy 静默丢、DiscardOldestPolicy 丢最老的。
15execute 和 submit 的异常差别?
execute 抛给线程的未捕获异常处理器;submit 封进 Future,不 get 看不到。
16shutdown 和 shutdownNow 的区别?
前者跑完队列里的任务再停,后者中断在跑的并把队列任务还给你。都不阻塞。
17为什么规约禁止用 Executors?
它给的队列和最大线程数都是无界的,一个会 OOM、一个会拖死机器。
18核心线程数怎么定?
CPU 密集 N+1,IO 密集 2N,更准确是 N×(1+等待/计算),最终靠压测。
19AQS 的两个核心组成?
一个 volatile int state 表示资源占用,一条双向等待队列放抢不到的线程。
20AQS 里 state 的语义由谁决定?
由子类决定:ReentrantLock 是重入次数,Semaphore 是许可数,CountDownLatch 是剩余计数。
21ReentrantLock 比 synchronized 多的能力?
等锁可中断、等锁可超时、可选公平锁、一个锁挂多条 Condition。
22ReentrantLock 最容易出的事故?
忘了在 finally 里 unlock,那个线程一异常锁就永不释放。
23ThreadLocal 为什么会内存泄漏?
ThreadLocalMap 的 key 是弱引用会被回收,value 是强引用收不掉,entry 就永久残留。
24ThreadLocal 怎么正确用?
用完在 finally 里 remove,尤其在线程池里,否则既泄漏又串号。
25InheritableThreadLocal 为什么在线程池里失效?
它只在创建子线程那一刻拷贝一次,线程池的线程是复用的,拷贝早过期了。
26ConcurrentHashMap 1.8 用什么保证线程安全?
桶为空用 CAS 直接放,桶不为空用 synchronized 锁这个桶的头节点。
27ConcurrentHashMap 的 size 精确吗?
不精确。baseCount 加 CounterCell 分散累加,size() 是估算值。
28ConcurrentHashMap 为什么不许 null?
并发下无法区分「没有这个 key」和「值本身就是 null」。
29ConcurrentHashMap 的迭代器是强一致的吗?
是弱一致。遍历时可以改,不抛异常,但不保证看到遍历开始后的修改。
30CAS 有什么问题?
自旋烧 CPU、只能保证一个变量的原子性、有 ABA 问题。
31ABA 怎么解决?
用 AtomicStampedReference 给值配版本号一起比,或用 AtomicMarkableReference 记布尔标记。
32死锁的四个必要条件?
互斥、持有并等待、不可剥夺、循环等待,破坏任意一个即可。
33线上死锁怎么查?
jps 找 pid,jstack 搜 Found one Java-level deadlock,下面会列持有和等待关系。
34BLOCKED 和 WAITING 的区别?
BLOCKED 只对应等 synchronized 的 monitor;等 ReentrantLock 是 WAITING(底层 park)。
35为什么没有 RUNNING 状态?
JVM 分不出正在跑和就绪等 CPU,合并成 RUNNABLE。
36sleep 和 wait 的三点不同?
sleep 不释放锁、wait 释放;sleep 属于 Thread、wait 属于 Object 且要在同步块里;sleep 自己醒、wait 要 notify。
37thenApply 和 thenCompose 的区别?
thenApply 返回普通值做映射;thenCompose 返回 CompletableFuture 做摊平,避免嵌套。
38CompletableFuture 默认用哪个线程池?
ForkJoinPool.commonPool(),并行度是核数减一,做 IO 必须换成自己的池。
39虚拟线程和平台线程的区别?
平台线程 1:1 对应 OS 线程;虚拟线程是 JVM 调度的,多个复用一个载体线程,阻塞时让出。
40虚拟线程适合什么场景?
IO 密集、高并发短任务。CPU 密集用它没有收益,因为瓶颈在核数。
41虚拟线程能池化吗?
不要。它本身很便宜,池化反而丢掉了「用完就丢」的优势。
42虚拟线程现在最大的坑?
JDK 21 在 synchronized 块里阻塞会钉住载体线程,退化成平台线程,JDK 24 才修好。