☰
Java多线程高并发底层原理:从JMM到锁升级与线程池
2026/10/1 15:07:13 网站建设 项目流程

Java多线程高并发这块,很多朋友都卡在“会用”但“不懂原理”的阶段。背了一堆面试题,synchronized怎么用、线程池几个参数分别什么意思都门儿清,可一旦遇到线上性能瓶颈、诡异的并发Bug,就完全无从下手。这篇文章我会直接从底层原理切入,把线程的底层模型、Java内存模型、锁的实现机制、AQS和线程池的底层逻辑一条线串到底,让你知其然更知其所以然。


1. 线程的本质与高并发系统的底层基石

1.1 先搞清一个问题:我们说的“多线程”,底层到底跑在什么上面

很多人写多线程代码,却从来没有停下来想过——new Thread()出来的这个线程,在Java虚拟机层面到底是什么?到了操作系统层面又是什么?

Java线程在JDK 1.2之后采用了一对一(1:1)的线程模型,也就是一个Java线程对应一个操作系统原生线程。你在代码里创建一个线程,JVM会通过底层的native方法调用,向操作系统申请创建一个内核线程,然后Java层面的线程栈、程序计数器、局部变量表这些运行时数据,全部关联到这个内核线程上。

所以当你写出几十个线程的时候,真相是:你的JVM进程向操作系统申请了几十条内核线程,操作系统内核在这些线程之间做调度、切换。这也是为什么很多新手在Windows、Linux上跑多线程程序,任务管理器里能看到对应数量的线程条目——这不是巧合,就是1:1映射的直接体现。

注意:从JDK 21开始,Java推出了虚拟线程(Project Loom的成果),它的底层是JVM自己调度的用户态线程,不再完全依赖内核线程。但在生产环境主流版本(Java 8/11/17)里,我们讨论的依然是传统的内核线程模型。

1.2 线程的生命周期状态,远不止你背的那六种

Java线程有六种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。这个八股文大家都会背,但有几个细节值得深挖。

第一,RUNNABLE 状态其实是“虚胖”的。Java层面处于RUNNABLE的线程,底层可能是三种情况之一:正在CPU上执行、在操作系统的就绪队列里排队等CPU、或者是被操作系统从CPU上抢下来等着下一次调度。也就是说,Java层面的RUNNABLE并不代表线程此刻真的在“跑”,它只是表示“没有主动阻塞在锁或等待队列上”。

第二,BLOCKED和WAITING的区别很多人理解错了。BLOCKED是线程在竞争synchronized监视器锁时,没抢到锁,被阻塞在临界区入口。而WAITING是线程已经持有了锁,主动调用了wait()或者join()挂起自己。一个是“想进去进不去”,一个是“进去了自己主动出来等着”。

第三,从调度的角度讲,操作系统并不认识Java的六种状态,它只知道就绪、运行、睡眠等原生状态。JVM在实现线程状态转换时,需要把Java语义映射到操作系统语义上。比如Java的WAITING在某些实现下就是操作系统层面的睡眠等待。

1.3 为什么说线程切换是高性能系统的头号敌人

聊多线程高并发,不得不提上下文切换的开销。所谓上下文切换,就是操作系统保存当前线程的上下文(寄存器、程序计数器、栈指针、内存映射等),然后加载下一个线程的上下文,让CPU去执行另外一个线程。

这个开销有多大?一次上下文切换在几微秒到几十微秒之间,听起来很便宜,但如果你一个线程只运行几百微秒就去抢锁、然后被阻塞、被唤醒、再切换,那大部分时间其实都消耗在切换上了。很多高并发系统性能上不去,不是代码逻辑不高效,而是线程数开得太多、抢锁太频繁,导致CPU大部分时间花在保存和恢复现场,真正干活的时间反而很少。

一个判断系统是否过度切换的实用方法:用top -H -p PID看看单个进程内线程数,或者用vmstat观察cs(context switch)列。如果每秒上下文切换次数动辄几十万甚至上百万,就要警惕是不是线程模型设计出了问题。


2. Java内存模型:高并发底层原理的“宪法”

2.1 为什么需要一套内存模型,CPU缓存不一致问题从何而来

多线程并发有一个绕不开的底层背景:CPU的运算速度和内存的访问速度差距太大。现代CPU为了解决这个问题,引入了多级缓存:L1、L2、L3。每个CPU核心都有自己的L1/L2缓存,一个线程运行在某个核心上时,它操作的数据会被复制到该核心的缓存里。

问题就来了。假设线程A和线程B分别运行在两个核心上,同时读写同一个共享变量,A改了缓存里的值,B还在用自己的缓存副本,两边看到的数值不一致。如果没有协议去协调,数据就乱了。

硬件层面解决这个问题靠的是缓存一致性协议,最经典的就是MESI协议。MESI用四种状态标记缓存行:Modified、Exclusive、Shared、Invalid。一个核心修改数据时,要先通知其他核心把相关缓存行标记为Invalid,其他核心再读的时候发现缓存行失效,需要重新从主内存去读。

2.2 Java内存模型(JMM)到底规定了什么

Java内存模型(JMM)是JVM规范层面的内存抽象。JMM定义了主内存和工作内存的概念:所有变量都存储在主内存中,每个线程拥有自己的工作内存,线程读变量时先把主内存的值拷贝到工作内存,写变量时先写工作内存,再刷回主内存。

这个模型和硬件层的CPU缓存结构是对应的:主内存对应物理内存,工作内存对应CPU缓存或者寄存器。

JMM要解决的核心问题有三个:原子性、可见性、有序性。

原子性,强调一个操作要么全部执行成功要么完全不执行,不可被线程调度机制打断。可见性,强调一个线程修改了共享变量后,其他线程能否立刻看到这个修改。有序性,强调编译器和CPU为了性能做的指令重排,是否会破坏程序语义。

2.3 happens-before规则:判断并发正确性的唯一武器

JMM通过happens-before规则来约束可见性和有序性。这个规则是面试必考内容,也是排查并发Bug的重要思想工具。

核心规则包括:程序次序规则(同一个线程内,前面的操作happens-before后面的操作);监视器锁规则(解锁操作happens-before后续对同一个锁的加锁操作);volatile变量规则(对volatile字段的写操作happens-before后续对这个字段的读操作);传递性规则(A happens-before B,B happens-before C,则A happens-before C),以及线程启动、线程终止、中断操作等规则。

记住一个关键点:happens-before关系并不意味着前一个操作必须“先执行完”后一个操作才开始,它保证的是“前一个操作的结果对后一个操作可见”。这是很多并发Bug难排查的根源——代码执行顺序上看起来不对,但在JMM的语义下却是合法的。

底层原理上,JMM的实现依赖两个手段:内存屏障和锁机制。volatile通过插入内存屏障禁止指令重排、保证写刷回主存;synchronized通过锁的获取和释放建立临界区的内存屏障语义。


3. 从对象头到监视器锁:synchronized 的底层实现深度拆解

3.1 synchronized 锁的信息藏在对象头里

很多人不理解synchronized“锁的是对象”这句话的底层含义。一个Java对象在内存中的布局包括三部分:对象头、实例数据、对齐填充。锁的核心信息就存在对象头里。

对象头里的Mark Word字段,在64位JVM底层是64位的数据结构,它的含义会随着对象状态变化而变化:

  • 无锁状态:存储对象的哈希码、GC分代年龄等
  • 偏向锁状态:存储持有锁的线程ID
  • 轻量级锁状态:指向栈中锁记录的指针
  • 重量级锁状态:指向Monitor监视器对象的指针

顺便说一句,很多面试题问“为什么重写equals必须重写hashCode”,这跟对象头其实是相关的——不重写hashCode会导致两个逻辑相等的对象有不同的哈希码,而HashMap底层要根据hashCode确定桶位。

3.2 锁升级:偏向锁、轻量级锁、重量级锁的完整链路

JDK 1.6之后synchronized做了大量优化,锁不再是“一上来就是重量级”,而是有一个锁升级的过程:

偏向锁:当第一个线程进入同步块时,JVM会把Mark Word设置为偏向锁模式,记录下这个线程的ID。之后这个线程再次进入同步块,只需要检查Mark Word里的线程ID是否是自己,如果不是自己就用CAS尝试替换,如果是就直接进入,不需要任何原子操作和系统调用。

轻量级锁:当有第二个线程竞争偏向锁时,偏向锁会撤销,升级为轻量级锁。轻量级锁的核心是CAS自旋。线程在栈帧中创建锁记录空间,尝试用CAS把Mark Word拷贝到锁记录并更新指针。如果竞争不激烈,自旋一会儿就能拿到锁,避免了线程阻塞和唤醒带来的用户态内核态切换。

重量级锁:当CAS自旋超过一定次数(默认自适应自旋),或者CPU核心数过多、自旋成本太高时,锁膨胀为重量级锁。重量级锁依赖操作系统的互斥量,线程会真正阻塞,涉及用户态到内核态的切换,开销大但绝对公平,不会出现CPU空转。

这里有一个高频面试点:为什么要有偏向锁和轻量级锁,直接都用重量级锁不行吗?答案很简单——绝大多数场景下,锁竞争并不激烈,甚至大量场景是同一个线程反复进入同一个同步块。如果每次都走操作系统级别的锁,开销是无法接受的。偏向锁就是为了解决“单线程反复加锁”这种场景,轻量级锁是为了解决“两个线程短暂交替竞争”的场景,只有真正竞争激烈的场景才让锁膨胀到重量级。

3.3 Monitor机制与wait/notify的底层联系

synchronized加锁的本质,是让一个线程获取到对象关联的Monitor监视器锁。在HotSpot虚拟机里,Monitor底层的核心数据结构包括:Owner(当前持有锁的线程)、EntryList(等待获取锁的线程队列)、WaitSet(调用wait方法后释放锁并等待被唤醒的线程队列)。

理解了这套结构,就能理解wait()/notify()的底层逻辑:

  • wait()的前提是当前线程持有该对象的Monitor,然后线程释放Monitor并进入WaitSet等待
  • notify()从WaitSet中随机唤醒一个线程,被唤醒的线程需要重新去EntryList竞争Monitor
  • notifyAll()把WaitSet中所有线程移入EntryList

用现实类比就是:synchronized是拿到了餐厅包间的钥匙才能进门,wait是你进了包间后主动把钥匙放桌上、然后出门排队,notify是前台喊了一个排队的人来拿钥匙。这也解释了为什么wait必须在synchronized代码块里调用——如果没有持锁,你连进包间的资格都没有,更谈不上“把钥匙放桌上”。


4. volatile、CAS与并发工具类:无锁并发的底层逻辑

4.1 volatile 的内存屏障语义:不只是“可见性”

volatile最常见的一句话解释是“保证共享变量的可见性”,但只理解到这一步,很难应对真正的并发问题。从底层看,volatile实际上做了两件事:

第一,禁止指令重排。JVM在volatile字段读写的前后插入内存屏障。读操作之后插入LoadLoad和LoadStore屏障,写操作之前插入StoreStore屏障、之后插入StoreLoad屏障,确保volatile写的值对其他线程可见时,前面的普通写也全部刷回主存。

经典的DCL(双重检查锁)单例模式就是典型的volatile应用场景。在没有volatile时,对象实例化过程可能被重排为:

  • 分配内存空间
  • 将内存地址赋值给引用
  • 执行初始化

如果另一个线程在“赋值”之后“初始化”之前读取了引用,拿到的就是一个半初始化的对象。加上volatile后,通过禁止重排,保证初始化完成之后引用才对外可见。

第二,确保读写都能直接命中主内存。volatile变量的读写不会被缓存到寄存器或CPU缓存中,每次写入都强制刷回主内存。

实际开发中,使用volatile的场景主要有几个:状态标志位(一个线程写、其他线程读)、双重检查锁、以及作为轻量级的“发布安全”保障。但是volatile不能保证复合操作的原子性,比如count++这种读改写操作,用volatile修饰依然是线程不安全的,需要Atomic类或者锁来兜底。

4.2 CAS:原子操作的无锁实现与ABA问题

CAS(Compare And Swap)是实现无锁并发的核心原语。它的语义是:比较内存值是否为预期值,如果是就用新值替换,否则什么都不做。底层是由CPU提供的原子指令(如x86的CMPXCHG)完成的,保证整个比较交换操作不可被中断。

Java里的AtomicInteger、AtomicLong、ConcurrentHashMap的很多操作,底层都是通过Unsafe类调用CAS实现的。

CAS有几个关键问题值得深聊:

第一,ABA问题。线程1把值从A改成B又改回A,线程2用CAS比较时发现还是A,就误以为没人动过。解决的思路是加版本号,AtomicStampedReference就是为此设计的。

第二,自旋开销。CAS失败后通常要循环重试,竞争激烈时CPU空转严重,在高冲突场景性能会急剧下降。这也是为什么很多底层算法会在CAS重试一定次数后让出CPU。

第三,只能保证单个变量的原子性。如果要原子更新多个变量,CAS无能为力,需要锁或者把多个变量封装到一个对象里用AtomicReference。

4.3 AQS:撑起JUC半边天的并发框架基础

聊高并发底层原理,不能不提AQS(AbstractQueuedSynchronizer)。它是ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock这些JUC工具类共同的底层骨架。

AQS的核心是一个state状态变量加一个CLH变体队列。state的含义由子类自定义:ReentrantLock里表示持有锁的次数,Semaphore里表示剩余许可数,CountDownLatch里表示还需倒数的计数值。所有获取锁或资源失败的线程,会被包装成Node节点挂到队列尾部自旋或者阻塞等待,当前一个节点释放资源时,会唤醒后继节点继续竞争。

理解了AQS,很多JUC工具类的“为什么这么设计”就迎刃而解了。比如为什么ReentrantLock支持公平锁和非公平锁?本质上就是在入队之前是直接CAS抢一次状态(非公平),还是严格入队等待(公平)。为什么Semaphore的许可可以“并发获取多个”?因为state可以一次性减N。

在网上搜索java八股文类资料时,AQS是必背内容,但真正生产环境里,我建议大家把AQS理解为“JUC并发工具的底层状态机”,这样再看任何并发类的源码都有一种“原来是同一套骨架”的通透感。


5. 线程池底层原理:高并发系统的“线程管理中枢”

5.1 线程池的核心参数之间到底是怎么配合的

ThreadPoolExecutor是Java高并发编程中使用率最高的类之一。它的参数很多,但在底层原理层面,真正要理解的是这套状态机的运转逻辑。

核心参数包括:核心线程数corePoolSize、最大线程数maximumPoolSize、空闲线程存活时间keepAliveTime、工作队列workQueue、线程工厂threadFactory、拒绝策略handler。

线程池的提交任务流程是这样的:

  1. 提交任务时,如果当前线程数 < corePoolSize,创建新线程执行任务
  2. 如果线程数 >= corePoolSize,任务入队等待
  3. 如果队列已满,且线程数 < maximumPoolSize,创建新线程执行任务(这些非核心线程在空闲keepAliveTime后会销毁)
  4. 如果队列已满且线程数 == maximumPoolSize,执行拒绝策略

这里有个高频坑:很多人以为“线程数先到最大线程数才入队”,这是完全错的。真实的执行顺序是,核心线程先干活,干不过来了任务先排队,队列满了才加临时线程。如果队列是LinkedBlockingQueue(默认无界队列),第三步永远不会触发,maximumPoolSize形同虚设。

5.2 线程池的状态机:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED

ThreadPoolExecutor用一个AtomicInteger同时记录线程池状态和线程数量:高3位表示状态,低29位表示线程数。这种“一个变量装两个信息”的设计不是为了炫技,而是为了让“CAS修改线程池状态+修改线程数”这个操作保持原子性,因为这两个字段经常需要同步判断。

  • RUNNING:可接受新任务,也可处理队列任务
  • SHUTDOWN:调用shutdown()后进入,不再接受新任务,但已入队的任务会继续执行
  • STOP:调用shutdownNow()后进入,不再接受新任务,不处理队列任务,并中断正在执行的任务
  • TIDYING:所有任务结束后进入,执行terminated()钩子
  • TERMINATED:terminated()执行完后的最终状态

线上问题排查时,看到线程池“卡住不干活”但线程数不为0,很多情况下是因为线程池进入了SHUTDOWN状态,队列里还有任务但不再执行,或者线程都阻塞在一个获取不到的任务上。看线程池状态时不要只看监控面板上“线程数”,要看“队列积压量”和“任务完成数”。

5.3 拒绝策略与任务队列的选择:底层参数的实战考量

四种拒绝策略的区别很多人背过,但背后的设计考量值得展开:

  • AbortPolicy(默认):直接抛RejectedExecutionException,让调用方感知任务被拒,适合对丢任务零容忍且可降级的场景
  • CallerRunsPolicy:谁提交谁执行,等于在当前线程里直接跑任务。好处是能自然限流——调用线程一边执行任务一边无法继续提交,系统压力被反馈到上游
  • DiscardPolicy:静默丢弃,适合允许丢数据的日志采集场景
  • DiscardOldestPolicy:丢弃队头任务再重新提交,适合追求新任务优先执行的场景

队列选择上,LinkedBlockingQueue无界队列“永不拒绝”但任务会堆积,内存可能被拖垮;ArrayBlockingQueue有界队列配合合理池数和策略,才能真正发挥线程池“削峰填谷”的作用。SynchronousQueue不存任务,直接交给线程,适合极端追求低延迟的场景。


6. 高并发线上问题排查与经验沉淀

6.1 死锁的定位:jstack实战记录

两线程互相持有对方需要的锁、且彼此不肯释放,就形成死锁。排查死锁最经典的手段就是使用JVM自带的jstack工具。

具体步骤是:

  • 用jps找到JVM进程ID
  • 用jstack PID抓取线程快照
  • 在输出中查找“Found one Java-level deadlock”字样,后面会列出死锁线程的详细信息

我在实际运维中,还遇到过一种“假死锁”——代码没死锁,但线程全部阻塞在某个第三方SDK的锁上,导致Tomcat的工作线程全部被占满,服务表现为“假死”。这种情况下jstack里会看到大批线程卡在同一个类名的方法栈上,顺着栈帧就能定位到是哪个SDK、哪个接口拖死了线程池。

6.2 锁竞争激烈、上下文切换过高怎么定位

高并发系统的很多性能问题,最终都归结到锁竞争和上下文切换上。定位思路一般是这样:

先用top -H看CPU占用,如果多核CPU利用率不均衡,某些核打满、某些闲着,大概率是锁竞争造成的,因为抢不到锁的线程只能等待。

再用jstack抓线程状态,重点关注两个指标:BLOCKED状态的线程数量和waiting to monitor关键字。只要看到大量线程阻塞在某个monitor对象上,就说明该锁是当之无愧的“热点锁”。

优化热点锁的思路有优先级:先看能不能缩小同步块(把耗时操作移出锁外);再看能不能用并发容器替代锁(如ConcurrentHashMap、CopyOnWriteArrayList);最后才是换锁类型(synchronized换成ReentrantLock、读写锁、甚至无锁CAS)。

特别提醒:很多新手喜欢“一有并发问题就上ConcurrentHashMap”,但并发度低、写多读少的场景,ConcurrentHashMap的CAS+链表扫描开销可能反而比普通HashMap加锁更高。先量化锁竞争的程度,再谈优化手段,这才是专业的做法。

6.3 ThreadLocal泄漏:一个容易被忽略的底层细节

ThreadLocal在多线程场景下用来做线程上下文隔离非常方便,但它有内存泄漏风险。这要从底层结构说起:每个Thread内部有一个ThreadLocalMap,key是ThreadLocal对象本身,value是你要存的数据。

坑在于:ThreadLocalMap的Entry继承WeakReference,key是弱引用,而value是强引用。当外部对ThreadLocal的强引用被置为null后,key会被GC回收,但value还在,并且因为Thread对象可能存活很长时间(例如线程池里的线程),value就永远无法被回收。

这就是为什么很多规范建议:用完ThreadLocal后必须调用remove()。线程池场景下尤其如此,因为线程是复用的,上一个请求的数据可能被下一个请求读到,造成难以追踪的数据串扰问题。

6.4 底层原理对高并发系统设计的实际指导

学习这些底层原理的最高境界,是形成一套“并发设计直觉”。几句话总结我的体会:

线程不是越多越好,线程数和CPU密集型/IO密集型任务强相关,核心逻辑是让“线程都在干活而不是都在切换”。

锁是并发冲突的放大器,写代码时保证并发安全的边界越小、越快加锁越快释放,系统吞吐量的提升就越明显。

无锁不一定比加锁快,但CAS自旋在低竞争场景的开销要远小于线程阻塞唤醒的系统调用;反过来在高竞争场景,无锁自旋又会浪费大量CPU,这时候重量级锁让线程排队休眠反而是更优解。

做高并发系统方案设计时,从“共享可变状态”到“不可变对象”再到“线程封闭”(ThreadLocal或栈上隔离),优先级是从高到低的。能不改就不改、能不可变就不可变、实在要共享就用锁和并发工具管好访问。


7. 面试题场景下的底层原理输出技巧

7.1 “讲讲synchronized的锁升级”怎么答才不显得像背课文

很多Java面试者能流利背出“偏向锁->轻量级锁->重量级锁”,但面试官追问一句“为什么要有偏向锁”就卡住了。要答出区分度,必须把“性能取舍”讲透。

推荐的回答脉络是:先讲对象头Mark Word存储锁信息的机制;再讲锁升级是为了适配不同竞争强度的性能优化——单线程重复加锁用偏向锁避免原子操作,低竞争用CAS自旋避免用户态内核态切换,高竞争才用重量级锁保证不空转CPU。最后能补一句“锁升级是不可逆的,一旦膨胀到重量级锁,后续即使竞争消失了也回不到偏向锁”,这就能体现出对底层实现的理解深度。

7.2 “ConcurrentHashMap为什么效率高”要从哪几个层次答

这个问题最能区分“背过八股”和“真懂并发”。一个完整的回答应该覆盖:

第一层:锁粒度——JDK 1.7用分段锁Segment,把整表锁拆成16段,JDK 1.8直接放弃分段锁,改用Node数组 + CAS + synchronized,锁粒度精细到单个桶位。

第二层:无锁路径——初始化的部分用CAS保证线程安全,空桶插入用CAS直接写入,不需要加锁。

第三层:锁优化——链表转换成红黑树后,缩小单个桶内遍历的复杂度;桶内用synchronized锁住链表头节点,避免对整个map操作。

能把这几个层次串起来,面试官会觉得你不只是会用ConcurrentHashMap,是真的理解它底层的设计逻辑。

7.3 高频面试问题速查表

问题核心考点底层原理要点
volatile能保证原子性吗可见性与原子性的区别volatile只能保证单次读写可见,不能保证复合操作原子性
wait和sleep的区别锁释放与状态切换wait释放Monitor并进入WaitSet,sleep不释放锁
为什么线程池不建议用Executors任务队列和OOM风险newFixedThreadPool默认无界队列LinkedBlockingQueue,任务积压可能OOM
如何实现分布式锁跨进程互斥与原子操作单机AQS无法跨进程,需Redis SETNX或数据库唯一索引
HashMap在多线程下会怎样线程安全性来源并发put可能触发扩容造成环形链表,JDK 1.7就有这个问题
ReentrantLock和synchronized区别锁的实现层次ReentrantLock基于AQS,支持公平锁、可中断、可超时
什么是伪共享缓存行与并发性能CPU缓存行无效化导致原本不冲突的变量互相拖累

7.4 一个完整的按层次答题示例

当面试官问“Java高并发是怎么保证数据一致性的”,完整的回答应该从内存模型讲到Java提供的层层递进的工具:

第一层,JMM定义主内存与工作内存的关系,通过happens-before规则约束可见性,这是所有并发一致性的基础。

第二层,语言级关键字:volatile解决可见性和有序性,synchronized解决原子性问题。

第三层,JUC类库:Atomic类基于CAS无锁原子操作,AQS是ReentrantLock、Semaphore等工具的统一骨架。

第四层,并发容器:ConcurrentHashMap、CopyOnWriteArrayList针对特定读写场景做并发优化。

第五层,分布式场景下的最终一致性方案,比如MQ异步削峰、本地消息表+定时任务对账等。

这套回答从单机到分布式层层递进,既覆盖了底层原理又展现了系统架构能力,是面试中比较加分的一个结构。


8. 写在最后的一些实践心得

我这些年排查线上并发问题的经验,有一条最重要的体会:底层原理不是用来背的,是用来定位问题的坐标系。没有JMM的概念,线上出现一个“偶发性的数据错乱”,你根本不知道怎么分析;没有线程状态和锁升级的知识,jstack打出来的线程快照在你眼里就只是密密麻麻的乱码;没有AQS的理解,别说优化并发工具,连自定义一个限流组件都无从下手。

另一个体会是:学习底层原理最好的方式是配合真实案例。起一个高并发压测脚本,用jstack抓一次线程状态,用top -H看一次CPU分布,用jmap看一次堆内存,那些概念就真正从纸上落到脑子里了。不要追求一次性看懂全部,先拿synchronized和线程池开刀,逐步扩大到JMM、AQS、并发容器,这条路走通了,你再看任何Java并发的源码都有一种“原来还是这套东西”的从容感。

最后分享一个小技巧:给线程命名。无论是ThreadPoolExecutor的threadFactory还是手动new Thread,都给它起个有意义的名字。并发问题排查时,jstack里一堆pool-3-thread-1只能告诉你这是个不知道哪里来的线程,但一个叫order-service-timeout-checker的线程,你一眼就能知道该去查哪一段代码。这个习惯在关键时刻能救你一次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询