Java面试中必会的并发编程核心考点
面试官问完这五个并发问题你的薪资可能直接翻倍“Java并发编程是衡量一个程序员从初级走向高级的分水岭。”这句话在我多年的面试经历中被反复验证。很多候选人能流畅说出HashMap的扩容机制却对ThreadLocal的内存泄漏支支吾吾能背出线程池的七个参数却在被问及“如何优雅关闭线程池”时乱了阵脚。今天这篇长文就是为你量身定做的“面试必杀技”。不罗列概念只讲那些面试官真正想听到的、能体现深度的核心考点。你准备好了吗线程的本质不是“跑得快的任务”而是“被调度的轻量级进程”面试官的第一个问题往往很基础“Java创建线程有几种方式”如果你只答“继承Thread、实现Runnable、实现Callable”那只能拿到及格分。真正的高手会从操作系统层面切入线程是CPU调度的最小单位Java线程与内核线程是一一映射关系1:1模型而协程如Project Loom才是未来的方向。更要命的是线程的生命周期。很多人搞混了BLOCKED和WAITING状态的区别。BLOCKED发生在等待获取对象锁时synchronized而WAITING是调用了wait()/join()/park()后的主动让出。面试官常追问“sleep(0)有什么用”这涉及到线程上下文切换的主动让出时机——sleep(0)会触发一次线程调度但不会释放锁。而yield()仅仅是提示性让出并不可靠。一个经典陷阱start()和run()的区别。调用run()只是普通方法调用不启动新线程start()才真正创建线程并执行run()。但更深一层的问题是同一个线程能否两次调用start()不能因为线程状态会从NEW变为RUNNABLE第二次调用会抛出IllegalThreadStateException。synchronized的锁升级从偏向锁到重量级锁的完整路径“synchronized是重量级锁吗”——这个问题在Java 6之后完全过时了。JDK 1.6引入了锁升级机制偏向锁 → 轻量级锁 → 重量级锁。面试官想听的正是这个演进过程。偏向锁当一个线程多次竞争同一对象时对象头Mark Word中会记录线程ID无需CAS操作。如果无竞争效率极高。但一旦有其他线程尝试获取锁偏向锁就会撤销需要等待全局安全点升级为轻量级锁。轻量级锁通过CAS自旋来获取锁不会使线程阻塞。但自旋消耗CPU所以自旋次数默认10次是关键参数。如果自旋失败锁会膨胀为重量级锁——依赖于操作系统的mutex互斥量线程挂起进入BLOCKED状态上下文切换开销巨大。面试官常问“为什么锁升级不可逆”因为一旦升级为重量级锁Mark Word会被修改为指向重量级锁的指针再降级需要复杂的条件判断且获益场景有限所以锁只能升级不能降级但偏向锁可以被撤销后重新偏向。记住这个结论偏向锁→轻量级→重量级是单向的。另一个深坑锁对象是类对象还是实例对象对静态方法加锁相当于锁住了Class对象对实例方法加锁锁的是当前实例。如果你在同步块中锁住了非final的字符串常量恰好字符串常量池中有相同的String引用就会导致意想不到的死锁。务必使用final对象作为锁。volatile的可见性屏障你以为只是“轻量级同步”“volatile保证可见性但不保证原子性”——这句话背得再熟面试官也能用一道题戳破你的泡沫。比如下面的代码volatile int count 0; // 100个线程分别执行 count 100次最终结果小于10000这里count实际是三步操作读取→加一→写入。volatile只能保证每次读取都能看到最新写入的值但无法阻止多个线程同时读取到旧值。这就是复合操作的非原子性。所以volatile不适合做计数器必须用AtomicInteger或synchronized。更深层的考点是内存屏障。volatile写操作前插入StoreStore屏障写操作后插入StoreLoad屏障保证写后读的可见性volatile读操作后插入LoadLoad和LoadStore屏障。这保证了volatile变量不会被重排序到其他内存操作之后/之前。面试官还喜欢问JMM的happens-before规则中volatile的规则是什么对volatile变量的写操作happens-before于后续任意线程对这个变量的读操作。这意味着线程A写volatile变量v之后线程B读v则A在写v之前的所有共享变量修改都对B可见因为写v会刷新缓存读v会强制从主存读取。一个高频陷阱用volatile实现单例模式的双重检查锁定DCL。典型写法private volatile static Singleton instance;为什么需要volatile因为instance new Singleton()可能被重排序为1.分配内存 2.将引用指向内存 3.初始化对象。如果线程A执行了步骤2但未执行步骤3线程B判断instance!null直接返回使用未初始化的单例。volatile禁止了构造函数的重排序保证了对象完全初始化后引用才可见。CAS与ABA问题乐观锁的野望与救赎“无锁编程”四个字就能吸引面试官的目光。CASCompare And Swap是Java并发包的基石AtomicInteger、ReentrantLock的底层都依赖它。CAS操作包含三个操作数内存位置V、预期值A、新值B。仅当V的值等于A时才更新为B否则重试。但CAS有三个大坑。第一ABA问题。线程1读V为A线程2将V改为B再改回A线程1CAS成功——它没发现变量被改过。解决方法使用带版本号的AtomicStampedReference或AtomicMarkableReference。第二循环时间长开销大。CAS自旋如果长时间不成功CPU开销巨大。Java 8优化了自旋策略比如LongAdder将热点分散到Cell数组减少CAS的激烈程度。第三只能保证一个共享变量的原子操作。如果需要同时操作多个变量要么封装成对象用AtomicReference要么用锁。面试官经常问AtomicInteger的底层实现是什么答案Unsafe类的compareAndSwapInt方法直接操作内存。CAS本身由CPU硬件保证原子性CMPXCHG指令。记住CAS是乐观锁synchronized是悲观锁。在冲突较少时CAS性能远高于锁冲突严重时CAS自旋浪费CPU反而锁更优。扩展考点LongAdder与AtomicInteger的区别。LongAdder通过内部维护多个Cell变量分散热点最终求和。适合写多读少的场景而AtomicInteger适合并发量中等的计数器。AQS与ReentrantLock面试官最爱的“框架设计”“请你讲讲AQS的原理。”这个问题能过滤掉95%的应聘者。AbstractQueuedSynchronizerAQS是JUC的基石ReentrantLock、CountDownLatch、Semaphore等都基于它。核心思想通过一个int类型的state表示同步状态0表示未锁定1表示锁定可重入时1以及一个CLH变体队列双向链表来管理等待获取锁的线程。独占模式与共享模式ReentrantLock是独占模式只有一个线程能获取锁CountDownLatch是共享模式多个线程可以同时获取。AQS通过tryAcquire/tryAcquireShared等模板方法让子类实现自定义同步器。ReentrantLock分为公平锁和非公平锁。非公平锁在加锁时会先尝试一次CAS获取锁如果失败才进入队列排队这样可能导致插队但吞吐量更高。公平锁则严格按照FIFO顺序但频繁的线程切换可能降低性能。面试官常问“非公平锁真的非公平吗”非公平锁并不保证任何线程都能插队成功它只是给新来的线程一次机会如果恰好锁被释放时新线程正好CAS成功才会插队。本质上非公平锁的“不公平”程度有限。Condition与await/signal每个Condition对象维护一个等待队列与AQS同步队列不同。同一个ReentrantLock可以创建多个Condition对象用于精确唤醒特定线程比如生产者消费者模式中区分“满”和“空”的等待集。线程池的七大参数与拒绝策略别再背错核心线程数公式“一个CPU密集型任务核心线程数设多少”——很多人脱口而出N1或N2N是CPU核心数。但面试官希望听到的是需要根据任务类型、IO比例、系统资源综合判断。CPU密集型最佳线程数为N1因为偶尔有页缺失等原因可让其他线程补上IO密集型最佳线程数为2N等待IO时CPU可处理其他线程。更精确的计算公式线程数 CPU核心数 (1 平均等待时间/平均计算时间)。ThreadPoolExecutor的七个参数corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler。关键在于理解线程池的运行流程当提交任务时如果运行线程数corePoolSize直接创建≥corePoolSize加入workQueue队列满且线程数maximumPoolSize创建新线程队列满且线程数maximumPoolSize执行拒绝策略。拒绝策略AbortPolicy抛异常、CallerRunsPolicy调用线程执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃队列最老的任务。面试官会问如果使用CallerRunsPolicy会不会阻塞会因为任务由提交者线程执行提交者线程被阻塞就无法继续提交新任务这客观上起到了限流作用。一个常被忽略的问题如何优雅关闭线程池调用shutdown()拒绝新任务但已提交的任务继续执行或者shutdownNow()尝试中断正在执行的任务。更好的方式是用awaitTermination等待一段时间后强制关闭。线程池中核心线程是否可以超时回收可以调用allowCoreThreadTimeOut(true)让核心线程也受keepAliveTime限制。并发容器ConcurrentHashMap的演进“ConcurrentHashMap怎么保证线程安全”从JDK 7到JDK 8的变化是必考。JDK 7采用Segment分段锁默认16个Segment每个继承ReentrantLock将数据分片理论上最多支持16个线程同时写入。JDK 8放弃分段锁改用synchronized CAS对每个桶数组元素使用synchronized加锁只有冲突时才锁同时用CAS保证一些乐观操作如数组初始化。结构改为数组链表红黑树当链表长度≥8且数组长度≥64时转为红黑树以提高查找效率。size()方法的实现JDK 8先尝试无锁统计累加CounterCell数组的baseCount与cells值如果竞争激烈再尝试使用CounterCell分散热点最后加锁汇总。这体现了无锁优先锁兜底的设计思想。CopyOnWriteArrayList写入时复制一份新数组写操作加锁读操作无锁。适合读多写少的场景比如白名单、路由表。但内存占用高不适合大数据量。BlockingQueue的四种实现ArrayBlockingQueue有界数组公平/非公平锁、LinkedBlockingQueue默认Integer.MAX_VALUE无界、SynchronousQueue不存储元素直接传递、PriorityBlockingQueue优先级队列。面试官喜欢问SynchronousQueue和LinkedBlockingQueue的应用场景SynchronousQueue适合任务直接交给工作线程的线程池Executors.newCachedThreadPoolLinkedBlockingQueue适合负载稳定的生产消费场景。ForkJoin与CompletableFuture现代Java不可错过的异步利器ForkJoinPool将大任务拆解成小任务用工作窃取work-stealing算法平衡负载。每个线程有一个双端队列线程处理完自己的任务后可以从其他线程队列的尾部窃取任务。这种机制避免了线程竞争提升了CPU利用率。适合递归分治型计算如归并排序、斐波那契。CompletableFutureJava 8引入的异步编程利器弥补了Future的短板无法手动完成、无法组合。它实现了CompletionStage接口支持thenApply/thenCompose/whenComplete等回调组合。面试常问如何并行执行两个任务并合并结果答CompletableFuture.allOf(task1, task2).thenJoin();或者task1.thenCombine(task2, (a,b)-...);。高级用法supplyAsync可以指定线程池避免使用默认的ForkJoinPool.commonPool()导致任务阻塞公用线程。写在最后面试不只是背题而是展示系统思维面试官期望看到的是你对并发编程底层原理的清晰认知是从硬件到JVM再到代码的完整链路。加粗再强调一次不要死记硬背要理解为什么。比如synchronized锁升级目的是为了在低竞争下使用轻量级方案在高竞争下使用系统级的阻塞方案。再比如线程池核心思想是复用线程、控制资源、调优性能。把这篇文章读透把它变成你自己的话去面试——当你能够流畅地解释Mark Word的存储结构、AQS的CLH队列状态、CAS的ABA解决方案时面试官的眼神会告诉你这题你稳了。

相关新闻