拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Java并发编程全解析:从三性到锁、线程池与面试实战

最近在面试别人时碰到一个挺典型的场景简历上写着精通并发编程结果聊到volatile和synchronized的区别答了一个修饰变量一个修饰方法就卡住了。再追问一句那synchronized在JDK 1.6之后到底优化了什么就开始背八股文了。这个现象其实不怪候选人市面上讲并发的资料要么太散要么直接甩源码很少有人能把整条线串起来讲清楚。这篇文章就是冲着把并发编程从入门到面试这一条线彻底理顺来的。我会从并发问题的根源开始讲一路走到锁、AQS、线程池、并发容器这些面试重点每个关键点都结合真实场景说明白为什么这么设计和实际代码里怎么用。不管是准备面试的Java开发还是工作中要写并发代码、排查并发问题的工程师这篇文章都能当一份反复翻阅的手册。1. 面试官问并发到底想听什么很多人在面试前背了几十道并发八股文synchronized和ReentrantLock的区别、volatile和AtomicInteger的区别、线程池的七大参数……但一被追问为什么有这些问题这些机制到底解决了什么根因就露馅了。并发编程看似知识点零散其实所有问题都源于同一个根源多个线程同时访问共享资源时如何保证结果正确。1.1 三性问题不是背概念是要讲出根源并发编程的三大核心问题——可见性、原子性、有序性几乎每次面试都会被翻出来。但如果你只背可见性是线程A改了值线程B看不见这种结论面试官只要追问一句为什么会看不见就麻烦了。根源要追溯到计算机的存储体系。CPU的计算速度和内存读写速度差距太大所以中间加了好几层缓存。现代多核CPU架构下每个核心都有自己的L1、L2缓存共享L3缓存和主内存。线程A在核心1上修改了一个变量的值修改首先发生在核心1的缓存里如果不写回主内存线程B在核心2上读到的就是旧值——这就是可见性问题的硬件根源。原子性问题则来自线程的上下文切换。Java里count看起来是一条语句编译后却是读取 → 加1 → 写回三步操作。线程可能在加1之后、写回之前被操作系统挂起让出CPU另一个线程读取到旧值最终导致结果丢失更新。有序性的根源是编译器和CPU为了提升性能做的指令重排。代码写的顺序不一定是实际执行的顺序只要重排前后单线程语义一致优化器就敢动手。但在多线程环境下这种看似没问题的重排可能导致另一个线程观察到违背直觉的执行顺序。这三个问题不是孤立的它们经常叠加爆发。面试中如果能从硬件到JVM把这层根源讲清楚就已经超过80%的候选人了。1.2 JMM与happens-before判断并发安全性的标尺JMMJava内存模型是回答并发问题绕不开的基石。它规定了哪些情况下一个线程的写操作对另一个线程可见核心就是happens-before原则。这套原则定义了Java并发编程的因果关系如果操作A happens-before操作B那么A的执行结果对B可见且A的执行顺序在B之前。happens-before规则里有几条面试高频的程序顺序规则一个线程内书写在前面的代码 happens-before 后面的代码。锁规则解锁操作 happens-before 后续对同一把锁的加锁操作。volatile规则对volatile变量的写操作 happens-before 后续对这个变量的读操作。传递性A happens-before BB happens-before C则A happens-before C。实际排查并发Bug时拿happens-before去套就能判断你的代码到底有没有保证可见性。举一个实际例子网上广为流传的双重检查锁单例模式public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // 问题点 } } } return instance; } }如果instance不加volatilenew Singleton()这一步并不是原子的。JVM会先分配内存空间、初始化对象、把引用指向内存。指令重排后可能出现引用指向了内存但对象还没初始化完成的中间状态。线程A拿到半初始化对象线程B第一次检查发现instance不为null直接返回用的就是坏对象。加volatile的深层作用就是禁止这步重排保证happens-before关系成立。很多居然还在怀疑volatile在这里的作用甚至有人会把instance的类型改成普通静态变量然后信誓旦旦说大部分情况没问题。对大部分情况确实没问题但并发Bug的可怕之处就在于大部分情况下没问题偶然出一次问题线上半夜就炸了。2. synchronized的锁升级从偏向锁到重量级的完整演化synchronized是Java并发里最基础也最常被问到的关键字。面试一般有两个维度一是问用法二是问原理。但深度一点的面试官会直接问JDK 1.6之后synchronized做了哪些优化这背后是一场关于锁性能的进化史。2.1 对象头与Mark Word锁信息存在哪要理解synchronized的实现机制得先知道锁信息放在哪里。Java对象在内存中的布局分为三块对象头、实例数据、对齐填充。对象头里包含Mark Word和类型指针数组还有数组长度Mark Word里存的就是锁状态的关键信息。Mark Word本身是一个可变长度的数据结构根据锁状态复用存储空间。无锁状态下它存对象哈希码、年龄分代偏向锁状态下存线程ID和偏向时间戳轻量级锁状态下存指向栈中锁记录的指针重量级锁状态下存指向监视器Monitor的指针。这就是为什么面试问synchronized锁的是谁时正确回答是锁的是Java对象头中的Mark Word而Monitor则通过对象的monitor指针关联。2.2 锁升级的完整过程JDK 1.6对synchronized做了重大优化引入了偏向锁、轻量级锁、重量级锁三条路径锁只能升级不能降级。整个升级过程是性能权衡的产物偏向锁无竞争场景下的极致优化。当线程第一次进入同步块时JVM会把对象头中的偏向锁标记置为1并在Mark Word里记录下当前线程的ID。之后这个线程再进入同一个同步块时不需要做任何同步操作直接执行即可。好处是单线程反复加锁的开销几乎为零。轻量级锁一旦出现第二个线程尝试获取偏向锁偏向锁会撤销并升级为轻量级锁。轻量级锁通过CAS自旋来获取锁线程在自己的栈帧中创建锁记录然后把对象头中的Mark Word复制到锁记录里再用CAS把对象头中的Mark Word改为指向自己栈中的锁记录。如果CAS成功说明抢锁成功失败则说明存在竞争。这里有个面试常追问的细节轻量级锁与重量级锁的本质区别。轻量级锁在没有实际竞争时避免了操作系统级线程阻塞和唤醒的开销线程通过自旋忙等。但如果自旋超过一定次数默认可能自适应调整仍然拿不到锁说明竞争激烈就会膨胀成重量级锁由操作系统通过Monitor实现线程的阻塞和唤醒。自旋本身也消耗CPU所以自旋次数太多不如干脆阻塞划算这个阈值由JVM自适应调节。用法上synchronized有三种加锁位置我整理了一个表格方便对照加锁方式锁定的对象适用场景修饰实例方法当前实例对象this保护实例状态修饰静态方法当前类的Class对象保护类级别的共享状态修饰代码块括号里指定的对象减小锁粒度只锁关键代码实际开发中如果锁的是同一个类的静态方法和实例方法它们之间不互斥这个坑很多人踩过。因为一把锁在Class对象上另一把在实例对象上压根不是同一个对象你锁个什么呢2.3 synchronized的三种用法和面试回答要点面试时synchronized常见的三种用法必须烂熟于心修饰实例方法、修饰静态方法、修饰代码块。这三种方式锁定的对象完全不同我在上表里已经做了对比。还有一个面试必考synchronized和ReentrantLock的区别。我会从四个维度回答实现机制synchronized是JVM层面通过Monitor实现的ReentrantLock是JDK基于AQS实现的。功能特性ReentrantLock支持公平锁、支持超时获取锁、支持多个Condition条件队列、支持可中断获取锁synchronized只支持非公平锁在JDK 1.6之后性能和ReentrantLock差距已经不大。锁释放synchronized自动释放ReentrantLock需要手动lock/unlock必须在finally里释放。原理层次synchronized有偏向锁、轻量级锁、重量级锁升级路径ReentrantLock底层是通过CAS和LockSupport实现的。记住这个回答结构远比零散地背ReentrantLock有tryLock强得多。3. volatile为什么能扛起可见性内存屏障与JMM的实战理解volatile可以说是Java并发里最轻量的同步机制但也是被误解最深的关键字。很多新手以为volatile能保证原子性这是个大坑。volatile保证两条可见性和禁止指令重排完全不保证原子性。3.1 内存屏障、缓存一致性协议与不可分割的操作讲volatile的可见性绕不开内存屏障。JVM在volatile变量的读写操作前后插入特定类型的内存屏障防止CPU和编译器对指令进行重排同时确保写操作的修改能立刻被其他线程看到。但真正的底层功臣是CPU的缓存一致性协议比如MESI协议。当线程A修改一个volatile变量时JVM发出一个lock前缀指令这个指令会让线程A所在核心的缓存行失效或写回主内存同时其他核心中缓存了该变量的缓存行会被标记为无效。其他线程再次读取时发现自己缓存行失效就强制从主内存重新加载。这个过程就是一个线程写其他线程立即可见的硬件基础。顺带说一句volatile保证不了原子性这一点经常成为面试题的切入点。假设多个线程同时执行volatile int count; count即便是volatile变量count依然不是原子操作。两个线程可能同时读到相同的旧值各自加1后写回最终只增加了1。要解决这个场景应该用AtomicInteger或者synchronized。3.2 从JMM层面理解volatile的happens-before前面提过volatile的happens-before规则对volatile变量的写操作 happens-before 后续对这个变量的读操作。这条规则的含义是线程A在写volatile变量之前的所有操作对于线程B在读取该volatile变量之后的操作都是可见的。这意味着volatile不仅仅保证变量本身的可见性还充当了一个内存栅栏把之前的操作推给其他线程。这就是为什么双重检查锁单例中instance加volatile之后不仅变量本身不会被重排instance的赋值操作之前的所有操作比如对象初始化的字段赋值也不会被重排到volatile写之后。实际工作中有一个经验volatile适用于一个线程写、多个线程读的场景比如状态标志位。我曾经在一个实时数据采集系统里用volatile布尔变量做服务优雅关闭的标记public class DataCollector { private volatile boolean running true; public void stop() { running false; // 主线程调用 } public void run() { while (running) { // 工作线程读取 // 采集逻辑 } } }这个场景下running是典型的一写多读用volatile足够了不需要引入锁性能几乎无损。判断是否该用volatile就看是否满足状态变量不依赖旧值和一写多读这两个条件。4. CAS的轻盈与AQS的厚重无锁到锁框架的内功修炼并发知识点里CAS和AQS是分不开的一对。CAS是无锁编程的基础AQS则是Java显式锁和大量并发工具的基石。面试到中高级岗位这两个基本是必考。4.1 CAS乐观锁自旋的代价与ABA问题CAS全称Compare And Swap比较并交换本质是一种乐观锁思想。它包含三个操作数内存位置V、预期原值A、新值B。只有V上的当前值等于A时才把V更新为B否则不做任何操作整个比较和交换是原子的由CPU指令保证。Java里的AtomicInteger、AtomicLong等类就是基于CAS实现的。以AtomicInteger.incrementAndGet()为例内部会进入一个自旋循环读取当前值计算新值然后CAS尝试更新失败就重试直到成功为止。没有线程阻塞所以并发不高场景下性能远优于synchronized。CAS有两个经典考点ABA问题和自旋开销。ABA问题指线程A读到变量值为X被挂起线程B把变量改成Y又改回X线程A继续执行CAS时发现还是X认为没被修改过就成功了。解决ABA问题可以用带版本号的AtomicStampedReference每次修改带上版本号比较时同时比较值和版本号。自旋开销则是另一个角度。CAS失败后会一直自旋重试如果竞争激烈大量线程都在自旋CPU占用率会飙升。高并发下的ConcurrentHashMap早期版本也是用分段锁而不是纯CAS就是在权衡这个开销。所以无锁一定比有锁快是个伪命题低并发下无锁有优势高并发激烈竞争下有锁反而更稳定。4.2 AQS的设计精髓CLH队列、state状态和模板方法AQSAbstractQueuedSynchronizer是JDK中几乎所有显式锁和同步工具的核心ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock全都构建在它之上。理解AQS等于理解了半部JUC。AQS的核心就三个东西volatile int state同步状态。不同子类用法不同ReentrantLock里表示持有锁的次数Semaphore里表示剩余许可数CountDownLatch里表示还需要等待的计数。CLH双向队列保存等待获取锁的线程。线程获取锁失败时会被包装成Node节点加入队列尾部通过LockSupport的park/unpark机制实现阻塞和唤醒。CLH队列没有用自旋等待而是真实地阻塞线程所以它能在竞争激烈时避免CPU空转。模板方法设计AQS定义了tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared等钩子方法由子类去实现具体的同步语义。比如ReentrantLock的公平锁和非公平锁就是在tryAcquire实现里差一个hasQueuedPredecessors()判断。公平锁会先看队列里有没有前驱节点有就排队非公平锁直接尝试CAS抢锁抢不到再排队。下面是一个简化版的ReentrantLock获取锁和非公平锁对比帮助理解这个设计// 非公平锁的tryAcquire final boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { // 锁是空闲状态 if (compareAndSetState(0, acquires)) { // 直接CAS抢锁 setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { // 可重入 int nextc c acquires; if (nextc 0) throw new Error(Maximum lock count exceeded); setState(nextc); return true; } return false; }面试时如果把AQS的三要素说出来再把模板方法和ReentrantLock的非公平锁执行过程串一遍基本就稳了。4.3 可重入、可中断、公平与非公平ReentrantLock的关键特性ReentrantLock是最常用的AQS子类几个关键特性面试也常问可重入同一个线程可以多次获取同一把锁state累加释放时需要释放相同次数。可重入的实现是靠判断当前线程是否是持有锁线程来实现的代码就在上面的tryAcquire里的else if分支。可中断lock.lockInterruptibly()支持在等待锁的过程中响应中断。这个特性在业务里有实际价值比如多个线程争同一把锁某个线程被外部调用interrupt()后可以主动退出等待避免业务卡死。公平与非公平公平锁保证先等待的线程先获取锁非公平锁允许后来者插队。ReentrantLock默认非公平因为非公平锁的吞吐量通常更高刚释放锁的线程再次获取锁时还在同一个CPU核心上缓存热成本低。强一致性的业务场景才需要公平锁。Condition条件队列ReentrantLock可以创建多个Condition实现类似生产者消费者的精确唤醒。相比synchronized的wait/notify只能整体通知Condition可以做到只唤醒读线程或只唤醒写线程。这个特性在实际代码里很实用比如实现一个有界阻塞队列。5. 线程池调参参数背后的计算逻辑和线上案例线程池是Java并发里实战性最强的知识点。面试时可能问你七大参数、四种拒绝策略但更高级的面试官会把问题落在线上系统怎么调参上。泛泛背参数意义不大真正要理解的是每个参数背后的触发时机和计算逻辑。5.1 七个参数与一次任务提交的完整旅程ThreadPoolExecutor构造函数的七个参数参数含义触发逻辑corePoolSize核心线程数线程数小于该值时新任务直接创建线程maximumPoolSize最大线程数线程数大于corePoolSize且队列满了创建新线程直到该上限keepAliveTime非核心线程空闲存活时间超过该时间回收非核心线程TimeUnit存活时间单位配合keepAliveTime使用workQueue阻塞队列核心线程全部忙时任务先进队列threadFactory线程工厂创建线程的工厂可以自定义线程名RejectedExecutionHandler拒绝策略线程满队列满时触发一次新任务提交线程池的判断顺序是这样的第一步判断核心线程数是否满没满就创建核心线程执行满了进第二步任务丢进阻塞队列队列也满了进第三步创建非核心线程执行线程数到了maximumPoolSize还是忙不过来就触发拒绝策略。很多人忽略了一个细节核心线程执行完任务后不会被回收而是阻塞等待下一个任务。所以corePoolSize本质是线程池的保底资源。这里的经验是使用线程池一定要给线程命名。默认线程池创建的线程名是pool-1-thread-1这种线上排查问题时根本分不清哪个线程在干嘛。用自定义ThreadFactory给线程取业务名出问题时查日志一眼就能定位ThreadFactory namedThreadFactory new ThreadFactory() { private final AtomicInteger seq new AtomicInteger(1); Override public Thread newThread(Runnable r) { Thread t new Thread(r); t.setName(order-async-processor- seq.getAndIncrement()); t.setDaemon(false); return t; } };5.2 核心线程数到底怎么算CPU密集型与IO密集型线程池调参最核心的问题是核心线程数和最大线程数设多少这必须结合任务类型才能回答。CPU密集型任务核心线程数设为CPU核心数 1就够了或者CPU核心数 * 2也可以接受。CPU密集型任务一直在消耗CPU线程数超过核心数太多没什么好处反而增加上下文切换开销。IO密集型任务线程大部分时间阻塞在IO上CPU很闲可以多搞些线程来压榨CPU。经验公式是CPU核心数 * (1 IO耗时/CPU耗时)更粗略的经验值是CPU核心数 * 2。如果任务里有很明显的等待比如HTTP调用、数据库查询线程数可以往大了调。这里说一个实际案例。我之前做过一个订单消息推送服务任务是往外调推送接口平均耗时80ms是网络IO而真正CPU计算只有2ms左右比例大约40比1。按公式计算4核机器可以开4 * (1 40) 164个线程。但实际我只配了32个核心线程、64个最大线程因为还要考虑下游推送服务的承受能力以及操作系统上下文切换的开销。调优是一个权衡过程不是硬套公式最终要通过压测来验证。5.3 队列选型、拒绝策略与优雅停机队列选型同样有讲究。LinkedBlockingQueue是无界队列任务不会被拒绝但风险在于队列无限增长可能导致内存溢出SynchronousQueue不存储任务直接交给线程适合不缓存任务、立即执行的场景ArrayBlockingQueue有界队列最推荐队列长度会根据业务承载能力做明确限定。拒绝策略有四种AbortPolicy直接抛异常、CallerRunsPolicy提交任务的线程自己执行、DiscardPolicy丢弃不报错、DiscardOldestPolicy丢弃最老的任务。线上生产环境我几乎不用默认的AbortPolicy因为任务被拒会导致业务失败CallerRunsPolicy更适合提交任务的线程由调用方自行消化压力起到天然限流的作用。还有一个非常容易被忽视的坑线程池的优雅停机。直接调用shutdownNow()会中断正在运行的任务可能导致数据状态不一致。我处理过的方案是先shutdown()阻止新任务提交等待一段时间让在跑任务自然结束超时后再shutdownNow()强制停止同时记录被中断的任务日志后续通过补偿机制重新处理。executor.shutdown(); // 平缓关闭 try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 超时后强制关闭 } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); }这段代码就是线上通用做法。很多时候并发问题不是代码逻辑出错而是资源没清理干净导致的下一次运行时数据错乱。6. ThreadLocal的内存泄漏与并发容器的选型决策ThreadLocal和并发容器都是面试爱问、实战常用的组件。这两个东西看起来简单坑也不少。6.1 ThreadLocal为什么会出现内存泄漏弱引用与强引用链ThreadLocal的使用场景很清晰每个线程一个变量副本互相隔离。最常见的就是SimpleDateFormat它不是线程安全的如果做成static的会被多线程并发调用出问题每次new一个又太浪费。用ThreadLocal包一层每个线程持有自己的实例既安全又复用。ThreadLocal的底层结构是每个Thread内部维护了一个ThreadLocalMapkey是ThreadLocal对象弱引用value是set进去的对象强引用。坑就在这里key是弱引用只要外部没有强引用指向ThreadLocal对象GC时key就会被回收但value还通过一条Thread → ThreadLocalMap → Entry → value的强引用链可达。如果线程一直存活比如线程池里的线程value就永远不会被回收导致内存泄漏。这也是为什么线程池里使用ThreadLocal要格外小心。线程池线程是复用的一个任务往ThreadLocal里塞了值下一个任务可能读到上一个任务留下的脏数据。正确做法是在finally块里主动removeThreadLocalSession sessionHolder new ThreadLocal(); try { sessionHolder.set(session); // 业务逻辑 } finally { sessionHolder.remove(); // 一定要清理 }6.2 ConcurrentHashMap的演进从分段锁到CASsynchronized并发容器里面试最常问的必然是ConcurrentHashMap。它有两个关键版本JDK 1.7的分段锁和JDK 1.8的CASsynchronized。JDK 1.7的ConcurrentHashMap把整个map分成若干个Segment默认16个每个Segment是一把可重入锁。写操作只锁对应的Segment其他Segment不受影响从而把锁竞争分散到16把锁上。锁粒度是Segment。JDK 1.8放弃Segment直接对桶数组中的每个节点Node[]的每个桶位使用CAS和synchronized实现并发控制。首先通过CAS尝试插入头节点失败则对头节点加synchronized锁再处理。锁粒度从Segment细化到单个桶并发能力进一步提升同时synchronized经过锁升级优化后性能已经足够好实现也简洁了很多。面试时问到ConcurrentHashMap最好还能补一个细节size()方法的实现。JDK 1.8不再像1.7那样先不加锁统计两次如果两次一致就直接返回而是维护一个baseCount结合CounterCell数组进行累加并发很高时把计数分散到不同的CounterCell上最终汇总。这样大大减少了统计size时的锁竞争。6.3 CopyOnWriteArrayList、阻塞队列与日常选型除了ConcurrentHashMapJUC包里还有几个容器值得记住面试也可能被问到选型CopyOnWriteArrayList读多写少的场景写操作通过复制一个新数组来完成读写分离。由于写时复制它天然支持弱一致性迭代器不会抛ConcurrentModificationException。代价是每次写操作都要复制整个底层数组内存开销大。LinkedBlockingQueue / ArrayBlockingQueue线程池的workQueue就是这两个。它们都是BlockingQueue实现支持take和put阻塞语义常被用在生产者-消费者模型里。ConcurrentLinkedQueue无界非阻塞队列基于CAS实现适合高并发下对队列长度不敏感的场景。选型逻辑一句话概括读多写少用CopyOnWriteArrayList高频读写共享Map用ConcurrentHashMap生产者消费者用BlockingQueue高吞吐无界队列场景用ConcurrentLinkedQueue。7. 并发排查实战一个真实死锁的定位过程八股文背得再熟线上出了问题不会排查也白搭。这里分享一个我之前遇到过的真实死锁案例完整走一遍排查链路这部分内容是面试官也很想听到你现场怎么处理的加分项。7.1 现象服务接口偶发超时线程池任务堆积当时是一个账务系统对外提供批量转账接口。上线后隔几天就会出现一次接口大量超时进程还在但新请求全卡住。用top看CPU不高用jstack看不到任何异常日志服务就像僵住了一样。这类现象的经典特征就是死锁或线程饥饿。服务没有挂但所有业务线程都阻塞在某个点上导致新任务无法处理。7.2 排查链路jstack加日志定位两把锁的交叉等待排查第一步先抓现场。用jstack pid导出线程快照搜索deadlock关键字。Java的ThreadMXBean会主动检测死锁jstack里如果存在死锁会直接打印出Found one Java-level deadlock的信息。当时确实打印出来了两个线程互相持有对方需要的锁循环等待。顺着线程栈往下看发现A线程持有锁A在等待锁BB线程持有锁B在等待锁A。两把锁来自两个不同的服务组件锁A是数据库行锁锁B是分布式缓存锁。业务代码里一个在扣减账户余额事务内去调用缓存接口另一个在更新缓存后反向查数据库账户正好交叉。定位之后修复方案其实不难调整加锁顺序所有流程都先拿数据库锁再拿缓存锁从代码层面破坏循环等待条件。同时给锁的获取加超时时间避免无限等待。7.3 生产经验并发问题的预防与应急三板斧死锁发生一次影响可能很严重所以预防比排查更重要。我的经验可以总结为三板斧第一锁顺序统一。多个锁嵌套使用的场景所有代码路径必须保持相同的加锁顺序。第二所有锁获取尽量带超时。比如用ReentrantLock的tryLock(timeout)而不是lock()拿不到锁就先放弃别赌对方会释放。第三建立线程池监控与告警。线上服务要监控线程池的任务队列长度、活跃线程数、拒绝次数这些指标可以做成Grafana监控面板。队列长度持续增长说明处理速度跟不上生产速度需要提前扩容或优化不用等系统真的卡死才去排查。排查工具方面jstack是首选但生产环境jstack一次会暂停JVM一小段时间高流量下要谨慎。更好的做法是提前通过JMX或Arthas在线导出线程栈或者用async-profiler做采样对线上影响更小。8. 从入门到面试通关一份可直接执行的复习路线最后这部分不做总结直接给一份能落地的复习和执行清单。如果你正准备Java并发面试按照下面这个顺序去准备比零散刷八股文高效得多。第一阶段基础扫盲约3天掌握三性问题、synchronized三种用法、Java内存模型和happens-before规则。自己能画一遍线程状态转换图并能说清WAITING和BLOCKED的区别面试很喜欢问。第二阶段内核原理约5天深入读透AQS源码重点看ReentrantLock的lock、unlock、公平锁与非公平锁。掌握volatile和CAS的底层原理知道JVM层面的内存屏障作用。这里建议动手写个小Demo用synchronized、ReentrantLock、AtomicLong分别实现一个计数器压测对比性能体感会强很多。第三阶段实战工具约3天线程池七大参数、执行流程、四种拒绝策略亲手封装一个自定义线程池工具类。CompletableFuture的常用API至少要会thenApply、thenAccept、thenCombine、exceptionally。并发容器重点看ConcurrentHashMap知道1.7和1.8的实现差异。第四阶段排查能力约2天学会用jstack、Arthas排查死锁和线程阻塞把上面的排查链路自己模拟一遍。理解ThreadLocal的内存泄漏原因和remove的正确用法。准备过程中有一个重要的心态调整不要试图背下所有源码面试官问源码时他要的是你理解设计意图并能讲清楚关键流程而不是逐行默写。把CLH队列是什么、state干什么、为什么这样设计这些所以然想明白比源码背诵有效得多。另外面试如果被问到你项目里哪里用到了并发编程一定要拿真实案例说事。哪怕是一个最简单的线程池任务提交能说出你当时怎么定的参数、为什么这样定、遇到过什么问题、怎么解决的这段回答的质量就远高于我们项目里用过线程池一句话。我把这几年面试候选人的经验和自己的踩坑经历都写进了这篇内容里希望能帮你把Java并发这条线真正串起来。并发编程没有什么玄学根子是计算机体系结构和JVM的内存模型机制是无非就是锁、CAS、队列、状态这些基础组件的组合想通一次后面就是水和渠成的事。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门