ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java并发编程高效学习路线:从JMM到线程池实战全攻略

Java并发编程高效学习路线:从JMM到线程池实战全攻略 并发知识是Java面试绕不开的大山也是生产环境里最能体现一个程序员功底的地方。我做Java开发快八年带过不少新人也面试过上百个候选人发现大家学并发普遍存在同一个问题学过就忘、懂原理但不会用、面试背得滚瓜烂熟一上线就抓瞎。这篇文章想聊的就是Java并发知识到底怎么学才算高效把学习路径、核心原理、实战场景、排查方法串成一条线。不管你是刚入行想补短板还是准备跳槽想系统复习一遍下面这套打法应该能帮你少走很多弯路。1. 先想清楚并发为什么难学主线到底是什么1.1 并发知识散成一地但背后有一条核心主线很多人学并发的第一反应是买一本《Java并发编程实战》从头啃或者打开视频课一节一节看。结果往往是看的时候觉得都懂合上书脑子里只剩几个名词Synchronized、Lock、线程池、CAS相互之间什么关系完全说不清。我总结下来并发知识之所以难不是因为它深而是因为它散。散在三个层面底层原理散比如JMM、CPU缓存、指令重排API工具散比如Lock、Semaphore、CountDownLatch、ConcurrentHashMap业务场景散比如秒杀、分布式锁、数据一致性。三个层面各有各的术语和用法没有一条主线串联的话学十个知识点等于学了十个碎片。我的经验是把整条知识线串成一句话并发本质上是在解决“多个线程同时访问共享资源时如何保证正确性同时尽量提升性能”。围绕这句话拆出三个问题就清晰了——共享资源怎么保护线程之间怎么协作流量大了怎么扛。保护靠的是锁和原子类协作靠的是队列和同步工具扛流量靠的是线程池和并发容器。有了这条主线你学每一个知识点时都知道它属于哪一类能解决什么问题记忆和理解的效率会提升好几倍。1.2 按三个层次规划学习路线从底层到上层逐步推进基于这条主线我给学习和复习划分了三个层次也可以理解成三阶段递进。第一阶段是地基层重点啃JMM、可见性、有序性、原子性、Synchronized、Volatile、CAS。这个阶段解决的核心问题只有一个为什么多个线程同时读写同一个变量会出问题语言层面给了哪些机制去解决。这一层不要求你背源码但要求你能画出线程、工作内存、主内存的关系图能说清楚Synchronized锁升级的四个状态。第二阶段是工具层重点啃Lock体系、AQS、并发容器、线程池。这个阶段解决的核心问题是在复杂场景下基础机制用起来不方便官方提供了哪些更强大的工具。比如Synchronized不能响应中断那就有了ReentrantLockHashMap线程不安全那就有了ConcurrentHashMap线程创建销毁代价高那就有了线程池。第三阶段是场景层重点啃高并发业务里的典型问题库存扣减怎么防超卖、接口超时怎么排查、数据库并发锁和Redis分布式锁怎么选、线程池参数在一个真实业务里怎么定。这一层是最终目的也是面试官最常深挖的地方。很多人的问题是跳过第一层直接学第三层背了一堆秒杀方案但连Synchronized锁在对象头上都不清楚结果面试被一问“为什么加锁能保证可见性”就露馅。反过来也有人永远停在第一层天天研究偏向锁撤销实际业务里根本用不上。三个阶段互相补充不能偏废。2. 底层地基JMM、Synchronized、Volatile与CAS这样啃2.1 先把JMM和可见性问题彻底搞透JMMJava内存模型是所有并发问题的逻辑起点。它定义了一张抽象的内存模型每个线程有自己的工作内存所有变量存放在主内存线程对变量的所有操作必须在工作内存中进行不能直接读写主内存。这句话听起来简单背后却引出了最经典的坑两个线程同时修改一个共享变量A线程改完之后B线程可能长时间看不到最新值。我常用一个类比帮助理解——主内存是公司总部的台账本线程工作内存是员工随身带的便签本。员工改数据先改自己便签本什么时候同步回台账本、什么时候从台账本刷新到便签本规则不明确的话另一个员工拿着旧数据做判断就会出错。Synchronized和Volatile就是JMM给你的两条同步规则。Volatile修饰的变量读写时强制刷新主内存保证可见性同时禁止指令重排序。但它有两个前提必须记住单个Volatile变量的读写是原子的复合操作不是。经典场景是计数器自增如果只把变量声明成Volatile两个线程同时执行操作照样会丢数据因为这个操作本质上是读、加、写三步不是原子操作。这个点面试被问烂了但回答时能主动说“Volatile只解决可见性和有序性不解决原子性”就已经超过一半的人了。2.2 Synchronized锁升级过程要理解到对象头Synchronized在Java 6之后做了大量优化现在已经不是过去那种“重量级锁包打天下”的形态了它会根据竞争激烈程度在四种状态之间升级无锁、偏向锁、轻量级锁、重量级锁。理解这个升级过程关键要看懂对象头里的Mark Word。简单说Mark Word里面存着锁的状态信息和持有者信息不同状态下记录的内容不一样。偏向锁场景下记录偏向的线程ID轻量级锁记录指向栈中锁记录的指针重量级锁记录指向Monitor对象的指针。当只有一个线程反复进入同步块时偏向锁几乎零开销一旦有另一个线程来竞争偏向锁撤销并升级为轻量级锁如果竞争加剧锁膨胀为重量级锁未抢到锁的线程进入操作系统内核态阻塞。为什么建议你要看到这一层两个原因。第一这是面试高频题面试官喜欢问“Synchronized在JDK 1.6之后做了什么优化”能讲清楚锁升级基本就过关了。第二生产环境下锁性能调优必须理解这个机制比如你在低竞争场景下锁很流畅突然流量上来接口变慢就要意识到锁正在膨胀线程在内核态阻塞切换而不是盲目怀疑数据库。一个实操心得写并发代码千万不要在一开始就到处加Synchronized先分析共享资源的竞争程度再决定用什么锁。单线程写完了再并发低竞争用乐观锁或自旋高竞争才需要考虑分段、降锁粒度甚至无锁方案。2.3 CAS与原子类自旋锁的适用边界CASCompare And Swap做的事情一句话比较当前内存值是否等于预期值相等才更新不相等就重试。它的好处是避免了线程阻塞和上下文切换坏处也明显——在高竞争场景下会长时间自旋空转白白消耗CPU同时只能保证单个变量的原子性还会遇到经典的ABA问题。针对ABA问题Java提供了带版本号的AtomicStampedReference原理是在CAS比较值的同时比较版本号。这属于面试加分项实际业务里很少遇到但能说出来代表你是懂底层原理的人。原子类这块我建议把Java并发包里那十几个类分个类再学习原子基本类型AtomicInteger、AtomicLong、原子引用AtomicReference、原子数组、原子字段更新器、甚至还有带累加器的LongAdder。其中LongAdder是必须要了解的它内部把热点值拆成多个槽位多个线程各写各的槽位最后汇总并发越高优势越明显。如果你在业务里用AtomicLong做计数器压测发现瓶颈换成LongAdder往往立竿见影。3. 工具层Lock体系、并发容器与线程池的实战要点3.1 ReentrantLock与AQS理解到什么程度最合适很多人一看到AQS源码就头大反复看又反复忘。我的建议很明确AQS不需要背源码但必须理解它的骨架逻辑。AQS内部维护一个volatile int类型的state状态值以及一个双向FIFO等待队列。获取锁就是尝试修改state修改失败就包装成Node节点放进队列尾部等待释放锁就是改回state然后唤醒队首节点。基于这个骨架你就能理解为什么它叫“抽象队列同步器”ReentrantLock用它实现独占锁CountDownLatch用它实现共享锁Semaphore也复用它实现信号量。模板方法模式在这里体现得淋漓尽致子类只需要决定“什么时候允许修改state”其余的排队、唤醒、中断处理全是AQS写好的一套框架。这个理解程度对绝大多数开发岗和多数的架构岗都够用了。真正写框架的人才会去研究AQS的源码级细节。如果面试被问“你说说AQS的原理”按“state状态加FIFO队列加两种模式”的方式作答比背一堆方法名强得多。我曾经面试过一个候选人一上来从头到尾背AQS源码背到第三十行我开始问“为什么唤醒的是队首节点的后继节点”他答不上来。背思路和背代码完全是两回事。3.2 并发容器选型ConcurrentHashMap和BlockingQueue的使用判断并发容器这块我见过最多的问题就是选型混乱。HashMap不安全对那就全部换成ConcurrentHashMap这是最常见但也最不走脑子的操作。选型要看你面对的是什么并发场景。ConcurrentHashMap在Java 8之后采用的是CAS加Synchronized加分段式结构锁的粒度是单个桶。它最复杂的部分是扩容阶段的辅助迁移读操作可以并发进行写操作需要争抢桶的锁。所以你判断它是否适合你的场景时核心指标是读写比例和冲突概率。读多写少它性能很好写多且大量写同一个桶瓶颈一样会出现。CopyOnWriteArrayList适合读多写极少的场景比如配置项、黑白名单这类几乎不改的列表。它的写操作会复制整个底层数组写频繁的话内存和GC压力都扛不住。BlockingQueue则是生产者消费者模型的核心ArrayBlockingQueue有界、LinkedBlockingQueue可选有界线上强烈建议使用有界队列并且设置好拒绝策略。分享一个真实教训我们之前有个服务用无界队列承接上游推送的数据流量稍微一抖动队列里积压了上百万条任务内存直接飙到接近上限最后服务被频繁GC拖垮。换成有界队列加拒绝策略之后在系统扛不住时选择快速失败或者降级反而保住了核心流程没有彻底瘫掉。有界、有界、有界这是线程池和消息队列都要记住的原则。3.3 线程池参数不能光背要看懂它是在给系统量带宽ThreadPoolExecutor里最关键的其实是三个参数核心线程数、最大线程数、任务队列的容量。周围同事经常争论线程池大小到底怎么设置网上也有各种公式但你要先理解线程池工作的完整流程提交任务时核心线程没满就先创建核心线程执行都忙了再往队列里丢队列满了才创建非核心线程非核心线程也满了触发拒绝策略。这个流程是一个典型的流量缓冲模型线程池不只是“池化复用线程”它本质上是一个“请求缓冲加执行能力控制”的阀门。核心线程数是常规流量下的常备力量队列是应对突发流量的缓冲区最大线程数是缓冲还不够时的紧急动员力量拒绝策略是连动员也撑不住的兜底方案。理解了这层你就知道为什么Executors工具类里面那几个快捷方法在生产环境不推荐用newFixedThreadPool用的是无界队列newCachedThreadPool最大线程数是Integer.MAX_VALUE且使用SynchronousQueue这两者在流量高峰时都可能带来灾难。至于线程数怎么设我一般不硬套公式而是看任务类型。CPU密集型任务线程数约等于CPU核心数加一IO密集型任务线程数可以适当放大参考公式是CPU核心数乘1加等待耗时除以计算耗时实际再根据压测微调。这个值不是一成不变的接入了监控之后观察线程平均活跃时间再调整比任何公式都靠谱。4. 连接真实场景高并发业务、监控排查和面试答题4.1 数据库并发扣减从悲观锁到乐观锁再到分布式锁高并发业务里最典型的就是库存扣减比如秒杀。很多刚学并发的人喜欢问“我直接在MySQL里UPDATE stock SET count count - 1 WHERE id ?不就行了吗”单行更新在InnoDB默认是可重复读隔离级别下确实会命中行锁确实不会超卖。但问题是这条SQL写下去所有请求串行化数据库成了最大的瓶颈压测QPS通常也就几百到一千多。更常用的方案是乐观锁UPDATE stock SET count count - 1 WHERE id ? AND version ?通过版本号CAS来控制并发。这个方案适合冲突概率不高的场景。冲突高怎么办先做前置过滤把大部分无效请求挡在业务层比如用Redis预扣减库存再用限流削弱流量只有真正抢到资格的那批请求才去操作数据库。这里要理解一个关键点并发控制不是孤立用某一种锁而是一个链路设计。网关层要限流业务层要防重放缓存层要预扣库存数据库层做最终一致性。面试里常问的“如何设计一个秒杀系统”考察的正是你有没有这种分层思维而不是你背过哪种锁的API。我常和团队里的人说并发方案的成功不在于某个环节设计得有多精妙而在于每一层都把压力消化在自己应该消化的那一层。数据库锁里还有一个高频词是间隙锁。排查线上死锁时如果发现同一个SQL在不同的并发条件下互等多半就是间隙锁纠缠在一起。开发时尽量让更新条件走唯一索引缩小锁范围减少死锁概率。4.2 老出现连接数超限怎么一步步定位线程与连接问题热搜里常年能看到“Nginx最大连接数老是超”“高并发下接口超时”这类词我见过太多人一上来就乱调参数Nginx的worker_connections翻倍、线程池的大小翻倍结果该崩还是崩。我要给你分享一下比较稳妥的排查顺序。第一步看系统整体指标CPU、内存、磁盘、网络先把资源瓶颈定位出来。第二步用jstack抓线程快照看线程是RUNNABLE还是WAITING还是BLOCKED。第三步结合慢查询日志和数据库连接池指标判断是不是SQL拖住了事务。第四步再看Nginx或网关的连接数和超时配置。很多所谓连接数超限真实原因不是Nginx连接数不够而是后端响应太慢占住了连接不释放前排网关自然越积越多。这里有个实操经验线上排查并发问题千万别凭感觉猜。先用监控确认现象再用线程转储确认卡点最后才动手改配置。改的时候一次只改一个变量对比压测数据再决定下一步。我见过最夸张的一次线上事故就是反向代理超时时间设置太短结果接口在数据库锁等待超过1.5秒就被网关断掉客户端反复重试把数据库拖垮整个应用雪崩。4.3 面试里的并发题为什么不能只会背结论Java面试题常问并发翻来覆去就是那几道Synchronized和ReentrantLock的区别、Volatile的作用、ConcurrentHashMap为什么线程安全、线程池参数怎么设、如何防止超卖。这些题网上都有标准答案但面试官真正想听的是你的思考路径。比如问“ConcurrentHashMap为什么线程安全”低水平的回答是“因为用了分段锁”中等水平的回答是“JDK 8之后锁粒度细化到单个桶CAS加Synchronized”高水平的回答会提到读操作怎么保证可见性、size()统计时会不会不准确、扩容时其他线程怎么协助迁移。这些细节都在源码注释里写明过但你只看结论不看场景是讲不出来的。我的一个建议是准备面试时给自己列一个“场景题库”每个知识块对应一个真实场景。比如Synchronized对应一个简单计数器Volatile对应开关标志位ReentrantLock对应需要公平调度或响应中断的资源访问ConcurrentHashMap对应热点缓存线程池对应异步任务削峰。这样面试官无论从哪个角度切入你都能快速把题目映射到场景上答题会有立体感而不是机械地吐名词。5. 高效学习的工具、资料与常见问题速查5.1 学会看线程转储用调试验证你的并发理解学并发光看不练效果很差动手实验是我最推荐的方式。第一步先学会制造问题写一个两个线程同时自增十万次的程序看看真实计数是不是小于二十万写一段会产生死锁的代码用jstack抓到线程转储里出现“deadlock”字样。这一步会让你对理论有一个直观的认知。第二步学会使用VisualVM和JMC观察线程运行状态。打开VisualVM连接本地进程能看到线程数、线程状态分布、CPU使用率配合JMC的事件分析能实打实地理解阻塞和等待的区别。做这些实验的时候心里时刻想着前面说的主线我的线程在等锁、等队列、还是在填报数据每一条线程状态背后都有原因。我经常在带新人时让他们先复现一次死锁不提前告诉他们答案。大多数人第一次看到两个线程互相持有对方需要的锁、各自WAITING那种理解和震撼比你讲十次理论都管用。想自己学着抓死锁两个线程或者多个线程按照相反的顺序去获取两把锁就可以复现然后在命令行执行jps -l jstack pid在输出里搜索“deadlock”关键字就能定位到是哪个线程持有哪把锁、正在等待哪把锁。掌握这个操作后你在生产环境遇到死锁时就不会手足无措。5.2 学习资料怎么组合效率最高并发方面的经典书不少但我很少建议直接从头到尾啃。我给你的组合大概是这样的以《Java并发编程的艺术》为主线快速过一遍基础概念这本书相对薄讲JMM、Synchronized和Volatile的思路很清晰遇到AQS和线程池细节再去翻《Java并发编程实战》对应章节期间遇到源码级问题就直接看OpenJDK在线源码。视频课也有合适的但不要贪多。最好是选一套体系完整的课跟着写代码而不是收藏一堆免费的片段。比较实用的做法是给自己定一个为期两周到三周的小目标第一周完成JMM和锁基础第二周完成并发容器和线程池第三周找一个真实的业务场景比如模拟秒杀把Redis、数据库、线程池串起来做一个小项目和压测。另外要做题。刷题不是为了背题而是测试知识盲区。你可以在网上搜各种Java面试题把自己答不上的点记录下来回到书和源码里找到对应章节用自己的话重新解释一遍。这个过程比单纯看书效率高得多。5.3 并发场景常见问题速查表在生产环境摸爬滚打几年我把并发相关问题整理成了一个小速查表团队新人遇到问题时我会直接扔给他对照。现象可能原因排查手段程序明明启动了但不往下执行线程全部阻塞可能在等锁或等待通知jstack看线程状态找WAITING或BLOCKEDi多线程结果偏小复合操作非原子简单加锁或用AtomicInteger代码审查加压测复现ArrayList在并发遍历时抛ConcurrentModificationException迭代过程中被其它线程修改用CopyOnWriteArrayList或加锁线程池任务积压内存持续上涨队列无界且生产速度大于消费速度换有界队列并设置拒绝策略两个线程互相等待进入死锁锁获取顺序不一致jstack搜索deadlock统一加锁顺序接口经常偶发超时日志里显示连接池获取超时数据库连接池被慢SQL占满查慢查询、加大连接池或优化SQL高并发下接口CPU不高但响应很慢可能在频繁上下文切换或锁自旋用perf或JMC分析线程切换与锁竞争这张表里我觉得最容易被忽略的是“接口偶发超时”。很多人第一反应是网络问题但实际上一大半是连接池满或线程池拒绝策略触发一定要把监控指标接起来看。最后一个避坑经验生产环境不要轻易调高各种超时时间。以前总觉得超时时间设长一点更稳定其实高并发场景下超时设置过长会让请求长时间占住线程和连接造成资源耗尽雪崩就是这么来的。服务之间调用一定要设合理的超时并配合快速失败、熔断降级这才是并发系统里更重要的“保护”。6. 一些额外的心得聊了这么多最后分享一点我自己的体会。我刚学并发的时候犯过一个典型错误花大量时间研究LockSupport和AQS的每一行源码觉得自己看懂了很厉害但写业务代码时依然不知道该怎么用。后来转变思路把并发当成一门“基于场景的工具学”每学一个知识点就问自己三个问题它解决什么问题什么场景下该用用了之后会有什么代价带着这三个问题去学习和复盘进步速度反而快了很多。如果你现在还在并发知识的迷宫里打转我建议你先放下大部头的书用一天时间把自己“知道”的并发名词写成一张图标出它们的层级和关系。你会发现你其实懂了很多只是缺少那条把它们串起来的线。顺着这条线从JMM到锁到容器到线程池再到真实案例一步一步推进最多一个月你会感觉到一种“忽然通了”的畅快。到那时面试题对你来说不再是背诵内容生产环境的问题也会慢慢变得有迹可循。
返回列表