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

Java并发编程面试核心原理精讲:从synchronized到AQS

想搞清楚 Java 并发编程光靠背题是不够的但完全不背题也很难应付面试。尤其这两年互联网公司面试越来越爱问底层原理从 synchronized 到 AQS从 volatile 到线程池基本每一轮技术面都会碰到。这篇博文我打算换个思路不按教科书目录平铺直叙而是从“面试官到底想考什么”这个角度切入把 Java 并发编程里最核心、最容易翻车、也是最有含金量的知识点一条条拆开讲。适合正在准备 Java 面试的候选人、刚工作一两年想补基础的后端开发以及写了好几年 CRUD 但一聊并发就心虚的同学。我自己的经验是并发编程是 Java 基础里最能拉开差距的模块。你可以在项目里用new Thread跑异步任务也可以用synchronized锁住关键代码但一旦需要排查线上 CPU 飙升、处理超高并发下的数据一致性、或者优化接口响应时间这些零散的知识就不够用了。把并发这块底层逻辑吃透对理解 JVM、理解操作系统调度、甚至理解分布式锁都有直接帮助。这篇内容我尽量把原理讲明白也会附带一些我实际排查问题和面试时总结出来的经验希望能帮大家少走弯路。1. 线程基础从 start 到死亡的完整闭环很多人学并发第一课就是“创建线程的几种方式”但面试问得最多的反而是线程状态、start()和run()的区别、wait()和sleep()的差异。这些看起来简单其实特别容易答漏。我见过不少候选人能把线程状态背出来但问到“yield()方法执行后线程进入什么状态”就犹豫了。这就是基础不扎实。1.1 线程的生命周期与状态流转Java 线程的顶层状态定义在Thread.State枚举里NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。很多人会用操作系统的五态模型去套但 Java 的状态更粗粒度RUNNABLE实际上囊括了操作系统里的就绪和运行两个状态因为 JVM 并不区分这两个状态它只关心线程是否可以被调度器执行。关键流转点有几个new Thread()之后是NEW调用start()进入RUNNABLE线程进入同步代码块但锁被别人持有就会变成BLOCKED调用Object.wait()或Thread.join()会进入WAITING带超时参数的版本进入TIMED_WAITING正常执行完或抛出未捕获异常后进入TERMINATED。面试中常问的“线程调用了yield()会进入什么状态”答案是仍然处于RUNNABLE因为yield()只是主动让出 CPU让调度器重新选择其他同优先级线程执行但该线程并没有被阻塞或等待它随时可能再次被选中。这里有一个我踩过的坑wait()和sleep()都能让线程暂停但wait()会释放对象锁而sleep()不会。用sleep()在持锁状态下等待会导致其他需要同一把锁的线程全部阻塞轻则性能下降重则超时报警。所以写等待逻辑时先想清楚这个线程占用的锁是否应该被释放如果需要释放应该用wait()或LockSupport.park()。1.2 start、run、join 与中断机制的正确理解start()和run()的区别是幼级题但我建议每个面试者都能从源码层面解释start()会通过 JVM 创建一条新的操作系统线程并让该线程执行run()方法直接调用run()只是在当前线程里执行一个普通方法不会新开线程。一个线程对象只能调用一次start()第二次会抛IllegalThreadStateException这是ThreadStatus状态校验的结果。join()的使用频率没有wait/notify高但在多线程任务聚合场景下很好用。主线程调用subThread.join()主线程会进入WAITING直到subThread执行完毕。源码里join()的实际实现是wait(0)循环检查存活状态所以它也会释放主线程持有的锁。如果你在持锁状态下调用join()一样会有死锁风险。中断机制是我认为国内教材讲得最敷衍的地方。Thread.interrupt()并不会真正强制终止线程它只是设置一个中断标志位。如果线程正阻塞在wait()、sleep()、join()上那么这些方法会立即抛出InterruptedException并清除中断标志如果线程在运行中则只修改标志位由业务代码自己决定如何响应。很多人写代码时捕获InterruptedException后直接e.printStackTrace()就完事这是非常坏的习惯。正确的做法是重新设置中断标志例如Thread.currentThread().interrupt()或者向上层抛出异常让调用方感知中断状态。我在实际项目中就见过因为吞掉中断异常导致关闭线程池时任务无法及时取消的问题。2. 并发编程的核心基石JMM、volatile 与 synchronized如果说线程基础是开胃菜那这块就是 Java 并发面试的正餐。几乎每一场技术面都会问volatile能不能保证原子性synchronized锁的到底是什么什么是 happens-before 规则我见过太多人背了答案但理解错位比如有人以为volatile能保证自增操作安全这就会在项目里埋雷。2.1 从 CPU 缓存到 Java 内存模型的映射要理解volatile先要理解为什么会有可见性问题。现代 CPU 为了提升执行速度会引入多级缓存L1/L2/L3每个 CPU 核心在操作共享变量时先查缓存再同步回内存。在多核场景下不同核心各自缓存了一份变量副本就可能导致某个核心修改了变量但其他核心看不到最新值。JMMJava 内存模型是在语言层面抽象出的内存模型它定义了共享变量存储在主内存中每个线程有自己的工作内存可以类比 CPU 缓存线程对共享变量的操作只能在“工作内存”中进行不能直接操作主内存线程间的共享变量传递需要通过主内存完成。这也正是volatile要解决的可见性根基。JMM 的核心约束是 happens-before 规则它并不要求任何操作都立即同步而是规定了两条操作之间如果存在 happens-before 关系则前一条操作的执行结果必须对后一条操作可见。我面试时经常用“程序顺序规则”“锁规则”“volatile 变量规则”“传递性”来举例。比如private int a 0; private volatile boolean flag false; // 线程1 a 1; flag true; // 线程2 if (flag) { // 这里能读到 a 1 吗 }因为flag是 volatile 变量线程1写flag true之前的普通写操作a 1在线程2读到flag true之后是可见的这是 volatile 规则与传递性共同作用的结果。如果flag不是 volatile线程2不保证能看到a 1。这是 JMM 里最经典的例子。2.2 volatile 的语义与极限volatile有两个核心语义可见性与禁止指令重排序。前者通过写操作后强制刷新主内存、读操作前强制从主内存读取来实现后者通过内存屏障来限制编译器和 CPU 的重排序。但volatile不保证原子性。经典例子是volatile int i的i操作它拆分成“读取 i 的值、加 1、写回 i”三步两个线程交错执行时可能丢失更新。我在代码评审里见过不止一次用 volatile 修饰计数器的场景一旦并发量上来就会出问题。如果要保证原子性应该使用AtomicInteger或加锁。此外volatile 也能用于安全发布不可变对象。比如把配置对象用 volatile 引用发布因为配置对象的所有字段在构造函数里赋值且构造函数在 volatile 写之前完成其他线程通过 volatile 引用读取时能拿到完整构造的对象。Java 8 以后还会用VarHandle和MethodHandles提供更细粒度的内存语义但面试中只要掌握 volatile 基本语义就够了。2.3 synchronized 的锁升级与底层实现synchronized在 JDK 6 之前是重量级锁因为底层依赖操作系统互斥量Mutex线程阻塞和唤醒需要内核态切换开销很大。JDK 6 之后引入了偏向锁、轻量级锁、重量级锁三级状态锁升级过程是无锁 → 偏向锁 → 轻量级锁 → 重量级锁。偏向锁的设计思路是绝大多数情况下锁不仅不存在多线程竞争而且总是由同一线程多次获得。所以当一个线程第一次进入 synchronized 块时JVM 会在对象头中记录该线程的 ID后续该线程再次进入时不需要任何 CAS 操作直接判断偏向线程 ID 是否是自己即可。一旦有其他线程尝试获取锁偏向锁就会撤销升级为轻量级锁。轻量级锁通过 CAS 和自旋来实现线程在进入临界区时会尝试用 CAS 在对象头的 Mark Word 中记录锁记录指针。如果设置成功表示获取轻量级锁成功如果失败说明锁被占用线程进入自旋等待。自旋会占用 CPU所以 JDK 默认开启自适应自旋JVM 会根据历史竞争情况动态调整自旋次数。如果自旋超过阈值或竞争加剧就升级为重量级锁也就是进入阻塞队列等待系统调度。这些原理面试时最好能结合对象头 Mark Word 来讲不用背二进制位但要知道 Mark Word 里存了锁标志位、偏向线程 ID、轻量级锁指针等信息。实际操作中我建议优先使用synchronized因为 JDK 一直在优化它而且它不会像使用ReentrantLock那样有忘记解锁的风险。但在需要可中断、可超时、公平锁语义的场景下ReentrantLock依然是更合适的选择。2.4 happens-before 规则速查我根据面试常考内容整理了一个 happens-before 速查表这里的核心不只是背规则而是能举例说明每条规则如何影响代码执行结果规则含义典型场景程序顺序规则单线程内前面的操作 happens-before 后面的操作同线程普通变量读写顺序监视器锁规则对一个锁的解锁 happens-before 后续对这个锁的加锁synchronized块内外的变量可见性volatile 变量规则对一个 volatile 变量的写 happens-before 任意线程后续对这个变量的读状态标志位、安全发布线程启动规则Thread.start()happens-before 该线程内所有操作在线程启动前设置的字段启动后能正确读到线程终止规则线程内所有操作 happens-before 其他线程检测到该线程终止join()返回后读取子线程写入的字段中断规则对线程调用interrupt()happens-before 被中断线程检测到中断事件配合InterruptedException判断传递性A happens-before BB happens-before C则 A happens-before C组合推导常用于代码分析面试中经常给一段多线程代码让判断变量可见性这类题基本就是用这张表来推导。建议你在复习时多写几个测试用例在本地跑一跑能加深记忆。3. 从 CAS 到 AQS读懂 JUC 的底层逻辑有人认为 JUCJava 并发工具包是一堆类的集合背熟ReentrantLock、Semaphore、CountDownLatch的用法就够了。但实际上 JUC 的底层逻辑非常集中核心就是 CAS 和 AQS。看懂 AQS整包工具类的原理就通了。3.1 CAS 机制的细节与 ABA 问题CASCompare And Swap是并发编程中最重要的无锁原语之一。它有三个操作数内存地址 V、旧的预期值 A、要设置的新值 B。仅当 V 的实际值等于 A 时才将 V 更新为 B否则不更新。在 Java 中CAS 通过Unsafe类的compareAndSwapInt、compareAndSwapLong等本地方法实现最终会映射到 CPU 的cmpxchg指令。CAS 的优势是不需要阻塞线程适用于竞争不激烈的场景但问题也很明显循环 CAS 在竞争激烈时会导致大量空转浪费 CPU而且 CAS 只能保证一个共享变量的原子操作多变量场景就不能直接用。更经典的是 ABA 问题如果一个线程从位置 V 读取到值 A另一个线程将 V 改为 B 再改回 A那么第一个线程的 CAS 会成功但它并不知道值曾被修改过。解决 ABA 问题有两种思路用版本号或时间戳标记每次修改或者使用AtomicStampedReference。在业务上很多 ABA 场景其实不影响最终结果比如栈顶元素从 A 变为 B 再变回 A如果栈中其他元素也被改过就可能产生问题。面试时一定要把 ABA 问题的本质讲清楚并提出解决方案而不是只背“版本号”。Java 8 之后引入的LongAdder、LongAccumulator是从另一个角度规避 CAS 竞争问题它们把热点数据分段每个线程在自己对应的 Cell 上累加最后再求和。这种“分段累加 最终合并”的思路在高并发计数器场景下效果很好。3.2 AQS 的设计思想与核心结构AQSAbstractQueuedSynchronizer是一个用于构建锁和同步器的抽象基类。JUC 里大部分同步工具ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock内部都有一个继承自 AQS 的静态内部类 Sync。AQS 的核心是三个要素volatile int state、CLH 变体队列、以及基于 LockSupport 的阻塞与唤醒。state的值在不同同步器里含义不同在ReentrantLock里表示持有锁的次数在Semaphore里表示剩余许可数在CountDownLatch里表示剩余计数值。共享变量与独占变量通过tryAcquire/tryRelease和tryAcquireShared/tryReleaseShared两种模式区分。CLH 队列的入队和出队都是用 CAS 保证线程安全的。当一个线程获取锁失败时会构造一个 Node 节点加入队列尾部然后通过LockSupport.park()挂起当持有锁的线程释放锁时会唤醒队列头部的后继节点。公平锁与非公平锁的差异就在这里非公平锁在获取时先做一次 CAS不管队列里有没有线程在等待直接抢公平锁则严格判断队列中是否有前驱节点有就乖乖排队。ReentrantLock默认是非公平锁因为它的吞吐量通常更高但如果你需要保证等待时间可以传true启用公平锁。我之前排查过一个线上问题某服务使用ReentrantLock作为分布式锁的本地位但代码里没有在 finally 中解锁导致一个线程拿到锁后抛异常释放不了其他线程全部阻塞在lock()上接口超时直线上升。排查时用jstack能看到大量线程处于WAITING状态。这个案例说明用 AQS 锁的时候一定要把lock和unlock像synchronized代码块一样处理好异常路径否则影响面非常大。3.3 JUC 常用同步工具对比除了ReentrantLock还有几个常考的工具类。我区分它们的方法很简单CountDownLatch是“等 N 件事完成”计数只能使用一次CyclicBarrier是“N 个线程互相等都到齐后继续”可以循环使用Semaphore是“控制同时访问资源的线程数”像是流量控制闸门。同步工具核心用途是否可重用底层机制CountDownLatch一个或一组线程等待 N 个事件完成否AQS 共享模式state 递减到 0 唤醒等待CyclicBarrierN 个线程互相等待到达屏障点是reset 后复用基于 ReentrantLock 和 Condition 的“分代”实现Semaphore控制同时访问临界资源的线程数是AQS 共享模式state 表示许可数Phaser更灵活的同步屏障支持分阶段是比 CyclicBarrier 更复杂的阶段控制我实际用CyclicBarrier不多反而CountDownLatch用的最多比如主线程等待 N 个子线程完成批量数据导入后再统一返回。但在使用CountDownLatch时要注意如果子任务执行过程中抛异常导致countDown()没执行主线程会一直等待。妥当的做法是无论是否异常都在 finally 里countDown()。4. 线程池并发执行的最强工具也是最容易踩坑的地方线程池是 Java 并发编程里用得最多、也最容易出问题的模块。阿里规范的强制要求加上面试常考让线程池成了八股文流量最高的内容之一。但很多开发只是会用Executors.newFixedThreadPool()并不清楚它的参数限制和内部工作原理。一旦遇到任务队列堆积往往束手无策。4.1 ThreadPoolExecutor 七大参数与执行流程ThreadPoolExecutor的构造参数有 7 个corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。核心执行逻辑是提交任务时如果当前线程数小于核心线程数创建新的核心线程执行任务如果当前线程数已到达核心线程数新任务进入阻塞队列等待如果队列已满且当前线程数小于最大线程数创建非核心线程执行任务如果线程数已经到达最大线程数且队列已满则触发拒绝策略。这个流程中关键点在于核心线程池不是先创建好等任务而是有任务提交时懒加载创建。prestartCoreThread()可以提前创建核心线程但没有特殊需求不需要用。keepAliveTime只对非核心线程生效默认情况核心线程即使空闲也不会销毁。如果你希望核心线程也超时回收可以设置allowCoreThreadTimeOut(true)。拒绝策略有四种AbortPolicy直接抛异常默认、CallerRunsPolicy提交者线程自己执行、DiscardPolicy丢弃任务、DiscardOldestPolicy丢弃队列中最早的任务。我在业务代码中最常用的是CallerRunsPolicy因为它不会丢失任务还能通过“反弹”机制降低任务提交速度相当于天然背压保护。但要注意使用CallerRunsPolicy后接口响应时间可能会因为回调执行而变长需要做好监控。线程池的 7 个参数从面试角度一定要能画一遍执行流程图并解释每种参数的意义。从实战角度我更推荐使用ThreadPoolExecutor显式构造而不是Executors快捷方法因为newFixedThreadPool的队列是LinkedBlockingQueue默认容量是Integer.MAX_VALUE一旦任务堆积会占满内存newCachedThreadPool的最大线程数是Integer.MAX_VALUE可能导致创建大量线程拖垮系统。4.2 线程池大小如何确定线程池大小设置是面试里最容易被追问的问题。理想情况下需要考虑任务是 CPU 密集型还是 IO 密集型。对 CPU 密集型任务线程数可以设置为CPU 核数 1。多出来的一个线程是为了应对某些线程出现缺页异常或偶尔阻塞时能继续利用 CPU。对 IO 密集型任务线程数可以设置为CPU 核数 * 2或更高因为线程大部分时间在等待 IO更多的线程能提高 CPU 利用率。更精确的经验公式是线程数 CPU 核数 * (1 平均等待时间 / 平均计算时间)比如一个任务平均耗时 100ms其中 90ms 在等待 IO10ms 在计算那线程数可以设为核数 * (1 9)也就是核数 * 10 左右。这个公式需要结合压测调节不能直接套用。我在项目中通常先用一个保守值再通过压测不断观察 CPU 使用率、线程池队列长度、任务执行耗时逐步调整。线程池不是一次性配置完就不管了必须配合监控指标。常见的监控指标有当前线程数、活动线程数、队列积压任务数、任务完成数、拒绝任务数。这些指标在 Spring Boot 中可以通过ThreadPoolTaskExecutor暴露给 Micrometer再接入 Prometheus 和 Grafana。4.3 execute 与 submit、异常处理与优雅关闭execute(Runnable)和submit(Callable)核心区别是submit可以传入有返回值任务或Runnable并返回Future用于获取结果或异常而execute没有返回值。还有一个容易忽略的差异execute抛出的异常会由线程池内部处理如果不自定义UncaughtExceptionHandler异常信息可能直接丢失submit提交的任务如果执行抛出异常会被封装在Future中只有在调用future.get()时才会抛出ExecutionException。所以在submit后不调用get()也会出现“吞掉异常”的假象。优雅关闭线程池是一个很容易被忽视的问题。shutdown()会停止接收新任务等待已提交和正在执行的任务完成shutdownNow()会尝试中断正在执行的线程并返回尚未开始执行的任务列表。比较稳妥的关闭方式是先调用shutdown()再循环调用awaitTermination()等待剩余任务完成如果超时后再执行shutdownNow()强制终止。我在一个数据同步项目中踩过坑主线程结束时直接调用了System.exit(0)而线程池里的任务还在执行最后导致部分数据丢包。后来改成通过 Spring 的PreDestroy钩子统一关闭线程池并设置合理的超时时间问题才解决。4.4 ForkJoinPool 与虚拟线程的补充如果不提ForkJoinPool和 JDK 21 的Virtual Thread线程池专题多少有点不完整。ForkJoinPool是 Java 7 引入的用于分治任务的线程池核心标志是工作窃取算法每个工作线程有独立的任务队列线程处理完自己的任务后可以从其他线程的队列尾部窃取任务来执行这能最大程度平衡负载。CompletableFuture的并行任务默认使用ForkJoinPool.commonPool()。JDK 21 正式发布的虚拟线程对传统线程模型冲击很大虚拟线程是 JVM 层面的轻量线程一个平台线程可以运行成千上万个虚拟线程这使得高并发 IO 场景不再需要依赖大线程池。我在新项目里会优先考虑虚拟线程来替代 IO 密集型线程池但要注意如果任务里包含 synchronized 块或 native 方法可能会发生平台线程阻塞虚拟线程的收益会打折扣。这个点面试里如果主动提出来通常能加分。5. 并发容器与 ThreadLocal细节决定成败并发容器和 ThreadLocal 属于“看起来简单问起来很深”的模块。ConcurrentHashMap几乎必考ThreadLocal的内存泄漏问题也常常被拿来当灵魂拷问。这块内容既 Java 基础又务实很多人容易忽略细节。5.1 ConcurrentHashMap 的分段锁与 CAS 演进JDK 7 的ConcurrentHashMap采用 Segment 分段锁默认支持 16 个 Segment每次只锁一个 Segment从而降低锁竞争。JDK 8 抛弃了 Segment改用 Node 数组 synchronized CAS 实现锁粒度更细当发生哈希冲突时只对链表的头节点或红黑树的根节点加锁不同桶之间的插入可以完全并发。JDK 8 的putVal流程大致是如果 table 未初始化使用一个基于sizeCtl的 CAS 来保证多线程并发初始化如果目标桶为空用 CAS 直接放入新节点如果桶不为空同步锁住桶头节点再执行链表插入或红黑树更新节点数达到阈值 8 时链表会树化为红黑树。size()方法也不再是简单的遍历计数而是通过 baseCount CounterCell 累加并且在高并发时通过分段计数来减少竞争。有一个面试盲区是ConcurrentHashMap的size()在多线程环境中并不是一个绝对准确的实时值它可能在统计过程中发生变化。源码里会在无竞争时先快速累加如果在累加过程中发现CounterCell被修改会尝试用 CAS 更新baseCount必要时加锁重试。所以在业务中不要指望ConcurrentHashMap.size()能提供强一致性的计数它更适合作为近似值使用。5.2 CopyOnWriteArrayList 与 BlockingQueue 的关键特性CopyOnWriteArrayList的思路是“写时复制”每次修改操作都会复制一份底层数组对新数组执行修改再把数组引用指向新数组读操作不加锁直接读原数组。这适合读多写少的场景比如监听器列表、白名单配置。缺点也明显写操作有复制成本频繁写入时内存开销大且读操作可能出现“弱一致性”延迟即读线程可能读到旧数据直到写操作完成发布新数组。BlockingQueue是线程池的核心组件常见实现有ArrayBlockingQueue有界基于数组、LinkedBlockingQueue可选有界基于链表、SynchronousQueue无存储空间直接传递、PriorityBlockingQueue支持优先级。ArrayBlockingQueue用一把锁和两个 Condition 实现阻塞LinkedBlockingQueue则用两把锁分别控制入队和出队并发性更好。SynchronousQueue看起来像队列但内部没有缓冲容量每个put必须等待一个take常用于需要严格配对传递任务的场景比如Executors.newCachedThreadPool()使用的就是它。5.3 ThreadLocal 原理与内存泄漏问题ThreadLocal为每个线程维护一份独立的变量副本。每个Thread内部有一个ThreadLocalMap键是ThreadLocal对象的弱引用值是实际存储的对象。线程读写变量时都从这个 Map 中取出对应的 Entry 操作从而避免多线程共享冲突。内存泄漏是面试重点。ThreadLocalMap.Entry继承了WeakReferenceThreadLocal即 key 是弱引用但 value 是强引用。ThreadLocal 的外部强引用被置为null后key 会被 GC 回收但 value 仍然被ThreadLocalMap里的 Entry 强引用持有。如果线程本身存活且一直不调用get、set、removevalue 就无法被回收导致泄漏。最典型的场景是 Tomcat 等线程池复用工作线程线程存活时间很长。规避方法很简单在使用完 ThreadLocal 后调用remove()清理当前线程对应的值。尤其是 Web 请求处理中在 Filter 里完成业务逻辑后删除 ThreadLocal 是规范做法。此外ThreadLocalMap在get/set时会对 key 为 null 的 Entry 进行清除但这只是降低泄漏概率不能完全依赖它。5.4 CompletableFuture 与并行编程实践虽然CompletableFuture不算容器但它常和线程池、Future 一起被面试提问。它提供了一种链式处理异步任务的方式可以轻松组合多个异步结果例如CompletableFuture.supplyAsync(() - queryDb(), executor) .thenApplyAsync(result - process(result), executor) .exceptionally(err - { log.error(query process error, err); return fallback(); });CompletableFuture在实现层面使用的CompletableFuture内部通过类似 AQS 的等待机制管理完成状态回调任务会提交到指定的线程池执行。如果不指定 executor默认使用ForkJoinPool.commonPool()在 Java Web 应用里可能会和其他并行任务竞争线程建议显式传入独立的线程池。我在实际项目里用CompletableFuture.allOf并行调用多个上游接口等所有结果返回后统一组装响应。这里有个容易踩的坑如果不设置超时上游接口迟迟不返回整个请求会一直挂住。解决办法是用orTimeout或completeOnTimeout给每个异步任务设置超时兜底再配合exceptionally处理超时结果。6. 并发编程高频面试题与实战避坑清单到了这里知识点基本都过了一遍。最后一部分我想结合自己面试别人和被别人面试的经验整理一个高频问题清单再加一些实战中的坑位提示。这个清单不是让我们死记硬背而是用来检查自己是否真的理解了每个知识点。6.1 高频面试题速问速答下面这些问题都是我实际在面试里遇到过或问过别人的覆盖了从基础到进阶的大部分维度问题核心考察点回答要点线程有哪些状态如何流转线程基础六种状态重点说 BLOCKED 与 WAITING 的区别wait()和sleep()有什么区别阻塞语义wait 释放锁sleep 不释放wait 依赖 synchronizedsleep 不依赖volatile能保证原子性吗JMM不能只能保证可见性和有序性synchronized底层是怎么实现的锁升级从偏向锁到轻量级锁到重量级锁结合对象头 Mark WordCAS 存在什么问题无锁并发ABA 问题、自旋消耗 CPU、只能保证单变量原子性AQS 的原理是什么JUC 基础state CLH 队列 LockSupport 阻塞唤醒ReentrantLock和synchronized怎么选锁对比synchronized 简洁自动释放ReentrantLock 支持可中断、超时、公平锁线程池有哪些参数拒绝策略有哪些线程池七大参数、四种拒绝策略结合执行流程说明如何设置线程池大小实践设计区分 CPU 密集和 IO 密集给出经验公式ThreadLocal为什么会导致内存泄漏内存分析key 弱引用、value 强引用线程池复用导致 long liveConcurrentHashMap为什么线程安全并发容器JDK8 使用 CAS synchronized 锁桶CompletableFuture和Future有什么区别异步编程Future 同步阻塞获取CompletableFuture 支持回调编排什么是活锁和死锁并发问题死锁是互相等待活锁是持续重试但无法推进可结合实际案例6.2 动态排查从线程转储到修复问题聊完面试题分享一下实际工作中排查并发问题的步骤。遇到 CPU 100% 或接口卡死时我一般按下面流程走用top -Hp pid找到 CPU 占用最高的线程 ID转成十六进制执行jstack pid thread_dump.txt在线程转储中搜索对应的十六进制线程 ID定位到具体业务代码检查该线程状态如果是WAITING可能是在等待锁或条件变量如果是RUNNABLE可能是死循环或频繁 GC再用jstat -gcutil pid观察 GC 情况排除 GC 抖动影响结合日志和监控确认是否有线程池队列积压或锁竞争。我处理过最典型的线程池队列积压问题某服务使用FixedThreadPool核心线程数 10队列无界某天上游数据量激增任务提交速度远超处理速度队列堆积了上千万个任务最终导致内存飙升。当时从监控指标看到队列长度持续上升但线程池活动线程数一直是 10说明生产速率大于消费速率。后来改成有界队列加CallerRunsPolicy并针对不同业务拆分了独立线程池问题才算根治。6.3 并发编程最容易被忽视的五个细节除了知识本身我再讲五个从代码评审和线上故障中总结出的细节它们不一定在八股文里但很容易让一个系统在并发场景下出问题。第一共享可变状态的访问没有统一加锁。有人喜欢在单线程测试后直接把代码丢到多线程环境结果变量被并发修改出现“内存可见性”问题。解决思路是少用共享可变状态尽量使用局部变量和不可变对象。第二锁粒度太粗或太细。粗了性能差细了容易出竞态。一个参考做法是先保证正确性再根据压测结果优化锁粒度。不要上来就写细粒度锁一旦漏掉某条路径就会出并发 bug。第三忘记处理中断异常。使用Thread.sleep()或BlockingQueue.take()时如果捕获了InterruptedException后直接吞掉线程在关机时无法及时响应中断甚至导致任务无法终止。正确做法是保留中断状态或重新抛出异常。第四没有设置异步任务超时。现在很多服务用CompletableFuture或Future做并行调用如果不设置超时下游服务长时间不返回上游线程会全部被占用最终拖垮整个应用。我在前面也提过orTimeout和completeOnTimeout是 Java 9 之后很重要的兜底手段。第五多个异步任务共享同一个线程池时不隔离。比如所有业务共用一个线程池某个接口流量突然上涨就会把其他接口的线程池资源挤占光。建议按业务场景拆分线程池并为不同池设置不同的容量和拒绝策略。6.4 如何高效准备并发编程的“八股文”最后说点关于备考的建议。很多同学抱着“背八股文”的心态刷题今天背 volatile明天背 AQS后天问起来又忘了。我的方法是“以点带面以写带背”针对每个核心知识点写一个可运行的 demo自己把结果跑出来再反问自己三个问题——“它解决了什么问题”“它是怎么解决的”“如果我是设计者会怎么做”。比如你学CountDownLatch可以写一个模拟游戏加载的 demo主线程等待三个玩家都下载完资源后开始游戏。跑完之后再去看源码看await()和countDown()内部怎么通过 AQS 实现然后试着用ReentrantLock和Condition自己实现一个简易版。这个过程能帮你把八股文从“记忆”变成“理解”面试时自然能讲得更有底气。还有一个很有效的复习方式建立一份自己的“并发知识脑图”从线程基础往下分裂到 JMM、锁、JUC 工具、线程池、异步编程、并发容器。每学完一个点就在脑图上补充一小段自己的话。不用太复杂但一定要用自己的语言写因为这其实是在检查自己有没有真的懂。从我自己面试候选人的经验看面试官最反感的不是“不会”而是“只背出结论但解释不了为什么”。比如问他为什么 ConcurrentHashMap 读操作不用加锁他能说出不加锁但说不清“读操作从不修改数据而且内部通过 volatile 语义保证可见性”这一层。这就是典型的知识没串起来。建议大家在准备并发八股文时先耐心读一遍源码再回来背结论这样即使题目有变化也能从容应对。做并发编程久了你会发现很多看似高深的问题其实都落在几个基础机制上可见性靠 JMM原子性靠 CAS 或锁阻塞唤醒靠 AQS。把这三条主线打通再复杂的并发问题也能拆解成容易分析的小块。希望这篇内容能帮你在复习 Java 并发时建立一个相对清晰的框架后面碰到具体问题至少知道该往哪个方向去分析。
分享:

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

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