Java多线程进阶小结:从线程池到数据一致性的并发实战

发布时间:2026/10/1 13:43:41
Java多线程进阶小结:从线程池到数据一致性的并发实战
我先说个真事。去年我帮一个朋友排查线上问题服务在高峰期突然卡死日志最后一条停在某个批量任务里后面什么都没打出来。我让他先jstack抓线程快照结果一抓就发现问题——几十个线程全部阻塞在同一个锁上而持有锁的那个线程自己又因为等另一个锁的结果直接睡死了。说白了就是多线程没理清楚把并发资源当成无限资源用了。从那以后我特别认同一个观点多线程不是“开几个线程跑任务”那么简单的 API 调用它是一整套关于共享、交互、协作的资源管理思路。今天这篇JAVA 进阶 THREAD 学习 12 多线程小结就是把我这些年折腾JAVA 多线程的笔记重新整理了一遍给正在啃并发、准备java 面试题、或者在生产上踩过坑的朋友做一个阶段性总结。如果你是刚接触并发不久建议先抓几个主线线程怎么创建和停止、JMM 到底在约束什么、锁和同步工具各自的适用场景、线程池参数怎么调。如果你已经写过一些并发代码那重点看后面的数据一致性案例和排查思路这部分是我实际项目里摔过跟头之后的沉淀。下面内容大致分七块聊从概念到工具从原理到踩坑尽量做到每一步都有代码、有依据、有坑点方便你直接照着用。1. 先搞清楚多线程到底在解决什么问题1.1 从一次“卡死”开始并发与并行的区别很多初学者会把“并发”和“并行”混成一回事其实这俩在工程上的差别特别大。并发是指多个任务在同一个时间段内交替执行你以为它在同时跑其实大家都在轮流占用 CPU 时间片并行才是真正多个核心同时执行多个任务。举个生活化的例子你一个人同时烧水、切菜、看锅这叫并发——你只是在不同的步骤之间来回切换如果来了两个厨师一个烧水一个切菜那就是并行。Java 的多线程模型天然是并发的线程能不能真并行取决于运行机器的 CPU 核数。我见过不少同学在一台四核机器上起了 200 个线程结果不仅没有变快反而因为上下文切换和锁竞争把性能拖垮了。这里有个非常关键的认知线程不是越多越好。理想的线程数大致可以按“CPU 密集型用 核数1、IO 密集型用 核数×2 左右”这样一个经验值起步再结合实际压测去调整。这不是什么高深理论就是让你别在资源使用上犯低级错误。1.2 进程、线程与协程的边界进程是操作系统分配资源的基本单位每个进程有自己独立的内存空间、文件句柄、环境变量线程是进程内的执行单元同一个进程内的线程共享堆内存、方法区、打开的文件等资源但每个线程有自己的虚拟机栈和程序计数器。所以多线程编程最爽的地方是“共享容易”最危险的地方也是“共享容易”——两个线程对着同一个变量改来改去如果不同步轻则数据错乱重则直接把你整懵。Java 里的线程还经常被人拿来和协程比较。协程是用户态调度的更加轻量的执行单元切换开销远小于线程。不过 Java 传统上只有线程模型直到虚拟线程出现才有了真正意义上的轻量级线程方案。你在面试里如果听到“协程”可以主动把 Java 虚拟线程聊出来说明你不只是背了 API而是理解不同并发模型之间的取舍。1.3 Java 线程模型与操作系统的关系Java 线程从 1.2 开始就是 1:1 映射到操作系统内核线程的也就是说每一个Thread对象背后都对应一个内核线程。这个模型的好处是稳定、简单JVM 不用自己调度线程坏处是线程创建、销毁、切换都要经过内核成本比较高所以才有了后面要说的线程池。线程状态也是大家逃不过去的一块。Java 定义了NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六种状态。注意RUNNABLE这个状态其实包含了两种子情况一种是线程正在 CPU 上执行另一种是线程在排队等待 CPU 时间片。如果拿jstack看到大量线程处于RUNNABLE不代表它们都在干活很可能是在空转或者疯狂自旋。2. 线程的创建与生命周期别再只会 new Thread2.1 三种启动方式与真实差异网上讲线程创建十篇里有八篇是同一个模子“继承 Thread、实现 Runnable、实现 Callable”。但很少有人告诉你这三者在实战中怎么取舍。我给你的结论是能不用继承 Thread 就尽量不要用。因为 Java 是单继承你一旦继承了 Thread就没法再继承其他业务基类了而且继承方式把任务代码和线程控制代码绑死在一起扩展性很差。实现Runnable接口是最常见的做法任务和线程解耦线程池也喜欢接收Runnable。但它有一个明显的局限——不能直接返回结果也不能抛受检异常。所以当你需要拿到线程执行结果、或者要捕获执行过程中的异常时就得用CallableV FutureTask或者直接用线程池提交Callable后获取Future。我贴一段比较典型的写法给你看看实际项目里更推荐用线程池但为了演示先不用// 方式一继承 Thread不推荐 Thread t1 new Thread() { Override public void run() { System.out.println(task1 running); } }; t1.start(); // 方式二实现 Runnable推荐 Runnable task2 () - System.out.println(task2 running); new Thread(task2).start(); // 方式三Callable FutureTask适合需要结果的场景 CallableInteger task3 () - { Thread.sleep(1000); return 42; }; FutureTaskInteger futureTask new FutureTask(task3); new Thread(futureTask).start(); Integer result futureTask.get(); System.out.println(result result);2.2 线程状态切换与几个容易误解的细节线程状态的切换是面试喜欢问、实际排查也经常用的一块。sleep和wait的差别是重中之重sleep是Thread的静态方法不释放锁wait是Object的方法必须在持有该对象锁的代码块里调用而且调用后会让出锁进入WAITING状态。很多人写生产者消费者代码时在该用wait的地方用了sleep导致锁一直被占着其他线程根本进不来程序就像“死”了一样。join也经常被误解。它的作用是让当前线程等待被 join 的线程执行完成底层也是基于wait机制实现的。注意join是可以被打断的如果你希望等待一个线程最多几秒钟可以用join(2000)超过时间就放弃等待。yield这个操作只做了一件事让当前线程从RUNNABLE回到RUNNABLE的调度队列里通知调度器“我可以让出 CPU”。它既不释放锁也不保证别的线程一定会先执行所以生产代码里基本没人用它更多是出现在教科书和面试题里。折腾过这几个方法之后你就明白Java 对线程的控制粒度始终是偏“协作式”的很多东西都需要你自己处理好。2.3 线程的停止stop、interrupt 与标志位线程停止是一个绕不过去但又特别容易被用错的话题。Thread.stop()这个方法在 Java 里早就标记为废弃了因为它会在线程执行的任意位置直接抛出ThreadDeath很可能导致被操作对象处于中间状态、数据被破坏。我见过老项目里有人用stop强杀线程结果数据库数据都出错了千万别这么干。正确做法是用interrupt配合中断标志。interrupt并不会强制杀死线程它只是把线程的中断标志置为true。如果你的线程逻辑里有sleep、wait、join这类阻塞操作那它们会立刻抛出InterruptedException如果线程只是在普通运算里你需要自己检查Thread.currentThread().isInterrupted()。比较坑的一点是捕获到中断异常后中断标志会被清除掉所以很多规范代码会在catch块里重新调用Thread.currentThread().interrupt()把标志恢复否则外部调用方就感知不到“这个线程其实已经被要求中断了”。3. 线程安全的核心可见性、原子性与有序性3.1 JMM 到底在管什么JMMJava 内存模型不是“Java 内存怎么分配”的意思它是一套规范主要规定了在多线程环境下一个线程对共享变量的写入何时对另一个线程可见。经典模型是所有变量都存在主内存里每个线程还有一个自己的工作内存寄存器、缓存、本地内存的抽象线程对变量的读写都要先和自己的工作内存打交道再同步回主内存。这就引出了可见性的根本问题线程 A 改了变量线程 B 可能看不到因为它读的是自己工作内存里的旧副本。JMM 还规定了 happens-before 规则。比如“程序顺序原则”“锁操作原则”“volatile 变量原则”“线程启动和终止原则”等。有了这些原则你才能判断一条语句的执行在并发环境下是否对另一条语句可见。我建议不要死记硬背原则的名字而是理解它的推导逻辑只要你能在自己的代码里找到一条 happens-before 链就能保证前面的共享变量写在后面是被看到的。3.2 synchronized 的三种用法与锁升级synchronized是 Java 里最基础的同步手段可以用在实例方法、静态方法、代码块上。用在实例方法上锁的是当前实例对象用在静态方法上锁的是当前类的 Class 对象用在代码块上锁的是你指定的对象。很多新手以为锁的是“方法里的代码”其实锁的是对象这一点决定了两个线程能不能互相阻塞——只有锁同一个对象才会竞争。JDK 1.6 之后synchronized 做了大量优化锁一共有四种状态无锁、偏向锁、轻量级锁、重量级锁。锁会随着竞争激烈程度逐步升级但不会降级。这意味着如果你在单线程环境下用 synchronized绝大多数场景只是打上一个轻量级标记就结束了并不像老教材里说的那么重。不过在激烈竞争下它依然是重量级锁线程会进入 BLOCKED 状态。那种短促的、快速执行的临界区代码我一般会用原子类或者自旋类工具处理防止大量线程排队阻塞。3.3 volatile 到底在什么时候管用volatile 是面试里出现频率极高的关键字但也是被用滥的关键字。它保证了两件事可见性和有序性禁止指令重排序但它不保证原子性。也就是说volatile int count对count这种先读后写再回写的操作来说依然不是安全的。这里必须强调一遍很多文章容易把 volatile 说成“线程安全万能药”这是错的。那么 volatile 到底适合什么场景一是状态标志位比如一个 boolean 变量控制线程是否要继续运行二是单例模式里的双重检查锁防止指令重排序导致其他线程拿到未初始化完成的对象三是某些只需要保证可见性、写状态不依赖旧值的场景。如果你想对计数器做累加请老老实实用AtomicInteger或者加锁。3.4 CAS 与原子类CASCompare And Swap是并发编程里的一把超级利器。它的核心思想是内存值 V预期值 A新值 B只有当 V 等于 A 时才把 V 更新为 B否则什么都不做。整个过程是硬件指令级别的原子操作不需要加锁。AtomicInteger、AtomicLong、AtomicReference这些类的底层都是 CAS 加自旋实现的。用AtomicInteger来替代synchronized做计数器是最常见的入门案例public class Counter { private final AtomicInteger count new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int get() { return count.get(); } }值得留意的是CAS 在竞争激烈时会不停地自旋重试CPU 开销反而比锁更大这就是所谓的“CAS 的 ABA 问题反倒是次要的性能问题才更直接”。ABA 问题是指变量从 A 变成 B 又变回 ACAS 看到还是 A 就认为没变过结果数据处理出问题。如果你关心版本变化可以改用AtomicStampedReference带上版本号去比较。4. Java 并发工具包的实战用法4.1 Lock 体系ReentrantLock、读写锁从 JDK 1.5 开始Java 提供了java.util.concurrent.locks.Lock接口最常用的实现是ReentrantLock。和 synchronized 相比它的主要优势包括支持可中断地获取锁、支持超时获取锁、支持公平和非公平策略、可以绑定多个Condition条件队列。代码形式上它需要手动lock()和unlock()记得把解锁放在finally里防止异常导致锁永远不释放。ReentrantLock lock new ReentrantLock(true); // 公平锁 try { if (lock.tryLock(2, TimeUnit.SECONDS)) { try { // 临界区代码 } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); }读写锁ReentrantReadWriteLock把锁拆成了读锁和写锁读读可以并发读写、写写互斥。这种锁特别适合“读多写少”的缓存场景。不过要注意它的几个坑写锁是悲观锁读锁不能升级为写锁如果读线程在持有读锁时想拿写锁很容易造成死锁。4.2 协作工具CountDownLatch、CyclicBarrier、Semaphore这三兄弟是并发面试的常客实际项目里也经常用。CountDownLatch是倒计时门闩主线程等若干个子任务完成后再继续CyclicBarrier是循环屏障多个线程相互等待直到大家都到达屏障点之后再一起放行Semaphore是信号量用来控制同时访问某个资源的线程数量相当于一个可计数的共享锁。举个例子假设你要并发跑 10 个接口然后等所有结果回来再汇总这就是CountDownLatch的典型用法int taskCount 10; CountDownLatch latch new CountDownLatch(taskCount); ExecutorService executor Executors.newFixedThreadPool(5); for (int i 0; i taskCount; i) { final int index i; executor.execute(() - { try { Thread.sleep(100); System.out.println(task index done); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } latch.await(5, TimeUnit.SECONDS);CyclicBarrier和CountDownLatch最直观的区别是CyclicBarrier可以循环使用而且它的计数是线程自己调用await()来减少的没有类似countDown和await的职责分离。Semaphore我经常用在限制第三方接口调用速率的场景比如某个外部服务只允许同时 3 个请求就初始化new Semaphore(3)每次请求前acquire()用完后release()。4.3 BlockingQueue 与生产者消费者阻塞队列可以说是我在生产环境里用得最多的并发容器之一。以LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue为例它们内部都实现了等待/通知机制。生产者线程往队列放数据时如果队列满了会被阻塞消费者从队列取数据时如果队列空了会被阻塞。这不就是现成的生产者消费者模式吗完全不用你手写wait/notify。ArrayBlockingQueue是基于数组的有界队列创建时必须指定容量支持公平和非公平的访问策略LinkedBlockingQueue是有界的链表队列默认容量是Integer.MAX_VALUE使用不当可能内存溢出SynchronousQueue是带特殊功能的队列它不存储元素生产者放入元素时必须等到消费者来取这个特性经常被用于“一对一直接交接”的线程池。实际选型时我优先推荐ArrayBlockingQueue因为容量明确不容易埋雷。4.4 ThreadLocal 的使用与内存泄漏ThreadLocal是一种“以空间换隔离”的方案每个线程都有自己的变量副本线程之间互不影响。它最典型的使用场景是日志追踪 ID、数据库连接、当前登录用户信息等。用法非常简单private static final ThreadLocalString TRACE_ID new ThreadLocal(); public static void setTraceId(String traceId) { TRACE_ID.set(traceId); } public static String getTraceId() { return TRACE_ID.get(); }但ThreadLocal有一个非常隐蔽的坑如果使用线程池线程复用之后ThreadLocal里的值不会被自动清理下一个任务可能读到上一个任务残留的数据。更严重的如果ThreadLocal里存的是比较大的对象而且线程长期存活就可能造成内存泄漏。因为ThreadLocalMap的 key 是弱引用value 是强引用key 被回收之后 value 还挂在 map 里。解决方式就是用完一定要remove()尤其是封装成工具类时要确保一次请求或一次任务的完整生命周期内能清理干净。4.5 AQS 是什么为什么都在聊它你在看并发工具源码时一定会遇到AbstractQueuedSynchronizer也就是 AQS。JUC 里很多核心类比如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock底层都是基于 AQS 实现的。AQS 的核心是一个volatile int state状态量加一个 FIFO 等待队列。加锁就是通过 CAS 把这些 state 从 0 改成 1 或其他值抢不到锁的线程会被封装成节点挂到队列上然后LockSupport.park()阻塞。理解 AQS 是一个分水岭。你能讲清楚“state 语义是什么”“独占模式还是共享模式”“队列怎么唤醒后继节点”面试官对你的评价就会明显不一样。说到底它就是提供了一套模板方法你把tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared这些钩子实现好同步器就自动帮你管理排队和阻塞唤醒。这也是为什么面试八股文里 AQS 经常和 ReentrantLock 一起出现的原因。5. 线程池生产环境真正在用的线程管理方式5.1 核心参数与执行流程如果让我只说一个多线程里最重要、最值得学习的 API那一定是线程池。你不应该在业务代码里随便 new 线程而应该统一用线程池管理。ThreadPoolExecutor有七个核心参数分别是核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略。我再用一段话把执行流程讲透任务提交过来后如果当前线程数小于核心线程数就创建一个新线程执行任务如果线程数已经达到核心线程数任务就进入等待队列如果等待队列也满了线程数还没达到最大值就继续创建新线程执行任务如果最大线程数也达到上限队列也满了那就触发拒绝策略。很多线上问题就是这几个参数搭配不合理导致的比如核心线程数设得太小、队列又设成无界结果系统负载全被队列吃掉了上游以为任务提交成功其实任务只是在一个越来越大的队列里排队。5.2 拒绝策略怎么选ThreadPoolExecutor自带四种拒绝策略AbortPolicy直接抛异常CallerRunsPolicy让调用者线程自己执行任务DiscardPolicy直接丢弃任务DiscardOldestPolicy丢弃队列里最老的任务。默认是AbortPolicy它会让任务提交抛出RejectedExecutionException如果你的代码里没捕获任务直接就失败了。我给一个项目里的选型经验核心业务任务优先用CallerRunsPolicy因为它在极端情况下不会丢任务只是拖慢提交速度相当于一个自然的背压机制允许丢弃的异步非核心任务比如日志上报、打点消息可以用DiscardPolicy或者DiscardOldestPolicy但一定要在监控里记录丢弃数量否则出了问题你根本不知道任务被丢过。5.3 线程池关闭shutdown vs shutdownNow关闭线程池也是写并发代码容易忽略的细节。shutdown()会等所有已提交任务执行完再关闭任务队列里的任务也会继续处理之后不再接收新任务shutdownNow()会尝试中断正在执行的任务并且返回队列里还没处理的任务列表。线上常用的优雅关闭方式一般是先shutdown()再awaitTermination等待一段时间如果超时说明还有任务没跑完再考虑shutdownNow()强杀。强杀之前一定要做好状态的补偿机制否则任务执行到一半被打断数据一致性就出问题了。我在项目里见过有人直接调用shutdownNow()把正在写文件的任务打断结果文件只写了一半。5.4 常见线程池实现与坑Executors工具类提供了几种现成线程池很多人图省事直接使用但这在生产环境有相当大的隐患。newFixedThreadPool的队列是无界的LinkedBlockingQueue任务堆积过多时内存可能被吃满newCachedThreadPool的最大线程数是Integer.MAX_VALUE高峰期可能创建出成千上万的线程直接把机器打挂newScheduledThreadPool在某些版本的 JDK 里也可能出现队列无限增长的问题。所以阿里的 Java 开发手册里才会强调不要用 Executors 创建线程池要自己通过ThreadPoolExecutor显式指定参数。这不是矫情是服务器资源有限无界的东西都很危险。而且我要再提醒一句线程池里的线程一定要设置一个有业务含义的名称用ThreadFactory自定义一下否则出问题的时候jstack里的线程名全是pool-1-thread-1你根本认不出是哪个业务模块。6. 高并发场景下的数据一致性问题6.1 数据一致性问题的来源什么情况下会出现数据一致性问题简答一句话多个线程并发读写同一个共享数据而读或写没有进行正确的同步。比如库存超卖、余额扣减、订单号生成都是典型场景。根本原因是“检查-操作”不是一个原子过程线程 A 检查了库存还有 1 件还没扣减线程 B 也检查到了 1 件两边都扣了库存就变成负数了。理解这个问题要从整体入手解决方案不是只有一种锁。锁、CAS、数据库乐观锁、分布式锁、幂等机制各自有各自的边界。你要先判断自己是在单机 JVM 内部竞争还是在多节点之间竞争。单机内部用 JVM 的锁跨节点就必须引入外部组件的分布式锁。热词里有人问“java怎么保证数据一致性”这正是这个章节要回答的问题。6.2 锁、CAS 与事务几种方案的取舍我们来对比一下常用方案synchronized实现简单适合临界区不重的场景但它会阻塞线程如果锁竞争激烈吞吐量上不去。ReentrantLock比 synchronized 更灵活支持超时和中断适合需要控制等待时间的场景。CAS 原子类适合短小轻量的状态更新比如计数器、标志位但循环重试会浪费 CPU。数据库乐观锁用版本号或时间戳判断是否冲突适合跨事务、对并发要求没那么极致的场景。分布式锁跨 JVM、跨进程时用常见的实现有基于 Redis 的 SETNX、基于 ZooKeeper 的临时节点等。真实项目中往往是既有 JVM 内锁又有分布式锁还会配合数据库的唯一索引兜底。比如扣库存可以在内存里先做一层串行化或限流真正入库时再用乐观锁避免每次更新都带上长事务锁表。6.3 实际案例扣库存我这里手写一个简化版的库存扣减案例帮你看清楚问题本身和优化方向。假设ProductStock表里有一个quantity字段最简单的扣减 SQL 是UPDATE product_stock SET quantity quantity - 1 WHERE product_id ?;这条 SQL 本身是原子的吗如果数据库的隔离级别和索引能保证的话单条更新确实不会出现超卖因为行锁会串行化同一行上的更新操作。但很多系统的复杂度在于扣库存之前要先查库存是否充足查和扣之间就有了时间窗口。所以更稳妥的做法是把判断和扣减放到一个受影响行数里判断int updated stockMapper.deductStock(productId, quantity, oldVersion); if (updated 0) { throw new BizException(库存不足或版本冲突); }对应 SQL 就是带条件的 UPDATEUPDATE product_stock SET quantity quantity - #{quantity}, version version 1 WHERE product_id #{productId} AND quantity #{quantity} AND version #{oldVersion};这样既保证了检查与扣减之间的原子性又避免了并发下多个线程同时扣减消耗同一批库存。WEB 场景里还要注意在并发入口做限流防止恶意刷库存。6.4 消息消费端的顺序性Kafka 消费多线程怎么保证顺序热词里有人专门搜“kafka消费端多线程如何保证消息顺序性”这也是高并发数据一致性的一个缩影。Kafka 在分区内部保证有序但同一个分区可能被多个消费者线程并发处理一旦先处理后面的消息、后处理前面的消息顺序就乱了。最简单实用的办法是每个分区对应一个单线程消费器或者把消费到的消息按 key 哈希到固定的业务处理线程保证同一个 key 的消息落到同一个线程串行执行。更复杂的方案是引入队列缓冲和滑动窗口机制比如消息设置了顺序号处理线程必须按顺序号提交结果如果前一条还没完成后一条就先缓存等待。这些方案的共同点都是不要指望框架帮你解决业务顺序你要在业务上主动约束“同一类消息只能走同一条处理链路”。多线程带来的并发能力提升必须以保证这种约束为前提。7. 常见问题与排查技巧实录7.1 死锁的定位与避免多线程项目里最常见也最难缠的问题之一就是死锁。死锁发生的四个必要条件是互斥、持有并等待、不可剥夺、循环等待。避免死锁的基本思路就是破坏这四个条件里的任意一个工程上最容易操作的是“破坏循环等待”也就是给所有锁资源定义一个全局顺序所有线程必须按这个顺序加锁不许乱序。另外一个技巧是使用tryLock(timeout)拿不到锁就退出不要无限期等下去。定位死锁的办法也不难。先在现象上观察服务卡住、日志停止推进、请求全部超时。然后执行jstackJVM 会在线程快照里直接发现Found one Java-level deadlock并列出死锁线程和各自持有的锁。看到这种结果之后再去代码里找对应的锁顺序基本就能定位。7.2 线程池耗尽与任务堆积线程池链路里有一个特别容易忽视的现象任务提交很快但执行很慢线程池里的任务队列越来越长最后内存暴涨甚至触发 OOM。排查这种问题时不要只盯着线程数还要看队列的深度。线程池没有直接的队列深度指标展示但你可以通过ThreadPoolExecutor.getQueue().size()拿到当前堆积数量把它接入监控。更关键的是当你发现线程池耗尽的根因是某个下游接口变慢了那单纯调大线程池参数并不可取因为下游容量的瓶颈没有解决只是把等待放到了更多线程上。这时候要做的是下游接口限流、超时缩短、批量任务拆分。线程池是容器真正的问题往往在依赖的稳定性上。7.3 CPU 飙高与上下文切换线上 CPU 飙高很多人的第一反应是“死循环”但不一定是。如果线程数设置得过大大量的上下文切换也会导致 CPU 占用居高不下。你可以用top -Hp pid查看线程级别 CPU再用jstack定位也可以用vmstat观察cs列上下文切换次数如果动不动每秒几十万基本就是线程太多了。另外还要小心自旋锁和 CAS 竞争。我之前优化过一个发短信接口代码里用了AtomicBoolean做状态切换刚开始性能不错后来流量涨了大量线程在同一时刻去更新同一个AtomicBoolean全部进入自旋重试CPU 一下子就上去了。这个场景换成synchronized反而更合适因为竞争激烈时偏向锁和自旋的收益很低不如干脆阻塞住。7.4 面试高频题速查表下面这张表是我整理的高频面试题每一行都指向一个核心知识点。准备的时候不要只背答案而是把背后的原理和代码场景一起带上。问题核心考察点一句话答题思路什么是进程和线程的区别操作系统基础资源分配和执行单元、共享与独立sleep 和 wait 的区别锁的释放sleep 不释放锁wait 释放锁volatile 和 synchronized 的区别JMMvolatile 管可见性不管原子性synchronized 两者都管什么是 CAS原子操作比较再交换硬件级原子注意 ABAAQS 原理JUC 基石state CLH 队列 LockSupport线程池参数和执行流程ThreadPoolExecutor核心数、队列、最大数、拒绝策略的顺序如何避免死锁死锁四条件破坏循环等待tryLock 超时如何保证数据一致性综合锁、CAS、版本号、分布式锁、幂等7.5 避坑清单最后我把自己和团队踩过的坑汇总成一份清单适合贴在工位旁边。不要用Thread.stop()停线程用中断和标志位协作处理。加锁必须在finally里释放尤其是Lock体系。ThreadLocal用完后必须remove()特别是在线程池里。线程池不要用Executors创建队列无界是灾难。线程名一定要自定义排查问题能少走一半弯路。共享变量优先考虑不可变对象、原子类不要一上来就上锁。读多写少用读写锁写多读少还不如普通锁。分布式场景里JVM 锁解决不了跨节点问题必须引入真正的分布式锁。说实话多线程这块没有捷径看再多的教程都不如亲手调一次死锁、排查一次 CPU 飙升来得深刻。我最初写并发代码的时候也犯过不少低级错误后来慢慢养成一个习惯每写一个共享变量都会先问自己一句“它会不会被多个线程同时读写怎么保证正确性” 多问几句很多潜在的坑就被拦在了代码评审之前。希望这篇小结能给你提供一条比较完整的路线后续再用的时候少走弯路。