ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java多线程面试全攻略:从volatile到线程池的底层原理与实战

Java多线程面试全攻略:从volatile到线程池的底层原理与实战 8月的Java面试战场上多线程几乎是所有大厂必考、中小厂重点关注的题目而且它通常不是一道独立的题而是会连环追问volatile 怎么保证可见性synchronized 底层原理是什么线程池参数怎么设置AQS 是什么你会发现背得再熟的八股文也架不住面试官一句那你说说它底层怎么实现的。为什么多线程这么重要因为它是 Java 后端开发绕不开的并发基石。无论你是写业务接口、消息队列消费者还是做中间件、基础组件只要系统一上量并发问题就会暴露出来。面试官问多线程表面是考知识点实际是看你能不能写出健壮的并发代码能不能处理线上出现的死锁、OOM、线程阻塞这类生产事故。这篇文章会围绕多线程面试中最常考的板块来写基础概念、JMM 与 volatile、synchronized 与锁升级、Lock 与 AQS、ThreadLocal、线程池、CompletableFuture、死锁排查以及一份可以直接照着做的 3 天冲刺计划。全文提供可复制的代码示例、答题思路和避坑清单适合秋招、社招和跳槽前临时抱佛脚也适合刚学完 Java 基础想系统整理并发知识的同学。1. 这篇文章真正要解决的问题先给一个判断多线程面试题已经在从背概念转向考原理、考场景、考排错。以前面试多线程问创建线程有几种方式sleep 和 wait 的区别就结束了。现在不行了。现在面试官喜欢把问题串起来从一个点跳到另一个点比如从 volatile 问到 JMM再问到 happens-before。从 synchronized 问到锁升级再问偏向锁和轻量级锁的区别。从线程池参数问到核心线程数怎么定再问线上线程池队列满了该怎么办。从 ThreadLocal 问到内存泄漏再问为什么要用弱引用。所以这篇文章要解决的不只是记住答案而是帮你建立一张多线程知识网络知道每个知识点在真实项目中解决什么问题。3 天时间能做什么我的判断是前 1 天搞定基础概念和线程安全核心机制第 2 天搞定线程池、JUC 工具类、异步编程第 3 天专门练死锁排查、高频题自测和模拟答题。每天都能产出能讲出来的知识而不是看着眼熟的知识。这里要特别提醒一句不要试图 3 天看完所有并发内容。像 ForkJoinPool 的底层工作窃取算法、Disruptor 的无锁队列这类高阶内容面试中出现的概率低性价比不高。先把高频、成体系的内容吃透比零散扫一百篇博客更有用。2. 多线程基础概念、状态与创建方式2.1 线程与进程到底差在哪进程是操作系统分配资源的基本单位线程是 CPU 调度的基本单位。一个进程内有多个线程线程共享进程的堆和方法区各自拥有独立的程序计数器、虚拟机栈和本地方法栈。面试官爱问的一个变体是为什么线程切换比进程切换代价小答案核心是同一进程的线程共享地址空间切换时不需要切换页表、刷新 TLB而进程切换需要切换独立的地址空间代价更高。这个问题虽然基础但答得完整能加分。2.2 并发与并行不要混成一件事并发是多个任务交替执行宏观上同时发生微观上还是轮流使用 CPU并行是多个任务在多个 CPU 核心上真正同时执行。单核 CPU 上只能并发多核 CPU 上才有可能并行。很多同学把这两个概念写反面试时一紧张就说错建议在纸上多写几遍。2.3 线程状态转换是必考题Java 线程在任意时刻处于以下六种状态之一NEW新建 RUNNABLE可运行 BLOCKED阻塞 WAITING等待 TIMED_WAITING定时等待 TERMINATED终止这里最容易被问的是 BLOCKED 和 WAITING 的区别。BLOCKED 是线程在争夺 synchronized 锁时没抢到锁而进入阻塞WAITING 是线程主动调用 wait()、join() 或 LockSupport.park() 而进入等待需要其他线程唤醒。用一句话记忆BLOCKED 是被动等锁WAITING 是主动等通知。2.4 创建线程的四种方式创建线程看似简单但面试官会追问这几种方式本质上有什么区别。// 方式一继承 Thread class MyThread extends Thread { Override public void run() { System.out.println(继承 Thread 创建线程); } } // 方式二实现 Runnable class MyRunnable implements Runnable { Override public void run() { System.out.println(实现 Runnable 创建线程); } } // 方式三实现 Callable FutureTask class MyCallable implements CallableString { Override public String call() throws Exception { return 实现 Callable 创建线程有返回值; } } // 方式四线程池 ExecutorService pool Executors.newFixedThreadPool(2); pool.execute(() - System.out.println(线程池创建线程));四种方式本质上都是把任务交给线程去执行。继承 Thread 的方式耦合度高不建议使用实现 Runnable 适合无返回值的任务Callable 有返回值、能抛异常配合 FutureTask 或线程池使用线程池则是最推荐的日常开发方式能复用线程、控制并发数。回答这个题时建议主动补充我们项目里基本只用线程池 Runnable/Callable不会裸写 Thread。这会让面试官觉得你有工程意识。3. 线程安全与 JMMvolatile、synchronized 与锁升级3.1 JMM 到底在讲什么Java 内存模型JMMJava Memory Model规定所有变量存储在主内存中每个线程有自己的工作内存线程对变量的读写必须在工作内存中进行不能直接操作主内存。这带来三个经典问题可见性、原子性、有序性。可见性一个线程修改了变量另一个线程不一定能立刻看到。原子性一组操作是不可分割的比如 i 在字节码层面是读-改-写三步不是原子的。有序性编译器和 CPU 可能对指令进行重排序。3.2 volatile 的可见性和有序性但没有原子性volatile 是面试出镜率极高的修饰符。它有两个作用保证变量在不同线程间的可见性禁止指令重排序。public class VolatileDemo { private static volatile boolean flag false; public static void main(String[] args) throws InterruptedException { Thread writer new Thread(() - { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag true; System.out.println(writer 将 flag 置为 true); }); Thread reader new Thread(() - { while (!flag) { // 如果 flag 不加 volatile这里是死循环 } System.out.println(reader 感知到 flag 变化); }); reader.start(); writer.start(); reader.join(); writer.join(); } }如果把 flag 去掉 volatilereader 线程可能一直看不到 flag 的变化因为在没有同步机制的情况下JMM 不保证这种跨线程共享变量的可见性。加了 volatile 后写入操作会立即刷新到主内存读取操作会从主内存重新读取。但 volatile 不保证原子性。经典反例是多个线程同时对 volatile 变量执行 count最终结果小于预期值。原因是 count 是读-改-写三步操作volatile 只保证了读和写的可见性没有保证这三步不会被其他线程插队。要保证原子性要么用 AtomicInteger要么用 synchronized 或 Lock。面试加分点volatile 还常用于双重检查锁的单例模式中用来保证单例实例的可见性和禁止指令重排序。3.3 synchronized 的三种用法和锁升级synchronized 是 Java 内置的关键字可以修饰实例方法、静态方法、代码块。它的互斥性来自监视器锁Monitor每个对象关联一个监视器线程进入同步代码块前需要获得锁执行完或抛出异常时释放锁。Java 6 之后 synchronized 做了很多优化锁有四种状态无锁 - 偏向锁 - 轻量级锁 - 重量级锁。偏向锁同一个线程多次获取同一把锁时锁会偏向该线程减少无竞争情况下的 CAS 开销。轻量级锁当有第二个线程竞争时偏向锁升级为轻量级锁线程通过自旋等锁。重量级锁自旋失败后升级为重量级锁线程进入 BLOCKED 状态由操作系统调度。面试官常问synchronized 和 ReentrantLock 怎么选。我的回答框架是如果不需要中断、超时、公平锁这些高级能力优先用 synchronized因为它是 JVM 原生支持随着 JVM 优化如锁粗化、锁消除性能已经很好而且代码更简洁需要可打断获取锁、超时获取锁、公平锁、多个 Condition 时才用 ReentrantLock。3.4 锁的代码示例public class SynchronizedDemo { private int count 0; // 修饰实例方法锁是当前对象 public synchronized void increment() { count; } // 修饰代码块锁是指定对象 public void incrementWithBlock() { synchronized (this) { count; } } // 修饰静态方法锁是当前类的 Class 对象 public static synchronized void staticMethod() { System.out.println(static synchronized); } }注意修饰静态方法和修饰实例方法用的不是同一把锁。静态方法锁的是 Class 对象实例方法锁的是实例对象这两者在并发执行时并不会互相阻塞。这个细节面试里经常用来挖坑。4. 显式锁与 AQSLock、Semaphore、CountDownLatch 的底层统一4.1 ReentrantLock 基础用法ReentrantLock 是 java.util.concurrent.locks 包下的可重入锁本质是通过 AbstractQueuedSynchronizerAQS实现的。它有公平锁和非公平锁两种模式默认是非公平锁。import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { private final Lock lock new ReentrantLock(); private int count 0; public void increment() { lock.lock(); try { count; } finally { lock.unlock(); } } }使用 Lock 时unlock() 必须放在 finally 里否则一旦业务代码抛出异常锁就永远无法释放最终导致线程死锁。这一点是面试里最容易白送的踩分点。值得强调的是可重入性同一个线程可以重复获得已经持有的锁锁内部有持有计数每重入一次计数加一每释放一次减一直到归零才真正释放。synchronized 同样可重入所以可重入并不是 ReentrantLock 独有的特点。4.2 CountDownLatch等所有线程就绪CountDownLatch 是AQS 的共享锁思想用于一个线程等待多个线程完成操作。import java.util.concurrent.CountDownLatch; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { int workerCount 3; CountDownLatch latch new CountDownLatch(workerCount); for (int i 0; i workerCount; i) { new Thread(() - { try { System.out.println(Thread.currentThread().getName() 开始执行任务); Thread.sleep(1000); System.out.println(Thread.currentThread().getName() 执行完成); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }, worker- i).start(); } latch.await(); System.out.println(所有 worker 执行完成主线程继续); } }注意 CountDownLatch 的计数不能重置所以它是一次性的。如果需要反复等待多个线程完成要考虑 CyclicBarrier 或自行实现。4.3 CyclicBarrier线程互相等待然后一起放行CyclicBarrier 和 CountDownLatch 的区别是面试高频题。可以这样记忆CountDownLatch 是倒数门闩一个线程等到 N 个 countDown 后继续CyclicBarrier 是循环栅栏N 个线程互相等都到齐后一起执行而且可以重置重复使用。import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { public static void main(String[] args) { int parties 3; CyclicBarrier barrier new CyclicBarrier(parties, () - System.out.println(所有线程已到达栅栏放行)); for (int i 0; i parties; i) { new Thread(() - { try { System.out.println(Thread.currentThread().getName() 正在执行); Thread.sleep((long) (Math.random() * 1000)); System.out.println(Thread.currentThread().getName() 到达栅栏); barrier.await(); System.out.println(Thread.currentThread().getName() 继续执行之后的任务); } catch (InterruptedException | BrokenBarrierException e) { Thread.currentThread().interrupt(); } }, thread- i).start(); } } }CyclicBarrier 有一个需要留意的异常BrokenBarrierException。如果某个线程在等待时被中断或超时栅栏会进入 broken 状态其他等待线程会收到 BrokenBarrierException。生产环境里如果使用 CyclicBarrier建议对栅栏状态做兜底处理避免线程永远卡在 await。4.4 Semaphore控制并发访问数Semaphore 是最容易理解的限流工具acquire()获取许可证release()释放许可证。比如数据库连接池最多只允许 5 个连接同时使用可以用 Semaphore(5) 来控制。import java.util.concurrent.Semaphore; public class SemaphoreDemo { private static final int PERMITS 2; public static void main(String[] args) { Semaphore semaphore new Semaphore(PERMITS); for (int i 0; i 5; i) { new Thread(() - { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() 获取许可证开始工作); Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { System.out.println(Thread.currentThread().getName() 释放许可证); semaphore.release(); } }, task- i).start(); } } }这个例子中Semaphore 设置为 25 个线程同时竞争同一时刻最多只有 2 个线程在工作其余线程阻塞在 acquire。release 同样要放在 finally 中防止线程异常导致许可证丢失。4.5 画 AQS 的统一视角AQSAbstractQueuedSynchronizer是 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 这些同步器的共同底座。核心设计是一个 volatile int state 代表同步状态一个 CLH 变体双向队列存放等待线程通过 CAS 修改 state 来获得和释放同步状态。面试回答 AQS 时记住这几句话就够用了AQS 维护一个 state 和一个等待队列。获取锁时通过 CAS 尝试将 state 从 0 改为 1成功则获得锁失败则把线程封装成 Node 放入等待队列并挂起。释放锁时将 state 改回 0唤醒队列中的下一个节点。ReentrantLock 把 state 当作可重入计数Semaphore 把 state 当作剩余许可证数CountDownLatch 把 state 当作剩余倒数计数。这个统一视角能帮你把散乱的并发工具串成一条线面试时显得全局观很强。5. ThreadLocal原理与内存泄漏5.1 ThreadLocal 解决什么问题ThreadLocal 提供了线程局部变量每个线程拥有自己独立的变量副本不会互相干扰。最经典的例子是 SimpleDateFormat 的线程安全问题还有用户登录信息、请求 ID 的透传。public class ThreadLocalDemo { private static final ThreadLocalString USER_CONTEXT new ThreadLocal(); public static void main(String[] args) { for (int i 0; i 2; i) { int userId i; new Thread(() - { USER_CONTEXT.set(user- userId); System.out.println(Thread.currentThread().getName() 设置值: USER_CONTEXT.get()); // 使用完成后必须 remove防止内存泄漏 USER_CONTEXT.remove(); }, thread- i).start(); } } }面试追问点通常是ThreadLocal 的底层结构是什么答案是 Thread 内部有 ThreadLocalMapkey 是 ThreadLocal 对象value 是 set 进去的值。每个线程访问 ThreadLocal 时实际上是访问自己 ThreadLocalMap 里对应的 entry。5.2 ThreadLocal 内存泄漏问题ThreadLocalMap 的 key 是 ThreadLocal 的弱引用value 是强引用。当外部没有强引用指向 ThreadLocal 对象时key 会被 GC 回收但 value 仍然被 ThreadLocalMap 中的 Entry 强引用着。如果线程长期存活比如线程池里的核心线程value 就无法被回收造成内存泄漏。解决方式很明确在线程结束前或使用完成后调用 ThreadLocal.remove() 清除当前线程的变量。尤其在线程池场景中线程会复用如果不 remove下一次执行任务时还能读取到上一次任务遗留的脏数据。面试答题模板先讲 ThreadLocal 存值取值流程再讲 Entry 的 key 是弱引用、value 是强引用最后一定要说出使用完必须 remove。能主动提到这个坑面试官通常会有好感。6. 线程池核心参数、执行流程与拒绝策略6.1 为什么不建议用 Executors 创建线程池很多入门教程会写 Executors.newFixedThreadPool(10) 或 Executors.newCachedThreadPool()。但实际生产环境尤其是阿里编码规范里明确建议不要用 Executors 的快捷方法因为它们有些默认值有坑。newFixedThreadPool 和 newSingleThreadExecutor 用的是无界 LinkedBlockingQueue任务堆积过多时可能 OOM。newCachedThreadPool 的最大线程数是 Integer.MAX_VALUE如果任务执行很慢会创建大量线程也可能 OOM。newScheduledThreadPool 同样是最大线程数极大。所以默认做法是使用 ThreadPoolExecutor 的完整构造方法明确核心线程数、最大线程数、队列容量、拒绝策略。6.2 ThreadPoolExecutor 的核心参数import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolDemo { public static void main(String[] args) { ThreadPoolExecutor executor new ThreadPoolExecutor( 2, // 核心线程数 4, // 最大线程数 60L, // 非核心线程空闲存活时间 TimeUnit.SECONDS, new ArrayBlockingQueue(100), // 有界任务队列 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); for (int i 0; i 10; i) { final int taskId i; executor.execute(() - { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println(Thread.currentThread().getName() 执行任务 taskId); }); } executor.shutdown(); } }参数含义对应参数含义corePoolSize核心线程数即使空闲也不会被回收maximumPoolSize最大线程数非核心线程超过空闲时间会被回收keepAliveTime非核心线程空闲存活时间unit存活时间单位workQueue任务队列线程不够时任务先进队列threadFactory线程工厂建议自定义命名handler拒绝策略队列和最大线程都满时的处理方式6.3 线程池执行流程这个流程几乎是必考题一定要按顺序说提交任务时如果当前线程数小于 corePoolSize创建核心线程执行任务。如果当前线程数不小于 corePoolSize把任务放入 workQueue。如果 workQueue 已满且当前线程数小于 maximumPoolSize创建非核心线程执行任务。如果当前线程数已达到 maximumPoolSize执行拒绝策略。记忆口诀先核心再队列再最大最后拒绝。6.4 四种拒绝策略策略行为AbortPolicy直接抛 RejectedExecutionException默认策略CallerRunsPolicy交给调用者线程执行降低任务提交速度DiscardPolicy静默丢弃任务DiscardOldestPolicy丢弃队列头部最老的任务重新提交当前任务面试中经常问如果队列满了怎么办很多人只能答出 AbortPolicy。我会建议在项目中根据业务选择核心场景用 CallerRunsPolicy 做降级或者自定义拒绝策略记录日志、发送告警、写入 MQ 补偿。这样答面试官会认为你真正处理过生产问题。6.5 核心线程数怎么设置这是经典开放题没有标准答案但面试官想看你的权衡。CPU 密集型任务核心线程数可以设置为 CPU 核数 1因为这类任务主要在计算线程太多反而增加上下文切换。IO 密集型任务核心线程数可以设置得大一些比如 CPU 核数 * 2因为线程在等待 IO 时会释放 CPU允许更多线程在阻塞期间交替运行。更稳妥的方式结合压测调整设置合理的队列容量和拒绝策略观察 CPU 使用率、线程数、任务积压情况后再调参。不要背死公式。更高质量的答案是线上服务有压测环境通过阶梯式调整找到最优参数同时给每个线程池配置监控告警。6.6 线程池监控生产环境中线程池暴露的问题往往滞后。建议至少记录以下指标当前线程数、核心线程数、最大线程数。活跃线程数。队列积压任务数。已执行任务数、拒绝任务数。可以通过 ThreadPoolExecutor 的 getPoolSize()、getActiveCount()、getQueue().size()、getCompletedTaskCount() 等方法来采集指标配合监控平台上报或者简单地定时打印日志。7. CompletableFuture 与异步编程7.1 为什么面试开始问 CompletableFuture很多项目已经在用 CompletableFuture 做异步编排比如一个接口需要并行调用用户服务、订单服务、商品服务最后聚合结果返回。相比 FutureTaskCompletableFuture 支持回调、组合、异常恢复代码更简洁。面试问 CompletableFuture考的不是 API 背诵而是怎么把多个异步结果组合起来以及async 后缀的方法到底用的哪个线程池。7.2 核心场景示例并行查询然后聚合import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class CompletableFutureDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFutureString userFuture CompletableFuture.supplyAsync(() - { // 模拟远程调用用户服务 return 用户信息; }); CompletableFutureString orderFuture CompletableFuture.supplyAsync(() - { // 模拟远程调用订单服务 return 订单信息; }); CompletableFutureString combineFuture userFuture .thenCombine(orderFuture, (userInfo, orderInfo) - userInfo orderInfo); System.out.println(combineFuture.get()); } }thenCombine 会把两个异步结果合并后做进一步处理。如果是多个独立任务同时执行还可以使用 allOf 等待全部完成anyOf 等待任意一个完成。异常处理用 exceptionally 或 handle。7.3 默认线程池的坑CompletableFuture.supplyAsync 不传线程池时使用的是 ForkJoinPool.commonPool()。在大批量异步任务场景中commonPool 的线程数等于 CPU 核心数减一任务多时容易排队而且如果任务里有阻塞操作很容易把公共线程池打满影响 JVM 内其他使用 commonPool 的组件。所以生产环境建议统一使用自定义线程池别依赖默认实现。这几乎是我见过的线上异步问题中出现频率最高的一条。ExecutorService asyncPool new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(500), new ThreadPoolExecutor.CallerRunsPolicy() ); CompletableFuture.supplyAsync(() - { // 业务逻辑 return result; }, asyncPool);8. 死锁代码复现、排查与预防8.1 死锁产生的四个必要条件面试常问死锁条件但更常问的是线上怎么排查死锁。先记四个条件互斥资源同时只能被一个线程占用。持有并等待线程持有一个资源同时等待另一个资源。不可剥夺资源只能由持有者主动释放。循环等待多个线程形成环形等待链。预防的核心是破坏其中一个条件实际工程中最常用的是破坏循环等待即对所有锁按固定顺序加锁。8.2 死锁代码复现public class DeadLockDemo { private static final Object LOCK_A new Object(); private static final Object LOCK_B new Object(); public static void main(String[] args) { new Thread(() - { synchronized (LOCK_A) { System.out.println(线程1 持有 LOCK_A等待 LOCK_B); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } synchronized (LOCK_B) { System.out.println(线程1 获取到 LOCK_B); } } }, thread-1).start(); new Thread(() - { synchronized (LOCK_B) { System.out.println(线程2 持有 LOCK_B等待 LOCK_A); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } synchronized (LOCK_A) { System.out.println(线程2 获取到 LOCK_A); } } }, thread-2).start(); } }这段代码运行后两个线程会互相持有对方需要的锁程序卡住不会打印后面的信息。如果加上 sleep 让两个线程先分别拿到一把锁死锁的概率会大大增加实际生产中也有类似时序问题。8.3 线上排查死锁的步骤首选三板斧使用jps找到 Java 进程 PID。使用jstack pid打印线程快照搜索 Found one Java-level deadlock。根据 jstack 输出的线程栈定位到具体代码行找出两个线程持锁和等锁的位置。jps -l jstack 12345实际输出中会明确标记类似 thread-1 持有锁对象并被 thread-2 等待同时 thread-2 持有另一个锁并被 thread-1 等待。根据线程栈中的类名、方法名和行号直接回到代码里调整加锁顺序。排查时需要注意死锁可能导致线上服务卡死但又不会崩溃如果只是少量线程死锁系统可能还有响应但线程数会持续增长CPU 占用不一定高。所以线上监控线程状态时要关注大量线程长期处于 BLOCKED 或 WAITING 的情况。9. 3天复习计划、答题框架与高频题清单9.1 3天冲刺计划很多同学的问题不是内容多而是不知道每天该干嘛。我按面试常考点做了一个可执行的分日安排。天数学习主题核心产出第1天基础概念 线程状态 创建方式 JMM volatile synchronized能画出线程状态转换图能讲清 volatile 与 synchronized 区别第2天Lock 与 AQS ThreadLocal 线程池 CompletableFuture能手动写线程池参数能说出拒绝策略选择能写出 CompletableFuture 组合代码第3天死锁排查 JUC 工具类 高频题模拟 手写代码训练能 jstack 排查死锁能完整回答高频连环题每天的学习方式建议是先读原理再手写代码最后口头讲一遍。口头讲是最容易发现知识空洞的环节讲不顺的地方就是需要补的地方。9.2 面试答题框架结论先行再展开原理面试答题有一个被验证有效的结构结论 原理 场景 踩坑。比如被问到 volatile 时结论volatile 保证可见性和有序性不保证原子性。原理写操作立即刷新主内存读操作从主内存读通过内存屏障禁止重排序。场景适合状态标志位、双重检查锁单例。踩坑不适合 count 这类复合操作要保证原子性需要用 AtomicInteger、synchronized 或 Lock。这个结构能让面试官快速 get 到你的答案重点也方便你自己组织语言。建议把每个高频考点都整理成结论-原理-场景-踩坑四行笔记。9.3 高频题自测清单下面这些是过去一年 Java 多线程面试中出现频率很高的题目可以作为第 3 天的自测列表问题回答要点线程有哪些状态BLOCKED 和 WAITING 区别六种状态等锁 vs 等通知volatile 能保证原子性吗不能只保证可见性和有序性synchronized 底层原理Monitor 监视器锁锁升级过程synchronized 与 ReentrantLock 区别是否可中断、超时、公平锁、Condition什么是 AQSvolatile state CLH 队列 CASThreadLocal 内存泄漏怎么解决key 弱引用、value 强引用使用后 remove线程池执行流程先核心再队列再最大最后拒绝线程池拒绝策略有哪些AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicyCountDownLatch 与 CyclicBarrier 区别等待方向、是否可以复用、BrokenBarrierException死锁怎么排查jps jstack搜索 deadlockCompletableFuture 的默认线程池ForkJoinPool.commonPool生产建议自定义每道题都要做到能对着人讲 2 到 3 分钟不卡壳能达到这个程度面试基本够用了。10. 工程最佳实践与后续学习路线10.1 多线程代码的工程建议第一创建线程池时一定要自定义线程名称。用 ThreadFactory 给每个线程起一个带业务含义的名字比如 order-async-pool-thread-1。线上排错时jstack 里能看到业务语义的线程名能瞬间定位问题归属如果全是 pool-1-thread-1排查效率会低很多。第二锁的粒度要尽量小。能用代码块就不用整个方法能用读写锁就不要一锁到底。锁的粒度会直接影响系统的吞吐量面试中聊到性能优化时这也是一个很实际的加分点。第三异步任务必须做超时控制和失败兜底。CompletableFuture.get() 如果不带超时时间可能因为下游服务慢导致线程无限等待。建议调用时加上超时时间并捕获 TimeoutException 做降级处理。第四共享变量优先考虑不可变对象。很多并发问题其实可以通过设计规避比如使用 final 字段、不暴露 setter、使用 ImmutableList 等。线程安全不只是加锁还包括尽可能消除共享可变状态。10.2 后续学习的优先级如果 3 天冲刺结束后还有时间建议按以下顺序深入学习再读一遍 JMM 规范和 happens-before 规则这是理解并发本质的钥匙。手写一个简单的线程池理解 Worker 和阻塞队列的协作。阅读 ReentrantLock、CountDownLatch 的源码理解 AQS 的独占和共享模式。学习并发容器比如 ConcurrentHashMap、CopyOnWriteArrayList因为面试很可能从多线程延伸到集合类。系统性看一遍 Java 并发编程实战中的活跃度与性能章节对死锁、饥饿、活锁的理解会更透彻。多线程不是背一背就能过的知识点它需要你真正想清楚数据在哪里共享、锁保护的是什么、线程之间如何协作。当你把这些问题想明白后面试题只是同一个知识网络在不同场景下的投影。最后提醒一句不管看了多少篇面试题整理一定要在面试前亲手把线程池、死锁、CompletableFuture 这三段代码敲一遍。面试时能写出干净、健壮的并发代码比任何口头回答都更有说服力。
返回列表