Java多线程面试题,几乎每年都是后端岗位的高频区。如果你正打算在8月准备Java面试,多线程这块不用追求把100道题全背完,更值得做的是把线程基础、锁、JUC、线程池、场景题和线上排查串成一条线。这篇文章按3天节奏来组织,适合准备Java后端面试的应届生、初级工程师,也适合想系统梳理并发知识的人。3天不夸张,关键是每天的范围要控制住,别第一天就钻进源码细节里出不来。
很多人准备多线程时有个误区:先背synchronized和volatile的区别,再背线程池参数,最后看两篇源码解析。背完感觉记住了,一到面试官问“你的项目里哪里用到了多线程,遇到问题怎么排查”,就答不上来。所以这篇文章不会只罗列面试题,而是按“基础 -> 原理 -> 工具 -> 场景 -> 排查”顺序拆,每个部分都告诉你复习到什么程度、怎么验证,以及面试时怎么组织语言。
1. 多线程面试到底考什么,值得花3天重新过一遍吗
1.1 面试官不是只想要你背结论
多线程是Java面试里一类比较“泛”的题目。它不像Redis、MySQL那样有明确边界,也不像算法题一样有标准答案。面试官问多线程,通常不是为了让你背出定义,而是想通过一系列追问,判断你有没有真正写过多线程代码,有没有在并发环境下遇到问题,有没有排查和治理的经验。
常见的情况是:面试官先问“创建线程有几种方式”,你答出4种,他会继续问“那线程池里的线程是怎么创建的”;你答出ThreadPoolExecutor,他又追问“核心线程数怎么设置”;你说按CPU核数设置,他又问“IO密集和CPU密集有什么区别”。这一连串追问并不是要为难你,而是要看你的知识是点状的还是链状的。
所以复习多线程,最重要的不是背题,而是把每个考点串起来。比如线程状态和锁有关系,锁和阻塞队列有关系,阻塞队列和线程池有关系,线程池和线上性能问题有关系。串起来之后,面试官无论从哪个点切入,你都能接住。
1.2 真实考点是下面四层
我把多线程面试内容拆成四层:
- 第一层是基础:线程创建方式、生命周期、JMM、volatile、synchronized、sleep与wait、线程中断。
- 第二层是锁与同步机制:ReentrantLock、公平锁、非公平锁、CAS、AQS、锁升级、死锁。
- 第三层是JUC工具与容器:ThreadLocal、ConcurrentHashMap、CopyOnWriteArrayList、阻塞队列、CountDownLatch、Semaphore、CyclicBarrier、Atomic类、CompletableFuture。
- 第四层是实战能力:线程池参数与调优、多线程调用外部接口、批量任务拆分、Spring Boot请求多线程问题、线上线程栈分析、OOM和死锁排查。
这四层对应面试题的难度。前两层是基础题,大部分面试都会问;第三层是进阶题,看你有没有系统用过;第四层是拉开差距的地方,应届生如果能把项目里的并发场景说清楚,会明显加分。
1.3 3天时间怎么分配才合理
3天不是把每天排满12小时。我更建议每天留出6到8小时,上午学新内容,下午动手跑代码,晚上把当天能答的题口头自测一遍。第1天覆盖基础、JMM和锁,第2天覆盖JUC工具、并发容器和线程池,第3天集中做场景题、项目串联和线上排查。每个部分单独拿出来都不算难,难的是把它们串成一条线。所以计划要有,但别卡太死。
2. 第1天上午先打底:线程创建、状态流转和JMM
2.1 创建线程的几种方式,别只说四种
网上最常见的答案是四种:继承Thread、实现Runnable、实现Callable、使用线程池。这个答案本身没错,但面试时如果只答到这里,面试官会觉得你只是背过。
更好的回答方式,是先给结论,再说明本质。Java里真正“创建线程”的操作只有一个,就是new Thread()之后调用start(),底层会调用本地方法创建操作系统线程。Runnable、Callable只是把“要执行的任务”抽象出来,线程池也只是对线程生命周期的复用。所以那些所谓“创建方式”,本质是“任务提交方式”。
基础代码平时还是要写一写。
// 方式1:继承Thread,不推荐,因为Java是单继承 class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } // 方式2:实现Runnable,更推荐 new Thread(() -> System.out.println("runnable"), "test-thread").start(); // 方式3:实现Callable,配合FutureTask获取结果 FutureTask<Integer> task = new FutureTask<>(() -> 1 + 2); new Thread(task).start(); Integer result = task.get();这里要注意的是,Callable和Runnable最大的区别是Callable有返回值,能抛受检异常。面试时如果提到FutureTask,最好能继续说一句:FutureTask.get()是阻塞方法,调用时会等待任务执行完成,所以不要在循环里频繁调用get(),否则会阻塞主线程。
2.2 线程状态切换和sleep/wait的本质区别
线程状态是Java多线程面试的必问题。我建议直接记住六种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。这里最容易被问的是RUNNABLE状态,Java里的RUNNABLE其实包含了操作系统层面的“运行中”和“就绪”两个状态,不要把它理解成只有CPU正在执行。
sleep和wait的区别也是高频。可以从三个维度答:
- 所属关系:sleep是Thread的静态方法,wait是Object的方法。
- 锁释放:sleep不释放锁,wait释放锁。
- 唤醒方式:sleep到时间自动醒,wait需要notify/notifyAll,或者带超时时间自动醒。
面试时如果只答这三条,已经算过关。但如果能补充一句“wait之所以设计在Object上,是因为它依赖对象的监视器锁,需要先拿到synchronized锁才能调用;sleep是线程自己的行为,不需要持有对象锁”,会明显显得理解更深。
还有一个容易混的概念是线程中断。interrupt()不是立刻把线程停下来,而是给线程打一个中断标记。具体怎么响应,由线程内部代码决定。如果线程正在sleep或wait,会抛出InterruptedException。所以写多线程代码时,不要用destroy()之类的方法停止线程,那不是正常方式;正常做法是用一个volatile标志位,或者通过interrupt()协作式中断。
2.3 JMM、可见性、有序性、原子性怎么串起来
JMM是Java内存模型。很多人一听这个就发怵,其实面试常考的就是三个问题:什么是可见性、什么是有序性、什么是原子性。
用一个很常见的例子说明:两个线程同时对一个int变量做i++,最后结果可能小于预期。因为i++不是原子操作,它分成“读取、加1、写回”三步,多个线程交错执行时会丢失更新。这就是原子性问题。
再看另一个例子:一个线程改了变量,另一个线程一直读不到最新值。因为变量可能被缓存在线程自己的工作内存中,没有及时刷新到主内存。这就是可见性问题。volatile关键字可以保证可见性,它告诉JVM,这个变量每次都从主内存读取,写入后也刷回主内存,但它不能保证复合操作的原子性。
有序性则和指令重排有关。编译器、CPU为了性能可能会调整指令执行顺序。单线程下不影响结果,多线程下可能出问题。volatile还有一个作用是禁止指令重排,所以双重检查锁单例里要用volatile修饰instance。
回答时建议把这三点放在一个场景里说,更容易让面试官觉得你理解了。比如:“一个计数器用普通int,多线程累加会丢更新;加volatile后,能保证读取可见性,但i++还是丢更新;要保证原子性,得用AtomicInteger或synchronized。”这个回答串起了可见性和原子性,比单纯背定义好很多。
3. 第1天下午:synchronized、volatile和锁到底怎么复习
3.1 synchronized的锁升级过程
synchronized是Java面试第一梯队考点。现在的回答已经不能只说“它给代码块加锁”了,至少要知道锁升级路径:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
偏向锁是同一个线程反复进入同步块时,减少CAS操作的锁。一旦出现其他线程竞争,会升级为轻量级锁。轻量级锁通过CAS尝试获取锁,如果失败,会自旋一会儿,自旋超过阈值或竞争太激烈,就会升级成重量级锁,也就是依赖操作系统互斥量的锁,线程会进入阻塞。
至于JDK 15之后引入了虚拟线程,以及偏向锁的一些变化,面试时不用背太细。只要把升级过程和“为什么会有升级设计”说清楚就行。设计逻辑很简单:为了平衡性能。多线程竞争不激烈时,用轻量手段;竞争激烈时,再切换到重量级,避免过度自旋消耗CPU。
3.2 ReentrantLock和synchronized的取舍
ReentrantLock和synchronized的对比也是必问。可以从这几个维度回答:
- ReentrantLock是JUC包下的API层面锁,synchronized是JVM层面内置锁。
- ReentrantLock支持公平锁,synchronized默认非公平。
- ReentrantLock支持中断响应,lockInterruptibly()。
- ReentrantLock支持超时获取锁,tryLock(timeout)。
- ReentrantLock支持多个条件队列,Condition可以让线程精确唤醒。
- 锁的释放方式不同,ReentrantLock需要手动unlock,synchronized自动释放。
补充一点:ReentrantLock的可重入性,意思是同一个线程可以多次获取同一把锁。synchronized也是可重入的。面试时提到可重入,最好顺带解释一句“防止同一个线程再次进入同步代码块时把自己锁死”。
什么时候用ReentrantLock?需要公平锁、超时等待、可中断或精确唤醒时。如果你的业务里只要简单的互斥,synchronized足够了。
3.3 CAS、AQS和LockSupport的关系
CAS是Compare And Swap,比较并交换。它通过比较内存当前值和预期值是否一致,一致才更新,整个过程是硬件级的原子操作。乐观锁思想的一种实现。AtomicInteger的incrementAndGet底层就是CAS。
CAS有个典型问题:ABA问题。变量从A变成B又变回A,CAS会认为没变过。解决方法可以用带版本号的AtomicStampedReference。这个知识点不算冷门,面试被问到的概率不低,至少要能说出ABA是什么。
AQS是AbstractQueuedSynchronizer。JUC里很多工具,如ReentrantLock、Semaphore、CountDownLatch,底层都是AQS。它的核心是维护一个volatile int state,以及一个FIFO等待队列。线程抢锁失败就进入队列排队,释放锁时唤醒队首线程。面试不一定要懂每个细节,但提到AQS时能说出“CLH队列、state状态、独占和共享模式”就算过关。
LockSupport是更底层的线程阻塞工具,park()和unpark(thread)可以挂起和唤醒线程。它和wait/notify的区别在于,LockSupport不需要先获取对象锁,unpark可以被先调用。理解LockSupport后,再看AQS的阻塞唤醒逻辑会顺很多。
4. 第2天:JUC工具和并发容器不能只背名字
4.1 ThreadLocal内存泄漏问题
ThreadLocal的作用是让每个线程持有自己的变量副本。典型场景是SimpleDateFormat、数据库连接、用户上下文。面试如果只问“ThreadLocal是什么”,已经太少见了,现在更常问“ThreadLocal为什么会导致内存泄漏”。
ThreadLocal的实现是每个Thread内部有一个ThreadLocalMap,Key是ThreadLocal对象,Value是业务数据。Key是弱引用,当外部ThreadLocal对象没有强引用时,Key可能被回收,但Value还存在,如果线程长时间存活且不调用remove,Value就泄漏了。尤其是线程池里的线程是长期复用的,泄漏风险更大。
回答时不要说“所以ThreadLocal不能用”,正确理解应该是:用完主动remove。最佳实践是在finally块中调用threadLocal.remove()。面试时如果能说清“为什么线程池环境下更容易泄漏”和“如何避免”,比单纯背弱引用概念更有亮点。
4.2 ConcurrentHashMap和HashMap、Hashtable的对比
这个对比几乎是必问。至少要覆盖:
- HashMap线程不安全,多线程put可能导致数据覆盖,JDK 7甚至可能出现死循环。
- Hashtable线程安全,但所有方法都用synchronized锁整张表,并发度低。
- ConcurrentHashMap并发度高,JDK 7用分段锁,JDK 8改用synchronized锁哈希桶头节点加CAS,锁粒度更细。
JDK 8里,ConcurrentHashMap在扩容和计数上做了很多优化,比如transfer协助迁移、CounterCell分散计数。面试不要求背到那么深,但至少要知道它比Hashtable高效在哪里。如果面试官追问“ConcurrentHashMap的size()是怎么算的”,你可以说“通过baseCount和CounterCell累加,在竞争激烈时用分区计数,减少CAS冲突”。
4.3 CountDownLatch、Semaphore、CyclicBarrier各解决什么问题
这三个工具很多人混在一起。要区分清楚。
CountDownLatch是倒计时门闩。一个或多个线程等待其他线程完成指定数量任务后再继续。比如主线程等5个子线程都执行完毕,再汇总结果。它是一次性的,计数归零后不能再复用。
CyclicBarrier是循环栅栏。多个线程互相等待,都在到达栅栏后放行。可以循环使用。它适合“多线程分阶段计算,每阶段结束后对齐一次”的场景。
Semaphore是信号量。控制同时访问某个资源的线程数量。比如一个接口最多允许10个线程同时调用外部接口,用Semaphore限流。
面试时每个工具最好能配一个业务场景。只背“CountDownLatch用于等待多个任务完成”太单薄,如果能说“我用CountDownLatch把一批用户数据分批查询完成后,再统一做汇总和导出”,会更有说服力。
4.4 阻塞队列和Future/CompletableFuture
阻塞队列是线程池的重要组成部分。常见的有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayQueue等。重点理解两个操作:put/take是阻塞的,offer/poll可以带超时。线程池里的workQueue,用来缓存暂时无法执行的任务。
Future代表异步执行结果,get()会阻塞等待结果。JDK 8之后推荐用CompletableFuture做异步编排,它可以串行执行thenApply、并行执行thenCombine、异常处理exceptionally等。如果项目里用了异步任务,面试时可以把CompletableFuture作为亮点讲。
一个常见的示例是:
CompletableFuture.supplyAsync(() -> queryUser()) .thenApply(user -> queryOrder(user.getId())) .thenAccept(order -> System.out.println(order));这里要注意的是,supplyAsync默认使用ForkJoinPool.commonPool(),如果任务里有IO阻塞,建议传入自定义线程池,避免公共线程池被占满。这又是一个能体现工程经验的细节。
5. 第3天:线程池和项目场景题是拉分点
5.1 线程池七参数和拒绝策略
线程池是Java后端面试的重头戏。建议直接背熟ThreadPoolExecutor的七个参数:
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:非核心线程空闲存活时间。
- unit:时间单位。
- workQueue:任务队列。
- threadFactory:线程工厂,可以自定义线程名。
- handler:拒绝策略。
执行顺序也要能说清楚:核心线程先执行任务;核心线程满了,任务进队列;队列满了,创建非核心线程;线程数达到maximumPoolSize,执行拒绝策略。
拒绝策略有四种。AbortPolicy抛异常;CallerRunsPolicy由提交任务的线程自己执行;DiscardPolicy直接丢弃;DiscardOldestPolicy丢弃队列中最老的任务。实际开发中,CallerRunsPolicy相对温和,但也可能导致提交任务的线程被拖慢,所以要看业务是否允许。
5.2 核心线程数和队列容量怎么设置
这个问题没有标准统一答案,面试官主要看你有没有依据。一般可以分两类说:
- CPU密集型任务,核心线程数建议接近CPU核数,比如N+1,避免过多线程争抢CPU。
- IO密集型任务,核心线程数可以适当放大,参考公式是CPU核数乘以(1 + 平均等待时间/平均工作时间),因为线程大部分时间在等待IO。
重点不在于公式精确,而在于说明:线程池参数要结合任务类型、队列容量、服务可用资源和对延迟的容忍度来评估,不要拍脑袋。比如队列设太小,容易被拒绝;队列设太大,任务积压会导致延迟升高,还可能OOM。如果线上接口峰值不稳定,宁可队列小一点,让部分任务失败重试,也不要把请求无限堆积在内存里。
示例配置:
ExecutorService pool = new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200), new CustomThreadFactory("batch-task"), new ThreadPoolExecutor.CallerRunsPolicy() );这个例子只表示一类配置思路。实际核心线程数、最大线程数、队列容量,要按接口压测结果和机器配置来调。
5.3 submit和execute的差别
线程池提交任务有两种常见方法。execute(Runnable)没有返回值,提交异常会直接抛出;submit(Callable/Runnable)返回Future,可以获取结果,但任务内的异常会被封装在Future.get()里抛出。如果通过submit提交任务又不主动get(),异常可能被吞掉,线上问题排查时会很难受。
所以一个经验是:提交任务时明确预期。不需要结果的用execute,或者submit后主动处理异常;需要结果的用submit,并且一定要处理Future.get()抛出的ExecutionException和InterruptedException。同时要注意,Future.get()会阻塞,如果大量任务同时get(),存在线程阻塞风险。
5.4 项目里用多线程调用外部接口,怎么回答才有亮点
“多线程调用外部接口”是热词里很常见的问题,也是项目场景题。面试官问这个,通常想考察你有没有踩过以下坑:
- 并发上去了,但外部接口QPS支撑不住,反而超时。
- 线程池参数乱设,导致内存或线程数暴涨。
- 调用外部接口没有设置超时,线程一直被IO阻塞。
- 没有失败重试机制,一批任务里有一个失败,影响整体结果。
可以这样组织回答:
“我在项目里用线程池批量调用外部接口。首先会根据外部接口的压测数据估算并发上限,比如对方最多支持20个并发,我就把核心线程数设为10到20;然后给每个请求设置连接超时和读取超时,避免线程长时间挂住;同时用Future收集结果,并对失败任务做重试,重试次数限制在2次以内;最后把线程池的队列和拒绝策略单独配置,满员时根据业务决定是快速失败还是由调用线程执行。”
这个回答里有并发评估、超时、重试、线程池参数、失败处理,足够覆盖大部分追问。面试官如果再问“你如何监控线程池”,可以说关注activeCount、queue.size、completedTaskCount和最大线程数是否触顶,配合日志和监控大盘告警。
6. 高频场景题:交替打印、多线程累加和Spring请求并发
6.1 两个线程交替打印数字,考察的是什么
交替打印原题大概是:线程A打印1、3、5,线程B打印2、4、6,交替输出1到10。很多人一听是线程通讯,第一反应是wait/notify,其实这个题目考察的是“怎么控制线程执行顺序”。
最简单粗暴的是用两个信号量或synchronized + 标志位。如果用ReentrantLock的Condition,可以精确唤醒。但要记住,手写代码不是目的,关键要解释为什么可以用Condition,以及park/unpark、volatile标志位各自适合什么场景。
面试时如果遇到场景题,一定要先问清楚限制条件。比如“两个线程交替打印”和“三个线程交替打印”难度不一样。“可以用锁吗”“不能用锁,只用线程池呢”“打印结果必须是严格顺序吗”,这些都要先确认。能主动确认边界,本身就是加分行为。
6.2 多线程累加为什么结果不对,怎么改成对的
经典题目:两个线程同时对count执行10000次++,结果明显小于20000。原因是count++不是原子操作。解决方式有三种:
- 用synchronized给累加方法加锁。
- 用AtomicInteger,底层CAS。
- 用LongAdder,高并发下性能更好,适合写多读少。
面试时建议把三种方式都答出来,并说明区别。AtomicInteger在低并发下简单好用,LongAdder在竞争激烈时通过分段控制减少CAS冲突。最后可以主动提一句“如果只是代码演示,AtomicInteger够了,生产环境还要结合业务选型”。
6.3 Spring Boot 请求是多线程吗
这个问题是热词里出现的。很多初学者会有困惑:Controller是单例Bean,那同时来多个请求,是排队处理还是并发处理?
答案是:在默认的Servlet容器下,比如内嵌Tomcat,每个HTTP请求由独立的线程处理。Tomcat会维护一个线程池,接受TCP连接后从线程池里取一个线程来处理请求。所以Controller虽然是单例对象,但多个请求是并行进入Controller方法的。这带来一个关键问题:Controller里如果有共享的可变状态,比如一个普通HashMap或SimpleDateFormat字段,多线程同时访问就有线程安全问题。
所以Spring MVC的推荐做法是Controller无状态,有状态信息尽量放在方法局部变量、请求参数或ThreadLocal中,并且ThreadLocal要记得清理。回答这个问题时,如果能延伸到“为什么Spring默认Controller是单例”以及“无状态Bean为什么更适合多线程”,效果会好很多。
7. 线上排查和避坑:这些坑面试也会问
7.1 先看日志、线程栈和资源占用
面试官经常问“线上线程池出问题,你怎么排查”。这个问题很能区分能力。基本路径可以这样:
- 先看现象:是接口变慢、CPU飙升、内存飙升,还是任务失败率升高。
- 再看日志:Exception堆栈、任务提交时间、失败频率、超时时间。
- 再看线程池指标:activeCount、queue.size、corePoolSize、maximumPoolSize、completedTaskCount。
- 最后看线程栈:用jps找到进程ID,用jstack导出线程快照,看线程处于RUNNABLE、BLOCKED、WAITING哪种状态。
JVM自带的工具其实就够用。
jps -l jstack <pid> > thread_dump.txt导出后重点搜索“java.lang.Thread.State”附近的内容。如果大量线程BLOCKED在同一个锁上,大概率是锁竞争;如果大量线程WAITING在某个条件上,可能是线程池等待队列或者异步任务等待;如果CPU高但堆栈显示在GC线程,可能不是并发代码问题,而是内存分配压力。
7.2 ThreadLocal不清理导致的OOM
热词里提到过OutOfMemoryError。多线程环境下,一个常见的OOM来源就是线程池里的ThreadLocal使用后没清理。原因是线程池线程存活时间长,ThreadLocalMap里的Value一直被强引用,无法回收。长期积累后,内存持续增长。排查时需要动态观察老年代和堆内存走势,同时查看线程栈里是否有ThreadLocalMap相关内容。
这类问题在面试时可以这样回答:“我会优先检查用ThreadLocal的代码有没有在finally中remove,尤其是线程池场景。没有清理的话,即使一次只存几十KB,几千个线程积累起来也可能触发OOM。”
7.3 环境问题也会卡住面试准备
热词里还有“源发行版 17 需要目标发行版 17”这类编译警告,它本身不算多线程题,但如果你准备面试时连代码都跑不起来,会很影响效率。常见的环境问题包括JDK版本和Maven编译版本不一致、Lombok和JDK版本不兼容、IDE缓存导致代码识别异常。
建议准备面试环境时统一JDK、Maven、Spring Boot版本。可以用一个简单的多线程Demo工程做验证,把线程池、ThreadLocal、CompletableFuture、CountDownLatch都放进去。能跑通之后再背诵概念,效果更好。遇到编译问题,先看JDK版本和Lombok版本,再看Maven的compiler配置,别一上来就怀疑代码逻辑。
8. 3天冲刺计划和自我验证清单
8.1 三天时间怎么分配
可以按下面的安排来执行。
第1天,基础与锁:
- 上午:线程创建、线程状态、JMM、可见性/有序性/原子性。
- 下午:synchronized、volatile、ReentrantLock、CAS、AQS。
- 晚上:手写一个交替打印、一个多线程累加,并口头复述锁升级过程。
第2天,JUC与线程池:
- 上午:ThreadLocal、ConcurrentHashMap、阻塞队列、CountDownLatch、Semaphore、CyclicBarrier、Atomic类。
- 下午:线程池七参数、执行流程、拒绝策略、CompletableFuture。
- 晚上:用线程池写一个批量任务Demo,输出执行耗时和结果。
第3天,场景与排查:
- 上午:回答Spring Boot请求多线程问题、多线程调用外部接口、死锁场景题。
- 下午:用jps、jstack做一次线程快照分析,模拟一次死锁或线程堆积。
- 晚上:把每个题目按“概念、原理、场景、坑”四段口头自测。
这个计划不求覆盖所有冷门题,但求把高频主线过一遍。如果你时间更紧,至少保证第1天和第2天内容完整,第3天以场景题和口头表达为主。
8.2 每天结束前的自测标准
复习效果不能靠“看过”来判断,要能口头答出来。建议每天结束时,闭上眼睛随机抽几个问题:
- 线程池执行任务的过程是什么?
- volatile和synchronized的区别是什么?
- ThreadLocal为什么可能内存泄漏?
- ReentrantLock和synchronized怎么选?
- 多个线程同时修改同一个变量,怎么保证正确性?
- 线上线程池满了,你从哪里看指标?
如果能不翻资料答出核心逻辑,并且能用项目里的例子补充,就说明当天合格。如果只能答出一两个关键词,第二天早上先复习薄弱点再推进。
8.3 最后一天晚上做什么
最后一天晚上不建议再学新东西。保持题感和表达状态更重要。你可以做三件事:
一是把每个考点整理成一页笔记,比如用表格写“锁方式、获取方式、是否公平、失败表现、适用场景”。二是找几个常见场景题,模拟面试官追问,比如一个线程池八股题,故意追问“队列满了怎么办,拒绝策略了怎么办,任务可以重试吗”。三是放松心态。多线程面试不是看谁背得多,而是看谁能在实际问题里把原理用起来。能连续回答“为什么”比单个答案正确更有价值。
如果面试中遇到没准备过的并发题,不要急。先拆题干,把变量、并发点、共享资源、操作原子性这四件事说清楚,再给出方案。大部分并发面试题都可以从“共享了什么,哪些操作不是原子的,如何加锁或减少共享”这三个角度切入。
踩过几次之后我的体会是:多线程复习真正重要的不是你背了多少题,而是你能不能把一次并发事故从日志、线程栈到代码逻辑串起来。3天把主线打通,至少能保证面试时遇到多线程问题不慌。把上面这些细节逐个验证一遍,比盲目看10篇源码解析更有用。