Java 后端真实面试专题 · 并发与多线程篇
并发是 10–25k 后端岗的必考区,问得深、追问狠。每题三段: ① 标准答(讲透:是什么+为什么+怎么做)→ ② 拓展(成体系带出关联点和面试官会追问的,答一题等于答一片)→ ③ 怎么接到你自己的项目(背八股的人答不出这段)。
年限标签:
🟢 3年内🔴 3年+这一篇的答法本身就是教学:别人问一个点,你成体系地答一片——这才是面试官眼里的"懂"。
1. 🟢 线程有哪几种状态?是怎么流转的?
标准答:Java 线程有 6 种状态(Thread.State 枚举):
- NEW:创建了还没
start()。 - RUNNABLE:调了
start(),包含"就绪"和"运行中"——Java 没区分这两者,因为是否真正占用 CPU 由操作系统调度,JVM 层看不到。 - BLOCKED:等待进入 synchronized 同步块、抢锁失败被阻塞。
- WAITING:调了
wait()/join()/LockSupport.park(),无限期等,要别人唤醒。 - TIMED_WAITING:带时间的等待,如
sleep(n)、wait(n)、join(n)。 - TERMINATED:run 方法执行完或异常退出。
拓展:面试官常顺着追问:
- "BLOCKED 和 WAITING 区别?"——BLOCKED 是等锁(被动,抢 synchronized 没抢到),WAITING 是主动等通知(调了 wait/park)。
- "操作系统层面线程有几种状态?"——新建、就绪、运行、阻塞、终止,Java 的 RUNNABLE 对应了就绪+运行两个。
- "线程状态在哪看?"——
jstack导出线程栈,每个线程都标了状态,线上排查全靠它。 - 状态不能跳——比如 NEW 不能直接到 RUNNING,必须经 start。
往项目引 ⭐:"理解状态对排查线上很关键。我项目有次接口大面积超时,jstack 一看几十个线程全 BLOCKED 在同一把锁上,立刻判断是锁竞争,定位到一段范围过大的 synchronized,缩小锁粒度后解决——状态不是背的,是排查问题的工具。"
2. 🟢 sleep 和 wait 的区别?
标准答:最本质一句话——sleep 抱着锁睡,wait 放锁等。
sleep是 Thread 的静态方法,让当前线程暂停指定时间、不释放持有的锁,到点自动回到就绪状态。wait是 Object 的方法,调用后当前线程释放该对象的锁、进入对象的等待队列(WAITING),必须由其他线程调同一对象的notify/notifyAll才能唤醒。- 两者都能响应中断。
拓展:这题能引出一大片,面试官最爱追:
- "wait/notify 为什么定义在 Object 而不是 Thread?"——锁是绑在任意对象上的,等待/唤醒针对的是"对象监视器(monitor)",所以必须是 Object 的方法。
- "为什么 wait 必须在 synchronized 里调用?"——调用前必须先持有该对象的 monitor,否则抛
IllegalMonitorStateException。 - "notify 和 notifyAll 区别?"——notify 随机唤醒一个等待线程(可能信号丢失),notifyAll 唤醒全部再重新竞争锁,生产上一般用 notifyAll 更安全。
- "wait 为什么要用 while 而不是 if 判断条件?"——防止"虚假唤醒",唤醒后要重新检查条件。
- 进阶对比
LockSupport.park/unpark:不需要持锁、更灵活,AQS 底层用的就是它。
往项目引 ⭐:"我项目里很少直接写 wait/notify,而是用更上层的 BlockingQueue(它内部就是等待-通知机制)。比如订单异步处理做缓冲队列,消费线程 take() 时队列空了自动阻塞、生产者 put 进来自动唤醒,比手写 wait/notify 安全得多。"
3. 🟢 创建线程有几种方式?为什么实际只用线程池?
标准答:四种——① 继承 Thread 重写 run;② 实现 Runnable(推荐,避免单继承限制);③ 实现 Callable + FutureTask(能拿返回值、能抛异常);④ 线程池。实际开发只用线程池,前三种是基础原理。
拓展:
- "为什么不手动 new Thread?"——频繁创建销毁开销大、线程不可复用、数量不可控(并发一高线程暴涨直接 OOM)。阿里开发规约强制要求用线程池。
- Runnable 和 Callable 区别——Callable 有返回值
call()、能抛受检异常,配合 Future 拿结果。 - Future 的问题——
get()会阻塞,所以 JDK8 出了 CompletableFuture 做异步编排。 - 本质上四种方式的"任务"和"执行"是分离的,线程池就是把"执行"复用起来。
往项目引 ⭐:"我项目所有异步任务都走自定义线程池。比如商品批量导入,用 Callable 把每一批丢进线程池并行处理、再用 Future 收集结果,十万条从几分钟降到几十秒——既复用线程又能拿到每批的处理结果。"
4. 🟢 并发编程的三大特性是什么?
标准答:
- 原子性:一个或多个操作要么全做完、要么都不做,中间不被打断。靠 synchronized、Lock、Atomic 类保证。
- 可见性:一个线程改了共享变量,其他线程能立刻看到。靠 volatile、synchronized、final 保证(每个线程有自己的工作内存,改了不刷主存别人看不到)。
- 有序性:程序执行顺序按代码先后(实际 CPU/编译器会指令重排)。靠 volatile(内存屏障)、synchronized 和 happens-before 规则保证。
拓展:
- "
i++为什么线程不安全?"——它是"读-改-写"三步,不满足原子性,多线程会丢更新。 - "volatile 能保证原子性吗?"——不能,只保证可见性和有序性,所以计数要用
AtomicLong。 - "什么是指令重排?为什么允许?"——为了优化性能,单线程下重排不影响结果(as-if-serial),但多线程下会出问题。
- happens-before 是判断"是否存在数据竞争"的核心规则,比如解锁 happens-before 后续加锁。
- 三大特性是并发所有问题的根,几乎所有并发工具都是在解决这三个中的某一个。
往项目引 ⭐:"我项目里按'缺哪个特性补哪个'来选工具:优雅停机的状态开关只要可见性,用 volatile;并发计数要原子性,用 AtomicLong;复合操作要原子性+互斥,才上 synchronized/Lock。而不是无脑一把锁锁到底,那样性能差。"
5. 🟢 线程池的核心参数有哪些?一个任务提交进来的完整流程?
标准答:ThreadPoolExecutor 七个参数——核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、空闲线程存活时间(keepAliveTime)、时间单位、阻塞队列(workQueue)、线程工厂(threadFactory)、拒绝策略(handler)。
提交一个任务的流程:① 核心线程没满 → 创建核心线程执行;② 核心线程满了 → 进阻塞队列排队;③ 队列也满了 → 创建非核心线程(直到最大线程数);④ 达到最大线程数且队列满 → 触发拒绝策略。
拓展:
- 高频追问"先创建线程还是先入队?"——先入队、再扩线程,很多人答反。原因是入队比创建线程代价小。
- "keepAliveTime 对核心线程生效吗?"——默认不,除非设
allowCoreThreadTimeOut(true)。 - 阻塞队列怎么选——有界队列(ArrayBlockingQueue/有界 LinkedBlockingQueue)防止任务无限堆积 OOM;SynchronousQueue 不存任务直接交付。
- 这套"先核心→再队列→再扩容→再拒绝"的设计哲学:优先复用、其次缓冲、最后才扩张和拒绝。
往项目引 ⭐:"我项目按业务隔离了多个线程池——订单、消息推送各用各的,避免一个业务把线程占满拖垮另一个(线程池隔离)。核心数是压测后定的,队列用有界队列,宁可触发拒绝策略也不让任务无限堆积把内存打爆。"
6. 🟢 线程池的拒绝策略有哪些?生产上你用哪种?
标准答:JDK 内置四种——
- AbortPolicy(默认):直接抛
RejectedExecutionException。 - CallerRunsPolicy:让提交任务的线程自己执行该任务(相当于"反压",降低提交速度)。
- DiscardPolicy:默默丢弃新任务,不报错。
- DiscardOldestPolicy:丢掉队列里最老的,再尝试提交。
拓展:
- 生产一般不用默认的——抛异常会丢任务且影响主流程。
- CallerRunsPolicy 适合"不能丢任务、宁可慢"的场景,用调用线程执行天然限流。
- 大多数情况会自定义拒绝策略:记日志、报警、把任务落库或丢 MQ 后续补偿。
- 拒绝策略触发说明池子已经扛不住了,要顺带排查是不是参数设小了或下游慢了。
往项目引 ⭐:"我项目自定义了拒绝策略:任务被拒时先记日志报警,再把任务持久化到 DB / 丢进 MQ,等池子空闲了补偿执行,保证重要任务(如订单后续处理)不丢——而不是默认抛异常把任务弄丢了。"
7. 🔴 为什么不建议用 Executors 直接创建线程池?
标准答:阿里规约明确禁止,因为 Executors 的工厂方法藏了 OOM 风险:
newFixedThreadPool和newSingleThreadExecutor用的是无界队列 LinkedBlockingQueue,任务堆积会撑爆内存。newCachedThreadPool和newScheduledThreadPool的最大线程数是 Integer.MAX_VALUE,线程能无限创建,也会 OOM。 所以要用new ThreadPoolExecutor(...)手动指定有界队列和合理的最大线程数。
拓展:
- 这题本质考"你是不是真的踩过/懂线程池参数",背过的人才知道。
- 延伸到"队列怎么选"——核心是有界,给一个能接受的堆积上限。
- 再延伸"线程数怎么定"(见下一题)。
往项目引 ⭐:"我项目所有线程池都是 new ThreadPoolExecutor 手动建、统一封装成工具类,用有界队列 + 自定义拒绝策略 + 有意义的线程名(方便 jstack 排查)。就是因为知道 Executors 的无界队列在流量高峰会把内存打爆。"
8. 🔴 线程池的线程数怎么设置?
标准答:看任务类型——
- CPU 密集型(大量计算):设
CPU 核数 + 1,线程太多只会增加上下文切换开销。 - IO 密集型(大量读写库/网络):线程多数时间在等 IO,可设
核数 * 2甚至更高。 - 更精确的经验公式:
线程数 = 核数 × (1 + IO 耗时 / CPU 耗时)。 - 最终都要压测,按吞吐和响应时间调到最优,公式只是起点。
拓展:
- "为什么 CPU 密集不能开太多线程?"——核数就那么多,线程多了只是轮流切换,切换本身耗 CPU。
- "怎么判断是 CPU 还是 IO 密集?"——看任务在算还是在等;可用监控看 CPU 利用率。
- 实际业务大多是 IO 密集(查库、调接口)。
- 还可以做动态线程池(如美团 DynamicTp),运行时调参数不重启。
往项目引 ⭐:"我项目导入任务是典型 IO 密集(大量读写库),线程数设得比核数高很多,再根据压测的吞吐曲线微调;而做图片处理那种 CPU 密集的池子就设核数附近,避免无谓切换。"
9. 🟢 synchronized 和 ReentrantLock 的区别?
标准答:
| synchronized | ReentrantLock | |
|---|---|---|
| 本质 | JVM 关键字,自动加解锁 | JUC 的 API,手动 lock/unlock |
| 释放 | 自动(出块/异常) | 必须 finally 里 unlock,否则死锁 |
| 中断 | 不可中断 | 可中断(lockInterruptibly) |
| 公平 | 只能非公平 | 可选公平/非公平 |
| 条件 | 一个等待队列 | 可绑定多个 Condition,精准唤醒 |
| 尝试 | 不支持 | tryLock 可带超时 |
| 两者都是可重入锁。 |
拓展:
- synchronized 的锁升级(无锁→偏向锁→轻量级锁→重量级锁,见下题)让它 JDK6 后已经不慢,简单同步优先用它,代码也更简洁不易出错。
- ReentrantLock 的优势场景:需要 tryLock 超时、需要可中断、需要多个 Condition(如阻塞队列的"非空"和"非满"两个条件)。
- "可重入"是什么——同一线程能重复获取自己已持有的锁,避免自己把自己锁死。
- 读多写少还可以用 ReentrantReadWriteLock 或 StampedLock 提升并发。
往项目引 ⭐:"我项目里大多数同步用 synchronized 就够、简洁可靠;只有一个抢占式任务调度的场景用了 ReentrantLock 的 tryLock(超时)——抢不到锁的线程不能一直死等,超时就放弃去干别的,这是 synchronized 给不了的。"
10. 🔴 synchronized 的锁升级过程?锁信息存在哪?
标准答:为了减少加锁开销,synchronized 的锁会随竞争加剧逐步升级(只能升不能降):
- 无锁 → 偏向锁:只有一个线程访问,在对象头 Mark Word 里记下该线程 id,下次进入无需 CAS。
- 偏向锁 → 轻量级锁:出现第二个线程竞争,用 CAS 自旋尝试获取,适合竞争不激烈、锁持有时间短。
- 轻量级锁 → 重量级锁:自旋超过阈值/竞争激烈,膨胀为重量级锁,靠操作系统互斥量(monitor),抢不到的线程真正阻塞挂起。 锁状态存在对象头的 Mark Word 里。
拓展:
- "为什么要锁升级?"——大多数情况锁竞争不激烈,重量级锁的阻塞/唤醒要切到内核态、很贵,所以先用轻量级方案。
- 偏向锁在高并发反复竞争下有撤销开销,JDK15 后默认禁用了偏向锁。
- 自旋是"忙等",消耗 CPU 但避免线程切换,适合锁很快释放的场景。
- 引申到对象内存布局:对象头(Mark Word + 类型指针)+ 实例数据 + 对齐填充。
往项目引 ⭐:"理解锁升级让我对 synchronized 有底——它在低竞争下其实很轻量。所以项目里简单的同步我放心用 synchronized,不会一上来就换成 Lock '显得高级',反而把代码搞复杂。"
11. 🟢 volatile 的作用和底层原理?能保证原子性吗?
标准答:volatile 保证两点——
- 可见性:写 volatile 变量会立刻刷回主存,读会从主存读最新值。
- 有序性:通过插入内存屏障禁止指令重排。
不保证原子性(如
volatile int i; i++仍线程不安全)。底层靠 CPU 的 lock 前缀指令 + 内存屏障实现。
拓展:
- "经典应用?"——双重检查锁(DCL)单例的 instance 必须加 volatile,否则可能因为"new 对象不是原子的(分配内存→初始化→赋引用,可能重排)"而拿到半初始化对象。
- "和 synchronized 比?"——volatile 只保证可见性/有序、更轻量、不阻塞;synchronized 还保证原子性、会互斥。
- 适用场景:一写多读的状态标志位。
- 要原子复合操作就上 Atomic 或锁。
往项目引 ⭐:"我项目优雅停机用 volatile boolean running 做标志位——主线程改成 false,各工作线程下一轮循环立刻可见并退出,不用加锁。但需要并发累加的计数我一律用 AtomicLong,因为 volatile 保证不了 ++ 的原子性。"
12. 🔴 CAS 是什么?有哪些问题?怎么解决?
标准答:CAS(Compare-And-Swap)是一种乐观的无锁并发手段:比较内存中的值和预期值,相等才把它更新为新值,整个过程由 CPU 指令保证原子。它是 JUC 里 Atomic 类、AQS 的底层基石。 三个问题:
- ABA:值从 A 改成 B 又改回 A,CAS 看不出变过。解决:加版本号,用
AtomicStampedReference。 - 自旋开销:一直 CAS 失败就一直循环,空耗 CPU。
- 只能保证一个变量的原子操作。解决:把多个变量包成一个对象,用
AtomicReference。
拓展:
- "CAS 和加锁比好在哪?"——无锁、不阻塞线程、没有线程切换开销,适合竞争不激烈的场景。
- Atomic 类底层就是"CAS + 自旋"(
getAndIncrement循环 CAS 直到成功)。 - 竞争激烈时 CAS 自旋失败率高,JDK8 的 LongAdder 用分段(Cell)思想分散竞争,比 AtomicLong 更适合高并发计数。
往项目引 ⭐:"我项目的库存扣减本质就是 CAS 思想——update stock set stock=stock-1 where id=? and stock>0,这是数据库层的乐观锁:不加悲观锁、靠条件更新,影响行数为 0 就说明卖光了。既防了超卖又避免了悲观锁的性能损耗。"
13. 🔴 AQS 的原理是什么?
标准答:AQS(AbstractQueuedSynchronizer)是 ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 的共同底层。核心两部分:
- 一个 volatile 的 state:表示同步状态(如锁的重入次数、信号量的剩余许可)。
- 一个 CLH 变体的双向队列:抢不到资源的线程包装成 Node 入队、阻塞等待。 线程通过 CAS 改 state 来争夺资源,成功则执行、失败则入队挂起(LockSupport.park),释放时唤醒队首。
拓展:
- 两种模式:独占(ReentrantLock,一次一个线程拿 state)、共享(CountDownLatch/Semaphore,多个线程可同时拿)。
- 公平锁 vs 非公平锁:公平锁严格按队列顺序、非公平锁允许新来的线程直接抢(吞吐更高,ReentrantLock 默认非公平)。
- AQS 用了模板方法模式,子类只需实现 tryAcquire/tryRelease。
- 底层挂起/唤醒用 LockSupport.park/unpark(不需要持锁,比 wait/notify 灵活)。
往项目引 ⭐:"这题偏底层,我面试会这么答:'我用过基于 AQS 的工具——ReentrantLock 做互斥、CountDownLatch 等多个并行任务完成、Semaphore 做并发数限流,并理解它们底层都是 state + 等待队列',把理论稳稳接到我真实用过的类上,而不是空背源码。"
14. 🟢 ConcurrentHashMap 是怎么保证线程安全的?和 HashMap、Hashtable 的区别?
标准答:JDK8 的 ConcurrentHashMap 用 数组 + 链表/红黑树,put 时:桶为空用 CAS 放入;桶非空对该桶的头节点 synchronized 加锁,只锁单个桶。所以并发度高(理论上等于桶数量)。
- HashMap:线程不安全,并发 put 会丢数据、JDK7 还会扩容成环。
- Hashtable:用 synchronized 锁整个表,安全但性能差。
- ConcurrentHashMap:锁桶粒度,安全且高效。
拓展:
- "JDK7 和 8 的实现差异?"——JDK7 用分段锁(Segment,默认 16 段),JDK8 改成 CAS + synchronized 锁单桶,粒度更细。
- "size() 怎么算的?"——用 baseCount + CounterCell 数组分散统计,避免单点竞争。
- "key/value 能为 null 吗?"——不能,因为并发下 null 无法区分"不存在"还是"值为 null"。
- 扩容时支持多线程协助迁移(transfer),提升扩容速度。
往项目引 ⭐:"我项目里本地缓存、并发计数这些都用 ConcurrentHashMap,而不是 Collections.synchronizedMap(那是锁整个 map、性能差)。比如统计各接口调用量,用 ConcurrentHashMap<String, LongAdder>,高并发下也准也快。"
15. 🟢 乐观锁和悲观锁的区别?分别用在什么场景?
标准答:
- 悲观锁:假设并发一定有冲突,操作前先加锁、独占资源。如 synchronized、
select ... for update。 - 乐观锁:假设冲突很少,不加锁直接操作,更新时校验有没有被别人改过。如版本号机制、CAS。 选择:读多写少、冲突概率低用乐观锁(少了加锁开销);写多、冲突激烈用悲观锁(乐观锁会大量重试反而更差)。
拓展:
- 乐观锁实现版本号:表加 version 字段,更新时
where version=旧值,成功才说明没被改、并把 version+1。 - CAS 也是乐观锁思想。
- 悲观锁要注意锁范围和死锁。
- 数据库乐观锁失败后业务上要决定是重试还是报错给用户。
往项目引 ⭐:"我项目库存扣减用乐观锁(stock>0 条件更新),并发高、冲突可接受、失败就提示'已抢完';而账户转账这种要绝对一致的,用 select for update 悲观锁锁住两个账户,避免中间被改。"
16. 🟢 CountDownLatch、CyclicBarrier、Semaphore 的区别?
标准答:
- CountDownLatch(倒计数门闩):让一个/多个线程等待另一组线程完成。
countDown()减一、await()等到归零。一次性,不能重用。 - CyclicBarrier(循环栅栏):让一组线程互相等待,都到齐了再一起继续。可重复使用,还能设到齐后执行的回调。
- Semaphore(信号量):控制同时访问某资源的线程数,
acquire()拿许可、release()还,常用于限流。
拓展:
- CountDownLatch 是"一个等多个"或"多个等一个开始",CyclicBarrier 是"多个互相等齐"。
- CountDownLatch 计数到 0 不能复位,要重复用得换 CyclicBarrier。
- 三者底层都是 AQS(前两个共享模式,Semaphore 控制 state 为许可数)。
往项目引 ⭐:"我项目首页要并行查商品、库存、营销三个服务,全部回来再聚合返回,就用 CountDownLatch(3),三个查询各 countDown 一次、主线程 await 等齐;接口并发保护用 Semaphore 限制同时处理的请求数。"
17. 🔴 CompletableFuture 怎么用?解决了什么问题?
标准答:CompletableFuture 是 JDK8 的异步编排工具,解决了 Future get() 阻塞、无法编排依赖、回调地狱的问题。常用:
supplyAsync异步执行有返回值的任务;thenApply/thenAccept对结果做转换;thenCompose串联有依赖的两个异步任务;thenCombine合并两个独立任务的结果;allOf/anyOf等待全部/任一完成。
拓展:
- "要不要指定线程池?"——必须传自定义线程池,否则默认用 ForkJoinPool 公共池,被一个慢任务占满会影响全局。
- 异常处理用
exceptionally/handle,别让异步异常被吞掉。 - 和 Future 比:Future 只能阻塞 get 或轮询,CompletableFuture 能声明式编排。
往项目引 ⭐:"我项目首页聚合接口原来串行调三个服务要 2 秒,改成 CompletableFuture 用自定义线程池并行发起、thenCombine 汇总,整体耗时变成最慢的那个服务(几百毫秒)。这是'多接口并行聚合'的标准做法,面试官很爱听。"
18. 🔴 怎么保证多个线程的执行结果是有序的?
标准答:线程调度本身不保证顺序,要"有序"得靠编排:
- 结果有序:给每个任务带序号,并行执行、最后按序号重排结果。
- 执行有序(严格串行):用单线程池,或用 CompletableFuture 的
thenCompose串起来。 - 等齐再走:CountDownLatch / CyclicBarrier。 别指望"按提交顺序进线程池就会按顺序执行"。
拓展:
- "需要顺序消费消息怎么办?"——把同一类消息发到同一队列/分区,单线程消费(引到 MQ 顺序消费)。
- 并行 + 有序往往是"并行计算、串行汇总"。
往项目引 ⭐:"我项目批量处理要并行提速、但结果必须按原顺序返回给前端,我给每个任务带上索引并行跑,最后用索引把结果重新排好,既拿到了并行的速度又保住了顺序。"
19. 🟢 ThreadLocal 是什么?底层原理?有什么坑?
标准答:ThreadLocal 给每个线程一份独立变量副本,实现线程内的数据隔离与传递。底层:每个 Thread 对象里有一个 ThreadLocalMap,key 是 ThreadLocal 对象(弱引用)、value 是值。get/set 操作的是当前线程自己的这个 map。
拓展:
- "为什么会内存泄漏?"——key 是弱引用会被 GC 回收,但 value 是强引用还挂在 map 上,线程池线程长期复用就会堆积。所以用完一定
remove()(最好放 finally)。 - "key 为什么用弱引用?"——为了 ThreadLocal 对象本身能被回收,是一种缓解措施,但不能替代 remove。
- "父子线程怎么传值?"——
InheritableThreadLocal,但线程池场景下要用阿里的TransmittableThreadLocal(TTL)。
往项目引 ⭐:"我项目用 ThreadLocal 存当前登录用户和租户 id——请求进来在拦截器里 set、整条调用链都能取到、请求结束在 finally 里 remove 防泄漏。多租户 SaaS 这套几乎是标配,也是它最典型的真实用法。"
20. 🔴 死锁是怎么产生的?怎么定位和避免?
标准答:死锁是两个以上线程互相持有对方需要的锁、谁也不放,永久等待。产生需同时满足四个条件——互斥、持有并等待、不可剥夺、循环等待。破坏任意一个即可避免,工程上最常用是破坏"循环等待":统一加锁顺序。
拓展:
- "怎么定位死锁?"——
jstack会直接打印Found one Java-level deadlock和涉及的线程与锁;也能用jconsole/Arthas 检测。 - 其他避免手段:用
tryLock(超时)拿不到就放弃(破坏"不可剥夺/持有并等待");减少锁持有时间和范围;尽量用无锁结构。 - 死锁、活锁、饥饿的区别——活锁是不断重试但都让步谁也不前进,饥饿是某线程一直抢不到资源。
往项目引 ⭐:"我项目转账场景两个账户互转曾经死锁——A 转 B 锁了 A 等 B,B 转 A 锁了 B 等 A。后来统一规则'按账户 id 从小到大依次加锁',破坏了循环等待,问题根治。这是死锁避免最经典也最实用的手段。"
你能答到第几层?
- 三段都能答、还能往项目引:你是面试里的少数派,冲 18k+ 没问题。
- 标准答 + 拓展能成体系答:知识扎实,差把它绑到你真做过的项目上。
- 标准答都磕巴:别急,并发是有主线的(三大特性 → 锁 → JUC 工具 → 线程池),按主线学一遍就通。
这是面试专题的「并发篇」,网站上还有 MySQL、Redis、Spring、微服务、项目场景等系统整理。 🌐 更多真实面试专题与资料:smallredtech.com 💬 想系统学 / 简历与辅导咨询,加微信:Ahongbb666(备注「面试题」)