ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java并发编程全优笔记:JMM、锁与线程池实战解析

Java并发编程全优笔记:JMM、锁与线程池实战解析 这两年面试Java后端几乎每一轮技术面都绕不开一道题——并发编程。不管是校招还是社招问线程池参数、问synchronized和ReentrantLock的区别、问ConcurrentHashMap的扩容机制都是家常便饭。我自己当年啃《Java并发编程的艺术》那本书时啃了好几遍才把JMM、AQS这些概念真正串起来后来工作里排查线上线程阻塞、调优线程池又把那些知识从头到尾验证了一遍。所以当我看到“阿里并发编程全优笔记2026最新版”这个标题时第一反应是这不就是当年我想要的那份资料吗把散落在源码、书籍、面试题里的并发知识点按照一条清晰的脉络整理成体系既适合面试突击也适合日常工作查漏补缺。这篇笔记的核心就是围绕Java并发编程构建一套完整知识框架从JMMJava内存模型到锁机制、从并发容器到线程池、从工具类到问题排查覆盖了并发编程中最常考、最常用的知识点。适合这些读者准备Java后端面试的候选人想系统梳理并发知识的中级工程师以及那些线上遇到过死锁、线程池打满、数据不一致等并发问题但还没想透根因的人。内容不追求堆砌冷门概念而是把每个知识点背后的“为什么”讲明白——为什么要有可见性为什么要用AQS为什么线程池参数这么调这些搞清楚了面试和实战都不会慌。1. 并发编程的知识地基JMM、三大特性与线程模型1.1 Java内存模型看清并发问题的根源很多初学者学并发上来就背synchronized和lock结果面试被问到“为什么会有并发问题”就卡住了。所有并发问题的根源其实都指向一个东西——Java内存模型简称JMM。JMM定义了一套规则规定线程和主内存之间的交互方式。简单理解Java中每个线程都有自己的工作内存线程栈中的一部分线程操作变量时不会直接读写主内存而是先把主内存的变量拷贝到自己的工作内存操作完成后再刷回主内存。这个设计是为了性能因为CPU直接操作主内存很慢多级缓存就是为了缓解速度差异但在多线程场景下它直接引发了三个经典问题原子性、可见性、有序性。这里用一个生活化类比假设主内存是公司前台的白板每个线程是独立的工位每个人手里有一张白板的复印件。线程A在白板上写下“库存100”然后拿起复印件改了改线程B也在操作同一份数据但它看到的复印件还是旧的“库存200”。两个人各改各的最后白板上的数到底是多少完全取决于谁先把复印件贴回白板。这就是并发的混乱来源。JMM的核心规则包括synchronized的锁规则、volatile的可见性规则、final的不变性规则以及Happens-Before原则。Happens-Before是理解JMM的钥匙它规定了一组“只要满足这些关系前面的操作结果对后面一定可见”的规则包括程序次序规则、监视器锁规则、volatile变量规则、传递性等。不需要背全但至少要知道如果一个操作不满足Happens-Before关系就不能假设另一个线程能看到它的结果。1.2 原子性、可见性、有序性三个必须刻进脑子的特性并发编程要解决的所有问题都可以归纳到这三个特性上。原子性指的是一个操作或多个操作要么全部执行且不被中断要么全部不执行。典型例子是i它看起来是一行代码但底层是“读取-修改-写入”三步。两个线程同时执行i结果可能不是期望的2而是1。volatile能保证可见性和有序性但保证不了原子性所以volatile的i依然会丢值。保证原子性的手段是synchronized、Lock或者Atomic类基于CAS。可见性指的是当一个线程修改了共享变量其他线程能立刻看到修改。volatile关键字就是干这个的对被volatile修饰的变量写操作会直接刷到主内存读操作会直接从主内存读。但注意volatile只能保证单个变量的可见性不能保证复合操作的原子性。有序性指的是程序执行的顺序按照代码的先后顺序执行。但编译器和CPU为了优化性能会做指令重排。单线程下重排不影响结果多线程下就可能出问题。经典的“双重检查锁单例”就是有序性问题的高发区所以单例要用volatile修饰instance防止“分配内存-初始化-赋值”这三步被重排成“分配内存-赋值-初始化”导致其他线程拿到一个未初始化完成的对象。这三个特性不是孤立的它们经常交织在一起。比如volatile可以解决可见性和有序性但解决不了原子性synchronized三个都能解决但性能开销更大。理解这个“分工”逻辑才能在具体场景里选对工具。1.3 线程的生命周期与上下文切换开销线程是并发编程的基本执行单元。Java中线程的状态有六种NEW新建、RUNNABLE可运行、BLOCKED阻塞、WAITING等待、TIMED_WAITING超时等待、TERMINATED终止。从状态名就能大致猜到触发条件进入synchronized块但抢不到锁会变成BLOCKED调用wait()或join()会变成WAITING调用sleep(timeout)或wait(timeout)会变成TIMED_WAITING。面试常问的一个点sleep和wait有什么区别sleep是Thread的方法不释放锁wait是Object的方法释放锁。前者是“我抱着锁睡”后者是“我放开锁让别人先来”。这个区别直接决定了它们在生产者消费者模型中怎么配合。上下文切换是并发编程里的隐形性能杀手。操作系统在多个线程之间切换时需要保存当前线程的执行状态、加载下一个线程的状态这个过程是有成本的。如果线程数量开得太多大量CPU时间会浪费在上下文切换上而不是真正执行任务。实测经验在I/O密集场景下线程数可以设置得比CPU核数多一些但在CPU密集场景下线程数设置为CPU核数1左右通常最优。这个经验我在后面线程池部分还会详细展开。2. 锁与同步机制从synchronized到AQS的底层演进2.1 synchronized的锁升级偏向锁、轻量级锁、重量级锁synchronized是Java内置的关键字也是并发编程的基础工具。很多人以为它只是简单粗暴地给方法或代码块加锁但JDK 6之后synchronized经历了大规模优化不再是过去那个“重量级选手”了。它在运行时随着竞争激烈程度会有四次锁状态升级无锁 → 偏向锁 → 轻量级锁 → 重量级锁。这个过程可以类比公共厕所的排队机制。无锁就是厕所没人用偏向锁相当于这个厕所长期只有一个人用门上挂了“此人专用”的牌子这个人来了直接进不用等待当另一个人也来了发现牌子不是自己的牌子撤销进入轻量级锁模式——相当于两个人轮流用但都用很短时间不需要真正排队用CAS自旋尝试拿到锁如果竞争越来越激烈自旋也拿不到就升级为重量级锁——相当于人多到必须叫号排队没叫到的都去休息室等着线程阻塞。这个升级机制带来的直接启示是写并发代码时尽量让锁的持有时间短、竞争不激烈这样锁大概率停留在偏向锁或轻量级锁阶段性能损耗很小。反过来如果锁保护的是一个耗时的IO操作那么即便用了synchronized也很快会升级到重量级锁性能就不好看了。有个很隐蔽的坑偏向锁在竞争发生时需要撤销偏向这个撤销操作本身有开销。JDK 15之后偏向锁被废弃了默认就是轻量级锁起步。所以2026年写代码的时候不需要太纠结偏向锁的问题但面试如果聊到锁优化演进这些历史还是要讲清楚的。2.2 ReentrantLock与AQS手写锁之前先读懂它synchronized是隐式锁用起来简单但功能有限。碰到需要尝试获取锁、超时获取锁、可中断获取锁、公平锁等场景就得请出ReentrantLock了。ReentrantLock的核心是AQSAbstractQueuedSynchronizer。AQS是JUCjava.util.concurrent包的基石ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock全都是基于它实现的。理解AQS就等于理解了JUC一半。AQS的设计思路是一个状态位state加一个FIFO等待队列。拿ReentrantLock举例线程尝试获取锁时用CAS尝试把state从0改成1改成功就拿到锁了没改成功就进入等待队列通过LockSupport.park()挂起自己。释放锁时把state改回0然后唤醒队列里第一个等待的线程。这里有一个容易被忽略的细节ReentrantLock是可重入的。同一线程多次调用lock()state会累加比如连续lock三次state3每次unlock()递减减到0才真正释放锁。重入的机制保证了同线程嵌套调用不会死锁。很多人背八股会说“synchronized和ReentrantLock的区别”但面试官更想听到的是你在什么场景下会选择哪一个。我的经验是能用synchronized就用synchronized代码简洁、不易出错而且JDK底层一直在优化需要尝试获取锁tryLock、需要线程可中断、需要公平锁策略时才考虑ReentrantLock。另外synchronized是JVM层面实现的发生异常时会自动释放锁ReentrantLock必须在finally里手动unlock否则锁永远不会释放这本身就是很常见的线上故障来源。2.3 读写锁和StampedLock什么时候该用哪种锁业务场景里有一种很常见的情况读操作远多于写操作。比如一个配置中心大部分时间都在读配置很少更新配置。如果用synchronized或ReentrantLock所有读线程都要互斥排队显然浪费了并发读的能力。ReentrantReadWriteLock就是为此设计的读锁是共享的多个线程可以同时持有读锁写锁是排他的只有拿到写锁才能写。读-读不互斥读-写互斥写-写互斥。这个机制在“读多写少”的场景下能大幅提升吞吐。但ReentrantReadWriteLock有个问题读锁和写锁之间是互斥的如果读线程很多写线程可能会一直等待甚至造成写线程饥饿。JDK 8引入了StampedLock它提供了三种模式写锁、悲观读锁、乐观读。乐观读不需要真正加锁它先读出数据之后判断在读期间有没有写操作发生通过stamp校验如果没有直接用读出的数据如果有再升级为悲观读锁重新读一遍。这种“乐观”策略在读多写少、且读操作很轻量的场景下性能很好。不过StampedLock不能重入且不支持条件变量API比较原始日常业务代码里用得不算多更多出现在框架层。我的建议是先掌握synchronized和ReentrantLock再理解ReentrantReadWriteLock的应用场景StampedLock了解原理即可不去深究。3. 并发容器与线程池高频实战场景的源码级解析3.1 ConcurrentHashMap为什么它是并发场景的首选Map并发场景下使用Map很多人第一反应是Hashtable或者给HashMap加synchronized。这两种做法在JDK 8之后都不推荐了。Hashtable是给整个表加锁并发写时所有线程都串行Collections.synchronizedMap也是类似思路锁粒度太粗性能不行。ConcurrentHashMap才是正解。JDK 8的ConcurrentHashMap放弃了JDK 7的分段锁设计改用CAS synchronized来保证并发安全。核心逻辑是数据存储采用数组加链表链表过长会转红黑树的结构插入时如果对应桶位为空用CAS直接放入不用加锁如果桶位不为空才对头节点加synchronized锁锁粒度细到单个桶。这意味着不同桶位的插入操作可以并行执行并发度大大提升。面试高频题之一是ConcurrentHashMap的扩容机制。JDK 8的扩容支持多线程协助迁移扩容任务被拆分成多个小任务每个线程认领一部分桶位进行迁移迁移完一个桶就把该桶标记为已迁移其他线程遇到标记就跳过。这就把一次耗时的全量迁移分摊到了多个线程上减少了扩容期间的停顿。还有一个真正常见的坑ConcurrentHashMap虽然单个操作线程安全但复合操作不是原子的。比如经典的“先检查后插入”——if (!map.containsKey(key)) { map.put(key, value); }两个线程可能同时判断都不存在然后都执行put。正确做法是用putIfAbsent或者compute方法。我在代码评审里经常看到这种误用大家一定要留意。3.2 ThreadPoolExecutor七个参数如何决定线程池行为线程池是并发编程里最实用的工具没有之一。但它也是八股重灾区很多人背了“核心线程数、最大线程数、队列长度、拒绝策略”但真到线上调优就抓瞎。这里把ThreadPoolExecutor的七个参数拆开说清楚再附上我自己的调参思路。七个参数分别是corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime非核心线程空闲存活时间、unit时间单位、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。线程池的工作流程可以简单概括为提交任务时先看当前线程数是否小于核心线程数是则直接创建新线程执行任务。否则把任务放入任务队列。如果队列满了再看线程数是否小于最大线程数是则创建非核心线程执行任务。如果线程数已经达到最大线程数触发拒绝策略。这个顺序非常关键线程池是优先填队列而不是优先创建新线程。很多人误以为核心线程满了就先扩张线程其实不是先排队。只有队列满了才扩张线程。核心参数的设置思路我的习惯是CPU密集型任务核心线程数设为CPU核心数1。因为这类任务基本不等待线程多了反而增加上下文切换开销。I/O密集型任务核心线程数可以设大一些比如CPU核心数×2或者按公式“CPU核心数×(1等待时间/计算时间)”估算。因为I/O操作会阻塞线程空闲出来的CPU可以给其他线程使用。队列长度有界队列是必须的无界队列如LinkedBlockingQueue不设容量会导致任务无限堆积极端情况下内存被耗尽。但队列也不能太长否则任务排队时间太久响应时间不可控。拒绝策略默认的AbortPolicy是直接抛异常容易让业务方感知到问题但线上我更常看到用CallerRunsPolicy让提交任务的线程自己执行任务这样既不会丢任务也能通过“提交者被拖慢”来反向压节奏。一个容易被忽视的参数是threadFactory。很多线上事故是因为线程池里的线程没有统一的命名规范排查问题时jstack一看全是pool-1-thread-1、pool-2-thread-1根本分不清是哪个业务模块的线程。建议创建线程池时一定用自定义ThreadFactory带上业务语义比如“order-async-pool-1”排查问题能省一半时间。3.3 并发工具类CountDownLatch、Semaphore、CyclicBarrier怎么选JUC包里有三个经常被拿出来对比的并发工具类它们的名字很相似但语义完全不同。CountDownLatch是倒计时器创建时指定一个计数N主线程调用await()等待其他线程完成任务后调用countDown()把计数减一计数到0时主线程被唤醒。适合“多个任务都完成了我再继续”的场景。比如批量查询用户数据拆成多个并发任务全部查询完成后汇总结果。CyclicBarrier是循环栅栏N个线程互相等待每个线程到达屏障点后都会阻塞直到所有N个线程都到达才一起放行。它和CountDownLatch最核心的区别是CountDownLatch是一次性的计数归零就结束了CyclicBarrier可以重置循环使用。语义上CountDownLatch是“我等你完成”CyclicBarrier是“人齐了再一起走”。Semaphore是信号量维护N个许可线程执行前先acquire()获取许可获取不到就阻塞执行完后release()归还许可。它本质是流量控制工具限制同时访问某个资源的线程数。比如数据库连接池最多只有10个连接可以用Semaphore限制同时查询的线程数避免连接被打满。选型建议只等一次结果用CountDownLatch多个线程互相配合、分阶段进行用CyclicBarrier限制并发访问量用Semaphore。这三个类在实际代码中用到的地方非常多性能不是主要考量理解语义、别用错才是关键。4. 并发问题排查与面试高频题实录4.1 死锁、活锁、饥饿经典并发问题的识别与预防并发编程写得多了一定会遇到死锁。死锁的经典定义是两个或多个线程互相持有对方需要的锁谁也不让谁导致全部阻塞。最典型的是“哲学家就餐”问题线程A持有锁1等待锁2线程B持有锁2等待锁1两边都等不到对方释放。死锁的四个必要条件互斥条件、持有并等待条件、不可剥夺条件、循环等待条件。预防思路也对应着打破这些条件但在业务代码里最实用的方法有两个一是统一加锁顺序所有线程都按同一个顺序获取锁就不会形成循环等待二是用tryLock并设置超时时间拿不到锁就放弃而不是无限期地等。活锁比较隐蔽线程并没有阻塞而是在不断重复同一个操作但始终没有进展。比如两个线程同时在互相谦让线程A检测到资源被占用就释放自己占的资源线程B也这么做结果资源一直被释放、一直没人用。活锁比死锁更难排查因为从监控上看线程都是RUNNABLE状态但任务进度为零。预防思路是加入随机退避让两个线程不要每次都同步重试。饥饿是线程一直得不到需要的资源。在锁场景里非公平锁可能导致某些线程长时间抢不到锁。ReentrantLock的公平锁就是为应对饥饿设计的它保证先等待的线程先获得锁代价是吞吐量会下降一些。实际业务里如果某个低优先级的任务长期无法执行可以检查是不是锁的公平性设置有问题。4.2 排查并发问题的实用工具箱jstack、jconsole、Arthas并发问题最麻烦的不是解决而是定位。这里分享几个我实际工作中用得很顺手的排查工具。首先是jstack它是JDK自带的线程快照工具。线上出现线程卡死、CPU飙升时先执行jstack pid拿到线程快照然后看线程状态。如果是BLOCKED状态快照里会显示它在等哪把锁锁被哪个线程持有。死锁时jstack甚至会在最底部直接打印“Found one Java-level deadlock”并列出相关线程基本是一抓一个准。jconsole是可视化监控工具可以看到线程状态的变化曲线、线程栈信息、内存使用情况适合观察并发问题的“动态过程”。不过它对线上环境侵入性较强一般本地或压测时用得多。Arthas阿尔萨斯是阿里巴巴开源的Java诊断工具强烈推荐。它可以在线查看方法调用耗时、方法入参返回值、线程栈甚至可以直接反编译线上代码。排查并发问题最常用的是thread命令thread -b可以直接找出当前阻塞其他线程的线程thread threadId可以查看指定线程的堆栈thread --stateWAITING可以按状态过滤线程。有一次线上线程池被打满我通过Arthas的thread命令快速定位到是某个第三方接口的读操作耗时过长线程全部阻塞在socketRead几分钟就找到根因了。4.3 面试高频题我从JD和面经里总结的并发考察点结合近几年的大厂面试题我把并发编程的高频考察点整理成几个方向每个方向除了背答案更建议从源码层面理解线程池相关线程池的核心参数有哪些工作流程是什么如何设置线程数拒绝策略有哪些以及什么时候选哪种这个问题几乎必问最好能把参数和流程串成一条线讲再结合你的业务场景举例。锁相关synchronized和ReentrantLock的区别synchronized锁升级过程 volatile能否保证原子性AQS的原理这几个问题通常是连环问回答时要主动往深处带。并发容器相关ConcurrentHashMap在JDK 8是如何保证线程安全的put的完整流程扩容机制这几个问题建议直接读源码读一遍源码带来的理解深度比背十篇八股文都有用。原子类相关AtomicInteger的实现原理CAS的三大问题ABA问题、自旋开销、只能保证单个变量原子性是什么ABA问题怎么解决AtomicStampedReference这里很多人只记住概念建议实际写一段代码看重试自旋的循环。我自己面试别人的时候最反感的是只背结论、问一句“为什么”就卡壳的答案。比如说到“线程池满了先放队列”我会追一句“为什么不是先扩张线程”——只有真正理解“队列可以缓冲请求、线程创建和销毁有开销”的逻辑才算真的掌握了。4.4 一个值得关注的新方向虚拟线程与并发编程的未来写到这里必须提一嘴虚拟线程。JDK 21正式引入了虚拟线程Virtual Threads这是Java并发模型的一次重要演进。虚拟线程是JVM管理的轻量级线程创建成本极低可以轻松创建成千上万甚至几十万个虚拟线程大大简化了高并发场景下的编程模型。以前一个高并发服务要处理大量IO请求最简单粗暴的方式是用线程池控制并发但线程池大小总是一个需要反复调优的参数。有了虚拟线程每个请求可以直接对应一个虚拟线程阻塞其实不占用平台线程载体线程底层会自动处理挂起和切换代码写起来就像同步编程一样直观。会不会替代线程池短期内不会。虚拟线程适合IO密集场景但CPU密集型的并行计算仍然需要合理数量的平台线程。线程池在资源控制、限流方面的作用依然不可替代。但作为Java工程师2026年这个时间点开始学习和使用虚拟线程是值得的。面试里如果能主动聊到虚拟线程和平台线程的区别通常会给面试官留下不错的印象。5. 从笔记到实战一份可以照着用的并发编程学习路径最后结合这份“全优笔记”的内容给出一条我实际验证过的学习路径按这个节奏走大概六到八周可以建立起比较完整的并发编程知识体系。第一周打地基把Java内存模型、三大特性、Happens-Before规则完全搞懂配合《Java并发编程的艺术》前几章和几篇深入解析JMM的文章这个阶段不要求全重点是建立“并发问题到底怎么产生”的直觉。第二周转到线程与锁把线程状态切换图和synchronized锁升级过程理解透彻动手写几个多线程程序用jstack观察不同状态下的线程栈让抽象的锁概念可视化。第三周到第四周啃JUC源码重点读AQS、ReentrantLock、ConcurrentHashMap、ThreadPoolExecutor四个类的源码。不用一行行精读抓主线AQS的state和等待队列是怎么协作的ConcurrentHashMap的桶位是怎么用CAS和synchronized保护的线程池提交任务的完整链路是怎么走的。第五周刷并发工具类的使用场景CountDownLatch、CyclicBarrier、Semaphore、CompletableFuture每种都写个小demo模拟一个真实场景比如并发请求汇总、限流、批量处理。第六周以后回到实战验证找一个线上服务观察它的线程池指标看看有没有优化空间写一个模拟死锁的demo用jstack和Arthas把它揪出来。这个阶段最有价值因为知识只有经过“遇到问题-定位-解决”的闭环才会真正变成你自己的。我个人在实际操作中最大的体会是并发编程没有一个知识点是孤立的。JMM解释锁为什么存在锁的实现又引出AQSAQS派生了各种并发工具并发工具又服务于线程池和业务场景——它们是互相咬合的一整张网。单独背任何一环都容易忘只有把链条串起来才能在面试和实战中真正游刃有余。这份“全优笔记”的价值也正在于此它不只是知识点的堆叠而是在帮你把这根链条完整地搭起来。
返回列表