Java多线程与并发,这个话题在面试和实战里被翻来覆去地问、反反复复地踩。很多人背了一堆八股文,从Thread到ThreadPoolExecutor,从synchronized到Lock,看似什么都懂,真到了线上排查问题、设计一个高并发接口的时候,脑子里的知识点全变成了浆糊。我也带过不少新人,看过太多“面试造火箭、入职拧螺丝”的尴尬落差。这篇东西,不打算给你念经式地堆概念,而是把Java多线程和并发编程这条线从头捋一遍,搞清楚每个核心机制到底为了解决什么问题、底层是怎么转的、实际项目里怎么用才不会出事。无论你是准备Java基础面试的应届生,还是被高并发IM、ERP库存这类场景折磨的业务开发,这篇文章都能给你一套可以落地的思考框架。
1. 并发问题的根源:不只是“线程安全”四个字
1.1 为什么多线程这么难搞
先想一个问题:单线程程序为什么简单?因为代码从上往下执行,每一步的结果都是确定的,你知道变量在这个时刻的值是多少。多线程一掺和进来,顺序就乱了:两个线程同时读写一个变量,最后的结果谁也说不准。
这个“说不准”不是玄学,而是由计算机硬件的三个底层事实决定的:
- CPU缓存不一致:每个CPU核心(或每个线程上下文)有一份高速缓存。线程A改了变量的值,可能还留在自己的Cache里,线程B在另一个核心上读到的还是旧值。
- 指令重排序:编译器和CPU为了优化执行效率,会把没有数据依赖的指令重新排序。单线程下不影响结果,多线程下可能就乱了套。
- 线程切换的随机性:操作系统随时可能把CPU时间片从线程A切走,线程A执行到一半的状态,对线程B来说可能是个“半成品”。
这三件事组成了并发Bug的三大来源:原子性(操作没执行完就被切走)、可见性(一个线程的修改另一个线程看不到)和有序性(指令被重排了)。我在前面加粗了这三个词,是因为它们几乎贯穿了Java并发编程的所有知识点。理解了它们,你再看synchronized、volatile、Lock、CAS这些概念,就会有一种豁然开朗的感觉:原来都是在解决这三个问题中的一个或几个。
1.2 Java内存模型(JMM)到底在讲什么
Java为了解决“上面三个底层事实导致的混乱”,专门定义了一套自己的规则,叫Java内存模型。不需要背晦涩的定义,你只需要知道JMM干了两件事:
第一,规定了主内存和工作内存的抽象模型。所有变量存在主内存,每个线程有自己的工作内存,线程操作变量必须先拷贝到工作内存,再写回主内存。这跟CPU的Cache模型是对应的。
第二,提供了happens-before规则。这是一组“人情规定”,只要符合这些规则,前面的操作结果对后面的操作就是可见的。比如:
- 同一个锁的解锁happens-before后续的加锁;
- 写
volatile变量happens-before后续读这个变量; - 线程的
start()happens-before线程内所有操作; - 线程内按代码顺序,前面的happens-before后面的。
注意:有了happens-before,不是说物理上就一定按这个顺序执行,而是说在语义上保证了可见性和有序性。这是理解内存屏障(Memory Barrier)的起点——虚拟机在指令层面插入屏障,禁止特定类型的重排序。
为什么我要花这么多篇幅讲JMM?因为太多人面试的时候被问“什么是JMM”就只会背“主内存、工作内存、原子性、可见性、有序性”,一问“为什么需要它”就卡住了。你得明白,JMM不是语法糖,它是Java跨平台并发语义的基石,也是我们排查并发问题时的“法典”。
2. 核心同步机制深挖:synchronized、volatile、CAS
2.1 synchronized:从重量级到轻量级,JVM做了什么
synchronized可能是你用得最多的关键字,但它的实现原理远比关键字本身复杂。在JDK 1.6之前,它被称为“重量级锁”,因为底层依赖操作系统的Mutex互斥量,线程阻塞和唤醒都要陷入内核态切换,性能很差。后来HotSpot虚拟机引入了锁升级机制,让synchronized在大多数场景下都是轻量级的。
锁升级路径是:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,而且这个状态只能升级,不能降级。
- 偏向锁:同一个线程反复进入同步块,没有其他线程竞争时,锁会偏向这个线程,记录线程ID,之后进入不需要CAS操作。
- 轻量级锁:一旦发生竞争,偏向锁撤销,升级为轻量级锁。线程通过CAS自旋去获取锁,如果没有抢到,就自旋等待,避免立即进入内核态阻塞。
- 重量级锁:自旋超过阈值(或者自旋线程数过多),锁就膨胀为重量级锁,进入内核态的同步队列。
这个升级设计的核心思想是:绝大多数同步块根本不存在多线程竞争,或者竞争时间极短,没必要一开始就动用最昂贵的方案。
实际编码中的经验是:
synchronized加在实例方法上,锁的是this对象;加在静态方法上,锁的是Class对象。锁错对象是最常见的坑。- 不要在同步块里做耗时操作(IO、远程调用、大计算),否则锁的持有时间太长,并发性能直接崩掉。锁粒度能小就小。
2.2 volatile:只解决可见性,不说原子性
volatile这个关键字经常被误解。很多人以为它能让变量变成“线程安全”,其实它只做了两件事:保证对该变量的读写在主内存层面是可见的、禁止指令重排序。它不保证原子性。
经典的例子是volatile int count,两个线程同时执行count++,最终结果依然可能小于20000。因为count++底层分三步:读值、加一、写回,volatile只保证了读和写的可见性,但没有机制阻止“读-改-写”三步之间被线程切换。
那volatile适合什么场景?答案是一写多读的状态标志,比如:
volatile boolean shutdown = false; // 线程A void close() { shutdown = true; } // 线程B while (!shutdown) { // 处理任务 }这种场景下,写操作没有依赖之前的读值,不存在复合操作,volatile就非常合适。还有一个必须提的点:volatile可以作为轻量级的“内存屏障”,比如Double-Checked Locking单例模式,instance声明为volatile,就是为了防止“分配内存”、“初始化对象”、“赋值引用”这三级被重排序,导致另一个线程拿到一个还没初始化完成的半成品对象。
2.3 CAS:无锁并发的地基
CAS(Compare And Swap)是并发编程里一块绕不开的地基,它让“无锁”成为可能。CAS的操作逻辑是:比较内存当前值是否等于预期值,如果相等,就更新为新值;否则什么都不做。这是一个CPU原子指令,AtomicInteger、ConcurrentHashMap、ReentrantLock、AQS全都是建立在这个基础之上的。
但CAS有个著名的ABA问题:线程A读到值=1,线程B把它改成2又改回1,线程A再CAS时发现还是1,于是认为“没人动过”,实际上这个值已经绕了一圈。在大多数业务场景下ABA无害,但如果你要“指针复用”这类精确场景,就得用AtomicStampedReference或AtomicMarkableReference,额外加个版本号。
CAS另一个隐患是自旋带来的CPU开销。高争用场景下,大量线程都在do-while循环里空转,CPU飙升,反而比阻塞式同步更糟糕。所以我自己在实际项目中,通常会先压测看争用程度,高并发且短临界区的场景选CAS,临界区长或冲突率高的场景老老实实用锁。
3. 线程池:把线程管理交给框架
3.1 手写线程池的教训
刚工作那会儿,我也干过“每来一个请求就new Thread(...)去处理”这种事。代码确实能跑,但很快就发现不对劲。一个是线程创建销毁的开销:new一个Thread涉及系统调用和内存分配,在高频场景下这开销非常可观;另一个是资源失控风险:并发请求一多,线程数无上限地涨,轻则GC压力大,重则OOM,把整个应用拖垮。
后来老老实实用线程池,才算把线程管理这块给理顺了。线程池的核心思想是:线程复用 + 控制资源水位。先创建一批常驻线程,有任务就分发给他们执行,任务多就排队等,线程不够用就适当增补,闲下来再回收。这样一来,线程数量可控,任务吞吐稳定,这是任何高并发应用的“保命底裤”。
3.2 ThreadPoolExecutor参数与任务执行顺序
Java里的ThreadPoolExecutor是线程池的完整实现,参数有七个:
corePoolSize:核心线程数,线程池中常驻的线程数(即使空闲也不会被回收,除非设置了allowCoreThreadTimeOut)。maximumPoolSize:最大线程数,线程上限。keepAliveTime:非核心线程空闲存活时间。unit:时间单位。workQueue:任务等待队列。threadFactory:创建线程的工厂,可以自定义线程名(强烈建议自己设置,否则排查问题时看到pool-1-thread-2这种名字完全无头绪)。handler:饱和策略(任务满了怎么办)。
很多人被这道经典面试题问住:“如果核心线程数为2,最大线程数为4,队列容量为3,提交5个任务会怎样?”关键在于记住任务的执行顺序:
- 核心线程还没满时,会创建新线程执行任务。
- 核心线程满了,任务进入等待队列。
- 队列满了,且线程数还没到最大,创建新线程执行任务。
- 线程数和队列都满了,执行拒绝策略。
也就是说,线程池不是“先开满最大线程数再排队的”,而是先让核心线程干活,再排队,排不下了才继续加非核心线程。这一点在京东、阿里系的二面里出现频率极高,很容易踩坑。
3.3 线程池参数怎么定
参数设计没有银弹,但我提供一个经过大量项目检验的计算思路:
首先分析任务类型:
- CPU密集型任务:线程数约为CPU核心数 + 1(考虑线程切换的成本);或者说核心数 * 2 以内的区间去压测确定。
- IO密集型任务:线程数可以放大,常见公式是
核心数 * (1 + IO等待时间 / CPU计算时间),我曾在一个网关服务里按这个公式把线程数从16拉到64,吞吐直接翻倍。
这只是初始值,最终一定要用压测去验证。我一直强调,线程池参数是调出来的,不是算出来的。我常用的做法是先按公式估算一个初值,然后用JMeter做并发压测,观察CPU使用率、线程等待时间、队列积压情况,再微调参数。上线后还要利用监控指标持续观察。另外建议线程池用独立的ThreadFactory和统一的拒绝策略,比如丢弃任务前记录日志,而不是裸奔的AbortPolicy(直接抛异常)让调用方懵掉。
4. 并发容器与锁工具:AQS家族的秘密
4.1 ConcurrentHashMap凭什么能扛高并发
HashMap在多线程下会出各种幺蛾子:扩容时可能出现循环链表(JDK 7及之前),CPU直接爆掉;JDK 8改成尾插法解决了循环链表问题,但依然会有数据覆盖和modCount不一致的坑。所以并发场景必须用ConcurrentHashMap。
JDK 8后的ConcurrentHashMap,数据结构是数组 + 链表 + 红黑树,并发控制粒度细化到每个桶(Node数组的每个槽位),利用CAS + synchronized完成并发安全:
- 插入时:如果对应的桶位为空,用CAS直接存入新Node(无锁);
- 如果桶位不为空,用
synchronized锁住这个桶的头节点,再进行插入(锁粒度很小); - 当链表长度超过阈值(8),链表转红黑树(容量达到64时才会转,否则先扩容数组)。
这个设计的巧妙之处在于:读操作基本无锁(Node的value被volatile修饰),写操作只在对应的桶上加锁。不同线程操作不同桶时,完全不会互相阻塞。因此它在高并发读多写少场景下表现非常亮眼。
4.2 AQS是锁工具的统一骨架
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock,这些工具的底层全是AQS(AbstractQueuedSynchronizer)。
AQS的核心是一个volatile int state变量 + 一个双向等待队列:
- 以
ReentrantLock为例,state=0表示锁空闲,state>0表示被线程持有(值表示重入次数)。线程抢锁就是通过CAS把state从0改成1,抢不到就进入等待队列挂起。 - 以
Semaphore为例,state表示剩余许可证数量,每次acquire则CAS减一,release则加一。同样是AQS不同的state语义。
理解了AQS,等于理解了整个Java锁工具家族的设计脉络。它的哲学是:把线程的排队、阻塞、唤醒这套通用机制,从具体业务里抽象出来,让不同的同步器只需关心state的语义变化。
对于日常开发,我不建议你直接依赖Lock的底层实现去手写同步逻辑,但一定要做到“看得懂、选得对”。比如:
- 需要公平排队,用
ReentrantLock(true); - 读多写少的缓存场景,用
ReentrantReadWriteLock(但写锁饥饿要注意,StampedLock的乐观读是更好的选择); - 多个线程同时到达再一起执行,用
CountDownLatch; - 多线程协作分批处理,用
CyclicBarrier。
4.3 ThreadLocal:线程隔离的“私有空间”
ThreadLocal在高并发场景里也是高频选手,尤其是解决SimpleDateFormat这类非线程安全工具的并发问题,或者在线程池里传递一些非上下文信息。它的机制是:每个Thread内部维护一个ThreadLocalMap,以ThreadLocal实例为key,存储各自的副本值。线程之间互相看不见。
但ThreadLocal有一个必须警惕的坑:内存泄漏。ThreadLocalMap的key是弱引用(WeakReference),而value是强引用。如果ThreadLocal实例不再被外部持有,key会被GC回收变成null,但value依然被线程的Map持有,无法被回收。一旦线程池里的线程存活时间很长,就会产生大量无法回收的value对象,造成内存泄漏。
标准做法是:用完必须remove()。尤其是在线程池场景下,线程复用,如果不清理上一个任务的ThreadLocal值,下一个任务可能会读到“脏数据”。我在项目里定的规矩是:谁set,谁负责在finally里remove,绝不留尾巴。
5. 从理论到项目实战:高并发IM与库存超卖的场景剖析
5.1 高并发IM场景:多线程如何组织
热词里有“高并发IM”,这个场景非常适合用来串知识点。IM系统的核心特点是:
- 连接数极高,且长连接多(比如WebSocket);
- 消息写入频繁,但每一条消息的数据量不大;
- 消息的实时性要求高,不能因为锁竞争导致明显延迟。
这种场景下,多线程的组织方式首先要解决连接和线程的映射问题。通常不是“一个连接一个线程”,那样线程数会爆炸,而是采用Netty这种Reactor模型:少数IO线程(一般等于CPU核心数)负责处理网络事件,业务处理丢给线程池异步执行。这正是“高并发”和“多线程”的经典分层:IO线程要轻,业务线程要控数。
在线程池选择上,IM这种IO密集场景,线程池的核心线程数可以设得偏高(公式参考上文),但队列要设置上限,不能无限积压。因为消息是有时效性的,积压超过几百毫秒用户就感知到卡了。我见过一个IM服务,线程池队列用的是无界LinkedBlockingQueue,结果高峰期内存直接飙到把堆塞满,这是妥妥的“技术债”。
另外IM里的“已读”和“在线状态”,天然适合用ConcurrentHashMap做节点本地缓存,配合分布式缓存做最终一致。每个连接的状态是写多读多的热点数据,本地缓存用并发容器能挡住大部分读压力。
5.2 ERP库存场景:超卖问题怎么解
另一个热词是“ERP库存场景高并发解决方案”,这个场景是并发业务的经典考题。假设只有一个商品,库存只有10件,同时来了20个下单请求,怎么保证不超卖?
先说最笨的方案:数据库行锁。下单时执行UPDATE stock SET count = count - 1 WHERE sku_id = ? AND count > 0,利用数据库的行锁保证原子性。这个方案简单可靠,但在极高并发下会把数据库拖垮——毕竟数据库的连接数是瓶颈。
常用的优化分层如下:
- 本地内存预扣:每个服务节点维护一份热点库存的本地缓存,先在内存里做CAS式扣减(用
AtomicLong或LongAdder),扣减成功才继续走后端流程。这一步能挡掉绝大部分无效流量。 - Redis原子操作:对于分布式环境,用Redis的
DECR或Lua脚本保证扣减的原子性,相当于把库存扣减操作前置到一个高性能的原子服务上。 - 数据库兜底:最终落库时仍然用条件更新
count > 0来兜底,防止Redis万一出现超卖。
在整个链路里,Java多线程的核心作用在于第一层:用并发安全的计数器和锁把“扣减库存”这个过程做成原子操作。很多人问:为什么AtomicLong能扛住高并发?因为它内部就是CAS自旋,没有线程阻塞,所以在高并发短操作场景下,它比synchronized要快得多。但要注意,高冲突场景下CAS自旋会浪费CPU,这时可以考虑LongAdder,它通过分段累加的方式减少冲突,吞吐量更高——不过它返回的只是近似值,热点记录场景足够,强一致的数据统计就不要用它。
5.3 压测是验证并发设计的唯一标准
不管理论设计得多漂亮,上不上得了台面,必须跑一轮压测再说。热词里挂着“jmeter怎么进行接口并发测试”、“数据库并发锁”,说明大家都想用工具验证自己的设计。
JMeter操作本身不复杂:
- 创建线程组,设置线程数和循环次数(模拟并发用户);
- 添加HTTP请求取样器,配置接口地址和参数;
- 添加聚合报告,观察吞吐量(TPS)、平均响应时间、错误率。
但压测的关键不在于点按钮,而在于如何设计压测场景:
- 线程数从50、100、200、500逐级往上,找到“拐点”(即吞吐量不再增长甚至下降的那个点);
- 同时监控服务端的CPU、内存、GC日志、线程池活跃线程数。如果CPU没满但TPS上不去,说明瓶颈在锁竞争或IO等待;如果GC频繁但内存不断增长,大概率有对象泄漏或线程池内存膨胀问题。
- 压测结果出来后,我习惯第一时间看tp99(99%请求的响应时间),平均值会掩盖长尾问题。tp99一旦出现尖刺,多半就是锁竞争加剧或者线程池排队严重。
6. Java多线程面试高频点与套路拆解
6.1 必须张嘴就来的核心八股
结合热词里的“java面试八股文”、“多线程面试题”,我整理了一份自己面试别人时最常问、也最喜欢听你讲出层次感的清单:
- 创建线程的几种方式:
Thread、Runnable、Callable、ExecutorService。注意不要再回答“FutureTask也是一种创建方式”——它本质是包装了Callable的一个任务类,不是新的线程创建方式。面试官问这个题是想听你区分“线程”和“任务”的概念。 - 线程的生命周期:新建、就绪、运行、阻塞、等待、超时等待、终止。重点区分
sleep()和wait():sleep不释放锁,wait释放锁并进入等待队列;sleep是Thread静态方法,wait是Object方法。 - 如何优雅停止线程:不要用
stop()(强制终止会破坏对象的内部一致性)。正确姿势是用volatile boolean标志位或者interrupt()配合Thread.interrupted()检查中断状态,让线程自行判断何时安全退出。 - 锁的宏观家族对比:
synchronized(自动加解锁、JVM层面)、ReentrantLock(可中断、可超时、可公平、需手动解锁,必须放在finally里)、ReadWriteLock(读写分离)、StampedLock(乐观读进一步降低锁竞争)。 - 线程池的核心参数和执行流程:如前面3.2节所述,这块几乎必考,必须能够画着流程讲。
- ThreadLocal的作用和内存泄漏问题:必须说清弱引用和强引用的区别,以及remove的必要性。
- 死锁如何产生和排查:死锁四个必要条件(互斥、持有并等待、不可剥夺、循环等待)要背熟。排查一定要提jstack命令,用
jstack <pid>看线程Dump文件里的“Found one Java-level deadlock”,这是最直接的工具。
6.2 回答面试题的套路:别背结论,讲场景
我给所有准备面试的读者的建议只有一条:每个知识点都要准备一个“为什么”和一段“真实案例”。
比如问你volatile和synchronized的区别,别只背“volatile不能保证原子性”。你可以这样回答:
volatile解决的是可见性和有序性,synchronized解决的是原子性、可见性和有序性。但synchronized是阻塞式实现,会引发线程上下文切换,在临界区极短的场景下开销较大。我之前处理过一个计数器场景,单线程写、多线程读,用volatile就够了;但如果是多个线程并发累加,就必须用AtomicInteger或者synchronized了。
这种回答既有原理深度,又有工程判断,比干巴巴背定义强太多。
再比如,被问“线程池的拒绝策略有哪些”,别只报名字:
AbortPolicy:直接抛异常(默认);CallerRunsPolicy:调用者自己执行,相当于把任务退回提交线程;DiscardPolicy:静默丢弃;DiscardOldestPolicy:丢弃最老的尚未处理的任务。
你可以补一句实战心得:在日志系统里,我通常用CallerRunsPolicy,让生产者自己背着任务跑,反而可以反向压测生产者速度;在实时性要求高的场景我更喜欢DiscardOldestPolicy,因为最新任务比最早任务的业务价值高。这种“场景化表达”就是区分高分答案和平庸答案的分水岭。
7. 常见问题与排查技巧实录
7.1 线上CPU飙高,怎么定位线程问题
查CPU飙升,几乎必然要动线程Dump。标准流程:
top -Hp <pid>找到占用CPU最高的线程ID。- 将线程ID转成十六进制:
printf "%x\n" <tid>。 jstack <pid> | grep -A 20 <十六进制tid>,找到对应线程栈。- 分析栈顶:如果是GC线程,那就去看GC日志和heap dump;如果是业务线程,极大概率是死循环、频繁GC、CtrlC级自旋或者正则回溯等耗CPU操作。
这里我想分享一个真实案例:有一个服务CPU突然跑到99%,压测都压不出这个效果。用上面这套流程定位后,发现是一个HashMap在高并发下扩容导致死循环(JDK 7的遗留代码),线程栈里停在一个诡异的位置。换成ConcurrentHashMap后,CPU立刻恢复正常。排查多线程问题最怕“拍脑袋”,按流程一步步切,定位效率才高。
7.2 线程阻塞了但找不到死锁
有时候线程池拒绝执行任务、接口迟迟不返回,但jstack里找不到死锁,这大概率是等待队列积压。任务都堵在LinkedBlockingQueue里了,线程池所有线程都在处理前面积压的任务,新任务无法被执行,接口自然超时。
对这种问题的排查,我会看三样东西:
- 线程池的活跃线程数:如果长期等于maximumPoolSize,说明线程数达到上限,负载过高;
- 队列大小:如果持续增长,说明生产速度远超消费速度;
- 等待时间:任务在队列里等待时间过长,说明消费能力不足或者下游依赖(数据库、外部API)变慢。
处理方式也有三种路径:扩容线程池(加线程数),优化单任务的耗时(减少锁竞争、批量处理),或者让生产者做熔断降级(丢弃或拒绝堆积任务)。我个人的经验是:大部分“线程卡死”问题,根子其实在下游——数据库连接池耗尽、外部接口超时重试,导致线程池的线程全部卡在IO等待上,而不是线程池本身出了问题。所以排查这类问题,先往下游链路的耗时看,比盲目调大线程池参数靠谱得多。
7.3 数据库并发锁与多线程的交互
热词里有“数据库并发锁”,很多业务开发的第一步锁是数据库锁,然后才意识到需要Java层面的并发控制。数据库的悲观锁(SELECT ... FOR UPDATE)和乐观锁(版本号 + 条件更新)各有适用场景。在高并发Java服务里,对数据库锁的优化方向就是“把并发尽量挡在数据库外面”——用Java的并发容器、分布式缓存、本地锁或Redis原子操作来削峰,让真正打到数据库的请求量降下来。
Java层面的锁和数据库锁会形成叠加关系:Java锁的作用是控制同一JVM内的线程不并发修改,数据库锁的作用是控制跨节点的并发修改(比如多个服务实例操作同一行记录)。两者不是互斥,而是协作关系。如果你在一个分布式环境里,只靠synchronized锁本机线程,根本没有用,别的机器照样并发修改,这个时候就要引入分布式锁(Redis的SETNX + Lua,或者ZooKeeper的顺序节点),这也是高并发面试的热门延伸点——从单机并发到分布式并发,思维方式要切换。
8. 写在最后的经验之谈
从一个“会用synchronized”的初级选手,到一个“能设计整套并发方案”的工程师,我走了不少弯路。回头总结,有几点经验特别想分享给你:
第一,永远不要迷信某个工具是银弹。ConcurrentHashMap再好,不适合的场景硬用反而徒增复杂度;volatile再轻量,复合操作照样翻车。每一种并发技术都有自己的边界,能说出它的适用场景和失效条件,才是真懂。
第二,并发场景首先设计数据共享策略,再选同步工具。很多并发Bug的根源是设计上就没想清楚“这个数据该不该被多个线程共享”。能不用共享就不共享(比如用ThreadLocal、用不可变对象),共享尽量缩小范围,剩下的才轮到锁、CAS这些工具上场。我发现一个有意思的现象:很多代码之所以复杂,但并发问题不断,就是因为本来不需要共享的数据,硬是塞进了一个全局容器里。
第三,排障工具练成本能。线上出问题时,jstack、jstat、jmap这些命令要信手拈来,不要等到服务器冒烟了才去翻文档。我平时会定期给服务做线程Dump,看看线程池状态和锁竞争情况,这种“预防式体检”能提前发现很多隐患。
最后再送一个小技巧:写多线程代码时,习惯性地给线程起有业务含义的名字。看起来是件小事,但真到了线上排障,一堆thread-1、thread-2的命名会让人抓狂;如果是order-async-process-pool-3这种名字,你一眼就能定位到是哪个业务模块在干活。实践里,我见过因为线程名太随意,排障多花了几个小时的真实案例,这种低级成本的节省,值得从第一天开始养成。