
1. 内容整体设计与思路拆解1.1 为什么写这篇“初识多线程下”上篇我们聊完了线程的创建方式、生命周期、状态切换还有start()和run()的区别这些入门基础。不少读者在评论区里追问线程我会建了但多个线程之间怎么配合数据竞争怎么处理什么情况下该用锁线程池又是怎么一回事这些问题恰好就是高并发编程从“会用”走向“会用对”的分水岭。很多同学简历上写着“熟悉多线程”一问到wait/notify的底层机制、volatile到底保证了什么、线程池的workQueue满了会发生什么就明显底气不足。说白了就是知其然而不知其所以然。这篇“下篇”我打算换个讲法。不按教科书那种“先讲概念再讲API”的顺序而是沿着一条主线走多个线程同时跑起来之后它们之间会产生哪些冲突业界又是怎么一步一步解决这些冲突的。把这根线捋清楚了synchronized、volatile、Lock、CountDownLatch、线程池这些东西就不是零散的知识点而是一整套顺理成章的解决方案。适合看这篇文章的人我大致分三类一是刚学完 Java 基础、准备啃高并发这块硬骨头的初学者二是面试前想系统梳理多线程知识体系的求职者三是写了好几年业务代码、线程池和锁一直在用但想搞明白原理的后端开发。不管你属于哪一类这篇文章的目标都是让你读完能建立起自己的多线程知识框架而不是背了一堆零散的面试题答案。1.2 从“多线程”到“高并发”的认知升级先聊一个很多人没想透的问题多线程和高并发到底是不是一回事不是一回事但它们是递进关系。多线程是一种编程手段关注的是“如何在一个进程内同时执行多个任务”高并发是系统的一种能力指标关注的是“系统在面对大量并发请求时如何保持低延迟、高吞吐、不崩溃”。要想支撑高并发多线程几乎是绕不开的手段但仅仅会用多线程离高并发还很远。举个例子。你写了一个接口每个请求进来创建一个新线程去处理。单测的时候没问题并发量一上来线程数量爆炸内存溢出CPU 频繁切换上下文系统直接雪崩。这就是典型的“用了多线程但没考虑高并发”问题就出在线程管理策略上。所以在讲具体 API 之前我想先帮大家建立一个整体认知框架一个线程安全的程序需要同时满足三个条件——原子性操作不可分割、可见性一个线程的修改能被其他线程看到、有序性代码执行的顺序符合预期。这三者统称并发编程的三大特性后面讲的所有工具和技术本质都是在解决这三个问题中的一个或几个。下篇的内容就围绕着这三个特性展开。看到synchronized时你要知道它同时保证了原子性和可见性看到volatile时你要知道它只解决了可见性和有序性但解决不了原子性看到ConcurrentHashMap时你要知道它是通过细粒度锁和 CAS 算法在并发度和安全性之间取了一个平衡。带着这个框架去学效率会高很多。2. 核心细节解析与实操要点2.1 线程通信wait/notify 的底层逻辑与正确用法多个线程各自跑各自的互相之间怎么打招呼Java 提供了一套基于**监视器Monitor**的协作机制核心就是Object类上的三个方法wait()、notify()、notifyAll()。先把底层机制讲清楚。每个 Java 对象都有一个内置锁也叫监视器锁线程进入synchronized代码块时会持有这个锁。当你调用wait()时当前线程会做三件事释放当前持有的监视器锁线程状态从 RUNNABLE 变为 WAITING该线程被放入该对象的**等待集Wait Set**中。直到其他线程调用同一个对象上的notify()或notifyAll()等待集中的线程才有机会被唤醒重新去竞争锁。这里需要注意一个关键点notify()是从等待集中随机唤醒一个线程而notifyAll()是唤醒所有线程。被唤醒的线程并不会立即执行它要和其他线程一起重新竞争锁拿到锁之后才能继续执行。有一个经典的坑wait()和notify()必须在synchronized代码块或方法中调用否则会抛出IllegalMonitorStateException。很多初学者不理解这是为什么其实从设计角度看很好解释既然要操作监视器锁和等待集那你必须先证明你持有了这个锁synchronized就是你的“入场凭证”。再分享一个我踩过很多次的坑——wait 必须放在循环里而不是用 if 判断。这是《Effective Java》里明确强调过的规范。原因是线程被唤醒后竞争到锁继续执行时条件可能已经被其他线程改变了。如果用 if线程唤醒后会直接执行后续逻辑不做二次检查就会产生错误的业务结果。我见过一个真实案例某团队做消息队列消费时用 if 包裹 wait高并发下偶发重复消费和漏消费排查了三天最后发现就是这个原因。synchronized (queue) { // 必须用 while 而不是 if while (queue.isEmpty()) { queue.wait(); } Message msg queue.poll(); }2.2 并发三大特性与 JMM 的深度理解有时候线程之间并不需要这么复杂的通信但数据还是会出错为什么这就引出了Java 内存模型JMM, Java Memory Model。JMM 规定所有变量都存储在主内存中每个线程有自己的工作内存可以类比为 CPU 缓存。线程对变量的所有操作读取、赋值都必须在工作内存中进行不能直接读写主内存中的变量。而且不同线程之间无法直接访问对方的工作内存线程间变量值的传递需要通过主内存来完成。这个模型带来了一个严重的问题可见性。线程 A 修改了一个变量但这个修改还停留在 A 的工作内存里没有刷新到主内存线程 B 读这个变量时读到的是主内存中的旧值。于是 B 就看到了一个“过期”的数据。我常用一个生活化类比来解释这层关系主内存相当于公司公告栏线程的工作内存相当于每个人手里的便签本。你把自己的想法写在便签本上同事想了解你的进度必须等你去公告栏更新。你如果一直不更新同事看到的永远是个旧版本。解决方案有三个层次。第一层是volatile关键字它保证了对变量的写操作会立即刷新到主内存读操作会从主内存重新加载相当于强制你每次改完便签本必须马上同步到公告栏。第二层是synchronized和Lock它们通过锁机制保证同一时刻只有一个线程操作并且在释放锁时把工作内存中的修改强制刷新到主内存。第三层是final关键字它提供了初始化安全性的保证——一个对象安全发布后其他线程看到的final字段一定是初始化后的值。2.3 volatile 的适用边界它到底解决了什么volatile是面试高频考点但它也是最容易被误用的关键字。先说它保证的两件事第一可见性。对 volatile 变量的写入JMM 会在写入后插入一个内存屏障强制把工作内存中的新值刷新到主内存。对 volatile 变量的读取会在读取前插入内存屏障强制从主内存加载最新值。第二有序性。JMM 会对 volatile 变量相关的指令进行重排序限制禁止指令重排序越过 volatile 的读写边界。这个特性的典型应用场景是双重检查锁单例模式DCL。但volatile不保证原子性。经典的count问题即使你把 count 声明为 volatile多线程执行count依然会出问题。因为count在底层是三步操作——读取 count 的值、加 1、写回 count——volatile 保证了每一步的可见性但无法保证这三步作为一个整体不被其他线程打断。线程 A 在读和写之间线程 B 可能已经完成了整轮操作导致 A 写回的是过期值加 1 的结果。那什么时候可以放心用 volatile我总结了几个典型场景状态标记位比如用 boolean 变量控制线程的启停一个线程写其他线程读单例模式中的 instance 声明发布不可变对象的引用比如把某个配置对象赋值给 volatile 引用其他线程读到的都是完整发布的不可变对象。除了这些场景如果你需要保证复合操作的原子性老老实实用锁或者原子类。3. 实操过程与核心环节实现3.1 锁的演进与升级从偏向锁到重量级锁Java 的synchronized在 JDK 1.6 之后做过一次大优化引入了锁升级机制。这背后其实是 JVM 对现实场景的一个洞察大部分锁在大部分时候都只被同一个线程竞争。既然大多数场景是“无竞争”或“低竞争”那每次都走操作系统内核态去加锁成本就太高了。锁的升级路径是这样的无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁的核心思想是如果同一个线程多次获取同一个锁就让这个锁“偏向”它后续它再来的时候不需要任何 CAS 操作直接进入。只有当另一个线程也来竞争这个锁时偏向锁才会被撤销。撤销偏向锁要等到安全点Safe Point这是偏向锁在 JDK 15 里被废弃的主要原因之一——在高竞争场景下偏向锁的撤销代价反而高于收益。轻量级锁的核心思想是线程进入临界区时先在栈帧中创建锁记录Lock Record然后通过 CAS 尝试将对象头中的 Mark Word 替换为指向锁记录的指针。如果 CAS 成功获取锁成功如果失败说明有其他线程持有锁锁会膨胀为重量级锁。重量级锁就是依赖操作系统的互斥量Mutex来实现线程会进入阻塞状态涉及用户态和内核态的切换开销最大。很多面试题会问到“synchronized 是公平锁还是非公平锁”答案是非公平锁。因为新来的线程会先去尝试 CAS 抢一次锁抢不到才进入等待队列。这种设计的好处是减少了线程唤醒的开销吞吐量更高代价是可能会产生“插队”现象某些等待时间很长的线程会一直等不到锁。3.2 ReentrantLock 的可控性与 AQS 原理ReentrantLock是java.util.concurrent.locks包下的可重入锁它和synchronized都能实现线程同步但 ReentrantLock 提供了更丰富的功能。我从实际使用出发把这几个差异点列成了表对比维度synchronizedReentrantLock锁释放方式自动释放代码块执行完自动解锁必须手动unlock()通常在 finally 中释放锁的获取方式隐式获取支持lock()、tryLock()、lockInterruptibly()三种方式公平性非公平构造方法传入 true 可实现公平锁条件队列只有一个等待队列支持多个Condition可以精准唤醒指定线程中断响应不响应中断支持响应中断ReentrantLock底层依赖的是AQSAbstractQueuedSynchronizer这是整个 JUC 包的地基。AQS 的核心设计是用一个volatile int state表示同步状态一个 FIFO 双向队列存放等待线程。加锁就是通过 CAS 把 state 从 0 改成 1解锁就是把 state 从 1 改回 0。我刚开始学 AQS 的时候也觉得抽象后来换了个角度理解就通了AQS 就像是一个银行叫号系统。来了一个客户线程先尝试直接办理业务CAS 获取锁。如果柜台被占用了就领一个号封装成 Node 节点在等候区CLH 队列排队。柜台办完一个释放锁就通知下一个叫号办理。公平锁就是不允许插队非公平锁就是新来的客户可以试一下能不能抢在排队的人前面。3.3 CAS 与原子类无锁并发方案的实践CASCompare-And-Swap比较并交换是无锁并发编程的核心。它的逻辑很简单比较内存中的值是否等于期望值如果相等就替换成新值如果不相等就什么都不做。整个过程是原子操作由 CPU 指令直接支持比如 x86 架构下的CMPXCHG指令。java.util.concurrent.atomic包下的原子类比如AtomicInteger、AtomicLong、AtomicReference就是用 CAS 实现的。用AtomicInteger替换int来做计数器可以避免加锁的性能损耗public class AtomicCounter { private AtomicInteger count new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } }CAS 有一个著名的坑叫ABA 问题线程 A 读到值是 1准备改成 2此时线程 B 把值从 1 改成 2又从 2 改回 1线程 A 的 CAS 比较发现值还是 1CAS 成功。但在这个过程里值其实已经被改过两次了。对于某些场景来说这不影响结果但如果你关心的是“值是否被改过”而不是“值当前是多少”ABA 问题就会导致逻辑错误。解决方案是用AtomicStampedReference它会额外维护一个版本号每次修改版本号都会变化。再说说 CAS 在并发竞争激烈时的性能问题。CAS 失败后会自旋重试如果竞争太激烈自旋次数过多会消耗大量 CPU。JDK 8 之后引入了LongAdder它把热点数据拆分到多个 Cell 里每个线程操作自己的 Cell最后汇总。这种方式在超高并发场景下的性能远优于AtomicLong。3.4 线程池高并发场景下必须掌握的武器搞清楚了锁和原子类我们再来看高并发场景里最常用、也最容易被用错的工具——线程池。先明确一个观点在高并发场景下永远不要用 new Thread() 创建线程。线程的创建和销毁是有成本的包括分配内存、初始化、系统调用而且线程本身占用大约 1MB 左右的栈内存。高并发下如果每个请求都新建一个线程系统资源很快就会被打满。线程池的核心价值就是复用线程降低线程创建销毁的频繁开销同时通过队列缓冲控制并发量。ThreadPoolExecutor的核心参数有七个参数含义设置建议corePoolSize核心线程数CPU 密集型设为 N1IO 密集型设为 2NN 为 CPU 核数maximumPoolSize最大线程数核心线程数 队列容量能承受的极限值keepAliveTime非核心线程空闲存活时间根据任务到达间隔和业务容忍度设置unit存活时间单位配合 keepAliveTime 使用workQueue任务队列有界队列或无界队列各有优劣threadFactory线程工厂建议自定义方便问题排查handler拒绝策略有四种标准策略可选线程池的执行流程是提交一个新任务时如果当前线程数小于 corePoolSize创建新线程执行如果达到 corePoolSize任务进入 workQueue 排队如果队列也满了并且线程数小于 maximumPoolSize创建新线程执行如果线程数已经达到 maximumPoolSize执行拒绝策略。这里的核心矛盾是核心线程数设置多少合适。我经常遇到团队直接把核心线程数设为 200、300结果应用启动后莫名卡顿线程大量阻塞等待 IOCPU 却跑不满。正确的思路是根据任务类型区分CPU 密集型任务线程数设为 CPU 核数 1让每个线程都在计算没有太多线程切换的必要IO 密集型任务线程数设为 CPU 核数 × 2。因为 IO 等待期间 CPU 是空闲的可以用更多线程来填充等待时间混合型任务如果能拆分就拆分不能拆分就按 IO 密集型的上限来设置。实际生产环境还要考虑内存限制、下游服务的承受能力单机硬件的核心数只是一个起点最终要配合压测来微调参数。我们团队的做法是先按公式设置一个初始值然后做压测看 CPU、内存、接口 RT、线程池活跃度这些指标再反推调整。3.5 JUC 工具类实战从并发协作到流量控制java.util.concurrent包下除了锁和原子类还有几个非常高频使用的工具类。CountDownLatch是一个倒计数器门闩核心场景是“主线程等待多个子线程都完成之后再继续执行”。我之前做过一个数据汇总功能主任务拆成 10 个子任务分别查不同数据源的数据全部查完后统一合并。用 CountDownLatch(10) 初始化每个子任务执行完调用countDown()主任务调用await()等待计数归零。这里的底层是 AQS 的共享锁模式state 初始化为 10每次 countDown 对 state 减 1减到 0 就唤醒等待的线程。热词里提到“线程等待都完成”指的就是这个场景。CyclicBarrier跟 CountDownLatch 容易混淆。两者的核心区别是CountDownLatch 是“等人齐了开工”计数只能减一次CyclicBarrier 是“人齐了各自开工可以循环使用”。CyclicBarrier 的典型场景是分批处理数据每批 5 个线程都到达屏障点后统一执行下一个阶段然后屏障自动重置等待下一批。Semaphore是信号量用来控制同时访问某个资源的线程数。它常被用作限流器。我之前在对接第三方支付接口时就通过 Semaphore(50) 限制同时发起的 HTTP 请求数避免把对方接口打挂。acquire() 获取许可release() 释放许可逻辑非常直观。生产者-消费者模式是这些工具类的最佳练习场景。用有界阻塞队列ArrayBlockingQueue作为缓冲区生产者线程负责往队列里放数据消费者线程从队列里取数据。队列满时生产者的 put 操作会阻塞队列空时消费者的 take 操作会阻塞。这种设计天然地实现了线程间的解耦还能起到削峰填谷的作用。下面是我在实际项目里写过一个简化版实现public class ProducerConsumerDemo { private static final int CAPACITY 10; private static final BlockingQueueString queue new ArrayBlockingQueue(CAPACITY); public static void main(String[] args) { // 2个生产者线程 for (int i 0; i 2; i) { new Thread(() - { try { String data 数据- Thread.currentThread().getName(); queue.put(data); System.out.println(生产: data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, Producer- i).start(); } // 3个消费者线程 for (int i 0; i 3; i) { new Thread(() - { try { String data queue.take(); System.out.println(消费: data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, Consumer- i).start(); } } }这里有一个容易被忽略的细节捕获到InterruptedException后应该调用Thread.currentThread().interrupt()重新设置中断标志位而不是简单地打印日志或者忽略它。因为中断标志被清除后上层代码就无法感知线程曾经被中断过这在需要优雅停机的场景下会造成线程无法及时退出。3.6 ConcurrentHashMap并发容器的正确选择集合类的线程安全性是高并发编程里的高频话题。HashMap在多线程并发写的情况下JDK 7 及之前版本可能因为头插法在扩容时产生环形链表导致 CPU 100% 的死循环问题。JDK 8 改成了尾插法解决了死循环问题但并发写依然会丢数据。所以并发场景下别再纠结 HashMap 为什么不安全了直接用并发容器才是正路。Hashtable和Collections.synchronizedMap()虽然线程安全但它们是给整个 Map 加一把大锁所有操作串行化并发度极低。ConcurrentHashMap就是为了解决这个痛点而生的。JDK 8 中ConcurrentHashMap采用了CAS synchronized的组合方案。它的底层数据结构是数组加链表加红黑树。写入时如果对应位置的桶为 null直接用 CAS 写入不需要加锁这是写入最快的路径。如果桶不为 null就对桶的头节点加 synchronized 锁锁粒度非常细不同的 key 落到了不同的桶上可以并行写入。每次扩容的时候JDK 8 的 ConcurrentHashMap 支持多线程协同扩容。它会把整个数组划分为多个区间每个线程负责一段区间的数据迁移迁移完成后协助其他线程而不是一个线程单独把所有数据搬完。这个设计在大容量场景下能显著缩短扩容时间。我之前在监控告警项目里用一个 ConcurrentHashMap 维护所有实时在线设备的状态写入并发量大约每秒 5 万次一直很稳。如果换成 Hashtable 或者加了大锁的同步 Map光是锁竞争就能把 CPU 打满。还需要注意一点ConcurrentHashMap的弱一致性问题。它的get操作不加锁因此在并发写的过程中读到的可能是某个时刻的旧值但是不会读到脏数据。如果你的业务场景要求读取的强一致性也就是必须读到最新写入的值简单的方案是改用读写锁但并发度会有所下降需要根据业务容忍度来取舍。4. 常见问题与排查技巧实录4.1 面试高频考点速查聊完了多线程的核心知识点我整理一些面试里出现频率极高的问题和参考思路供大家自查。这些问题都是我在面人时经常问的也是我自己面试中遇到过的。问wait() 和 sleep() 有什么区别wait()是 Object 的方法必须配合 synchronized 使用调用后会释放监视器锁线程进入 WAITING 状态sleep()是 Thread 的静态方法不释放锁线程进入 TIMED_WAITING 状态。高并发场景下如果你需要基于某个条件等待用 wait如果只是让当前线程暂停用 sleep。问synchronized 和 ReentrantLock 怎么选这是一个经典的取舍问题。如果只是简单的方法同步、代码块同步synchronized 足够而且 JVM 会自动释放锁不容易出错。如果你需要可中断获取锁、超时获取锁、公平锁、多个条件队列这些高级功能ReentrantLock 是更好的选择。另外JDK 版本的演进让 synchronized 在多数场景下的性能已经不输 ReentrantLock所以在选型时不必过度纠结性能差异主要看功能需要。问什么是死锁如何避免死锁是多个线程相互持有对方需要的锁同时又在等待对方释放锁导致所有线程都无法继续执行。经典的四个必要条件是互斥、持有并等待、不可剥夺、循环等待。排查死锁先用jps找到 Java 进程号再用jstack piddump 线程栈搜索“deadlock”关键字就能定位。避免死锁的常用手段是尽量缩小锁的范围、按固定顺序加锁、使用tryLock带超时时间来获取锁。4.2 线程池线程名优化与线上排查实战线程池有一个常被忽视的问题默认的线程名是pool-1-thread-1这种格式。一旦出问题比如某个线程 CPU 飙高、死锁、OOM你 dump 线程栈之后根本看不出来这个线程是干什么的。我强烈建议在创建线程池时自定义 ThreadFactory把线程名设置成有业务含义的格式ThreadFactory factory new ThreadFactory() { private final AtomicInteger seq new AtomicInteger(0); Override public Thread newThread(Runnable r) { Thread t new Thread(r); t.setName(order-async-processor- seq.getAndIncrement()); t.setUncaughtExceptionHandler((thread, throwable) - System.out.println(线程 thread.getName() 发生未捕获异常: throwable.getMessage())); return t; } };我踩过一个真实的坑某次线上服务出现间歇性超时用 jstack 一查发现有几个线程一直卡在数据库连接池的获取连接处。正常情况下获取连接是毫秒级但那次等了几十秒。继续排查发现连接池大小只有 10但业务线程池的核心线程和最大线程都设成了 50大量线程同时去拿连接把连接池打满后面的请求全部排队等待。最后把连接池大小调大并给线程池设置了合理的拒绝策略问题才解决。4.3 常见并发问题排查速查表问题现象可能原因排查思路CPU 使用率过高线程死循环、CAS 自旋过度jstack 查看线程栈定位堆栈热点接口响应变慢锁竞争激烈、线程池队列积压jstat 查看 GC 情况dump 线程看锁等待数据错乱/丢失并发写共享变量、HashMap 并发操作检查代码中共享变量加锁情况内存溢出线程数过多、无界队列堆积jmap 查看堆内存排查线程池是否用了无界队列程序卡死死锁、线程阻塞jstack 搜索 deadlock 或 BLOCKED 状态这里分享一个通用的排查流程也是我自己的习惯遇到线上问题第一步不是看代码而是先把现场数据留好。用jstack连续 dump 三次线程栈每次间隔 5 秒观察线程状态的演变趋势。如果线程状态一直停留在 BLOCKED 或 WAITING大概率是锁的问题如果线程状态频繁切换但 CPU 很高可能是业务逻辑计算密集或者自旋。再配合jstat -gcutil看 GC 情况和jmap -heap看堆内存基本能定位大部分问题。5. 关于并发编程的学习路径与踩坑心得很多读者会问我同一个问题多线程的知识点太多了学了后面忘了前面到底应该怎么学我的建议是不要按知识分类去学要按问题场景去学。先搞清楚“多个线程同时修改一个变量会怎样”然后去接触 synchronized再问“synchronized 性能太差怎么办”然后去学锁优化和并发容器再问“线程创建销毁开销大怎么办”然后去学线程池。每一步都是为了解决一个具体问题知识点之间自然形成关联记忆成本会低很多。再说一个我观察到的现象。很多初学者写多线程代码喜欢在一段代码里同时用多种并发工具好像用的工具越多显得越专业。这实际上是错误的。并发编程的第一原则是能用简单方案就不要用复杂方案。在低并发场景下synchronized已经足够了没必要为了“性能”强行引入ReentrantLock。在高并发场景下能使用无锁方案原子类、不可变对象、线程封闭就不要用锁必须用锁时尽量缩小临界区的范围。举一个线程安全的反面例子我之前接手过一个优惠券发放系统原来的开发为了保证并发安全把整段发券逻辑都放在了一个大 synchronized 块中包括查库存、校验用户、锁账户、写流水、扣库存逻辑本身没问题但并发量一上来所有请求全部串行执行接口响应时间从 30ms 涨到 3 秒。后来我们把锁粒度做了细化只锁住扣减库存那一段其他的操作都移出临界区接口响应时间降到了 80ms 左右吞吐量提升了整整一个数量级。除了粒度还需要关注锁的顺序。多个线程如果都以不同的顺序获取多把锁就很容易产生死锁。比如线程 A 持有锁 1 再去获取锁 2线程 B 持有锁 2 再去获取锁 1这就是教科书级的死锁模型。解决思路是让所有线程按照相同的顺序获取锁或者在获取锁时设置超时时间获取不到就释放已经持有的锁避免无限等待。还有一个常见误区是过度使用 volatile。volatile 不适合做计数器也解决不了复合操作的原子性。我在代码评审中经常看到有人用 volatile 修饰一个 List 或者一个业务对象期望实现线程安全这显然是不现实的。volatile 只能让你看到最新的引用但无法保证你拿到这个对象之后对象内部的字段不被并发修改。正确的做法是使用并发容器、锁或者把对象设计成不可变对象。踩了这么多坑之后我最大的体会是多线程编程的难点不在于 API 记不记得住而在于建立并发环境下的思维习惯。写代码的时候时刻问自己三句话这个变量有没有可能被多个线程同时读写这段操作是不是原子的如果线程被中断或者锁获取失败程序能不能自愈最后再分享一个我坚持了很多年的习惯。每次写完一段多线程核心逻辑我都会做三件事第一用线程名标识好每个线程的职责第二模拟并发场景写一个小测试至少跑 100 万次操作验证数据一致性第三用jstack观察一下运行中的线程分布和状态是否跟预期相符。这个习惯帮我提前发现了不少潜在问题也让我在面试聊多线程的时候比单纯背八股文的人多了一份“实战感”。如果你也在学 Java 多线程建议从今天开始也试着做起来。