你好我是fengxin_rou这是我的个人主页fengxin_rou的主页❄️欢迎查看我的专栏我的专栏《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWebAI的talis学习系统》、《苍穹外卖》目录前言一、Java 并发手撕题的考察核心与价值二、CAS 自旋锁无锁并发的底层实现2.1 CAS 原理与核心组件2.2 CAS 自旋锁的手写实现2.3 优缺点与面试扩展三、线程通信经典题交替打印与顺序执行3.1 线程通信基础synchronizedwait/notify3.2 两线程交替打印 0-100奇偶分离3.3 三线程顺序打印 ABC四、LRU 缓存数据结构与并发设计4.1 LRU 原理与数据结构选择4.2 LRU 缓存的手写实现4.3 优化与扩展五、面试手撕题通用技巧与避坑指南5.1 通用解题技巧5.2 常见坑点总结5.3 面试加分项结语前言Java 并发编程是后端开发的核心能力也是大厂面试的必考点。 手写实现类题目能直接考察开发者对底层原理的理解和代码功底。 本文详解 4 道最高频并发手撕题从原理到实现全面覆盖面试考点。一、Java 并发手撕题的考察核心与价值在多核 CPU 时代并发编程是充分利用硬件资源、提升系统吞吐量的关键技术。 面试中的并发手撕题并非考察代码背诵能力而是验证开发者对原子性、可见性、有序性三大并发特性的理解程度。本文覆盖的 4 道题目是近三年 Java 后端面试中出现频率最高的手撕题CAS 自旋锁考察无锁编程思想和 Unsafe 类的底层应用两线程交替打印奇偶考察 synchronized 锁与 wait/notify 线程通信机制三线程顺序打印 ABC考察多线程协调与执行顺序控制手写 LRU 缓存考察数据结构设计与并发安全基础这些题目不仅是面试的敲门砖其背后的技术原理更是 JUC 并发包、缓存系统、分布式协调等核心技术的基础。掌握这些题目能帮助开发者建立扎实的并发编程思维提升实际开发中的问题解决能力。二、CAS 自旋锁无锁并发的底层实现2.1 CAS 原理与核心组件CASCompare And Swap比较并交换是一种无锁原子操作通过硬件指令保证操作的原子性。 其核心逻辑是先比较内存中的值与期望值是否相等若相等则更新为新值否则不做任何操作。Java 中 CAS 操作依赖Unsafe类实现该类提供了一系列 native 方法直接操作内存。 同时共享变量必须用volatile修饰保证其内存可见性防止指令重排序导致的线程安全问题。2.2 CAS 自旋锁的手写实现以下是基于 CAS 实现的基础自旋锁代码修正了原笔记中的命名规范问题import sun.misc.Unsafe; import java.lang.reflect.Field; /** * CAS自旋锁基础实现 * 原理通过无限循环尝试CAS修改状态变量成功则获取锁 */ public class CasSpinLock { // 锁状态0表示未锁定1表示已锁定 private volatile int state; // Unsafe实例用于执行CAS操作 private static final Unsafe unsafe; // state变量在对象中的偏移量 private static final long stateOffset; static { try { // 通过反射获取Unsafe类的单例实例 Field field Unsafe.class.getDeclaredField(theUnsafe); field.setAccessible(true); unsafe (Unsafe) field.get(null); // 获取state变量的内存偏移量 stateOffset unsafe.objectFieldOffset( CasSpinLock.class.getDeclaredField(state) ); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(Unsafe初始化失败, e); } } /** * 获取锁自旋直到CAS成功 */ public void lock() { // 无限循环自旋尝试获取锁 for (;;) { // CAS操作比较当前state是否为0若是则改为1 if (unsafe.compareAndSwapInt(this, stateOffset, 0, 1)) { break; } } } /** * 释放锁直接将state置为0 * 由于state是volatile变量写操作会立即刷新到主内存 */ public void unlock() { state 0; } }2.3 优缺点与面试扩展优点无阻塞避免了线程上下文切换的开销在锁竞争不激烈的场景下性能优异。缺点不可重入同一线程多次获取锁会导致死锁、非公平等待时间长的线程可能无法优先获取锁、存在 ABA 问题、长时间自旋会消耗大量 CPU 资源。面试常见扩展可重入 CAS 自旋锁添加线程标识和重入计数器ABA 问题解决使用带版本号的 CAS 操作AtomicStampedReference自适应自旋根据历史自旋次数动态调整自旋时间三、线程通信经典题交替打印与顺序执行3.1 线程通信基础synchronizedwait/notifyJava 中线程间通信主要通过wait()、notify()和notifyAll()方法实现这些方法必须在synchronized同步块中调用。wait()释放当前持有的锁使线程进入等待状态notify()随机唤醒一个等待该锁的线程notifyAll()唤醒所有等待该锁的线程重要注意事项必须使用while循环检查等待条件而不是if语句。 这是为了防止虚假唤醒—— 线程可能在没有被notify()的情况下被唤醒此时条件可能仍不满足需要重新检查。3.2 两线程交替打印 0-100奇偶分离需求两个线程交替打印 0 到 100 的整数一个线程只打印奇数另一个线程只打印偶数。/** * 两线程交替打印奇偶数字 * 核心通过共享计数器和对象锁实现线程同步 */ public class PrintOddEven { // 共享计数器 private int count 0; // 锁对象必须是引用类型才能调用wait/notify方法 private final Object lock new Object(); /** * 打印奇数的方法 */ public void printOdd() { while (count 100) { synchronized (lock) { // 等待count变为奇数 while (count % 2 0) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } // 二次检查边界条件防止count超过100 if (count 100) { lock.notify(); return; } System.out.println(Thread.currentThread().getName() : count); count; // 唤醒等待的偶数线程 lock.notify(); } } } /** * 打印偶数的方法 */ public void printEven() { while (count 100) { synchronized (lock) { // 等待count变为偶数 while (count % 2 ! 0) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } // 二次检查边界条件 if (count 100) { lock.notify(); return; } System.out.println(Thread.currentThread().getName() : count); count; // 唤醒等待的奇数线程 lock.notify(); } } } public static void main(String[] args) { PrintOddEven printer new PrintOddEven(); new Thread(printer::printOdd, 奇数线程).start(); new Thread(printer::printEven, 偶数线程).start(); } }关键坑点说明边界条件检查必须放在wait()之后因为当 count100 时线程可能从wait()位置被唤醒不会执行循环开头的判断必须处理InterruptedException并恢复线程的中断状态每次打印完成后必须调用notify()唤醒对方线程3.3 三线程顺序打印 ABC需求三个线程按顺序打印 A、B、C循环执行指定次数。java运行/** * 三线程顺序打印ABC * 核心通过共享标志位控制执行顺序 */ public class PrintABC { // 锁对象 private final Object lock new Object(); // 执行标志1表示打印A2表示打印B3表示打印C private int flag 1; // 循环打印次数 private static final int PRINT_TIMES 10; /** * 打印A的方法 */ public void printA() { for (int i 0; i PRINT_TIMES; i) { synchronized (lock) { // 等待flag变为1 while (flag ! 1) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } System.out.print(A); // 修改标志位允许B线程执行 flag 2; // 唤醒所有等待线程 lock.notifyAll(); } } } /** * 打印B的方法 */ public void printB() { for (int i 0; i PRINT_TIMES; i) { synchronized (lock) { // 等待flag变为2 while (flag ! 2) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } System.out.print(B); // 修改标志位允许C线程执行 flag 3; lock.notifyAll(); } } } /** * 打印C的方法 */ public void printC() { for (int i 0; i PRINT_TIMES; i) { synchronized (lock) { // 等待flag变为3 while (flag ! 3) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } System.out.println(C); // 修改标志位允许A线程执行 flag 1; lock.notifyAll(); } } } public static void main(String[] args) { PrintABC printer new PrintABC(); new Thread(printer::printA, A线程).start(); new Thread(printer::printB, B线程).start(); new Thread(printer::printC, C线程).start(); } }关键设计说明使用notifyAll()而不是notify()因为notify()只能随机唤醒一个线程可能唤醒的不是下一个需要执行的线程导致死锁通过循环控制打印次数避免无限循环标志位的修改必须在同步块内完成保证可见性和原子性四、LRU 缓存数据结构与并发设计4.1 LRU 原理与数据结构选择LRULeast Recently Used最近最少使用是一种经典的缓存淘汰策略。 当缓存容量达到上限时会淘汰最久未被访问的元素。为了实现高效的 LRU 缓存需要同时满足以下操作的 O (1) 时间复杂度查找元素使用 HashMap 实现插入元素使用双向链表实现头部插入删除元素使用双向链表实现 O (1) 删除数据结构设计HashMap存储键到节点的映射实现快速查找双向链表维护元素的访问顺序最近访问的元素放在头部最久未访问的放在尾部4.2 LRU 缓存的手写实现以下是基础 LRU 缓存的实现修正了原笔记中的构造方法名错误import java.util.HashMap; /** * LRU缓存实现 * 核心HashMap 双向链表 */ public class LRUCache { /** * 双向链表节点 */ static class Node { int key; int value; Node prev; Node next; public Node(int key, int value) { this.key key; this.value value; } } // 头节点和尾节点哨兵节点简化边界处理 private final Node head; private final Node tail; // 键到节点的映射 private final HashMapInteger, Node map; // 缓存容量 private final int capacity; // 当前缓存大小 private int size; /** * 构造方法初始化缓存 * param capacity 缓存容量 */ public LRUCache(int capacity) { this.capacity capacity; this.map new HashMap(capacity); this.size 0; // 初始化哨兵节点 head new Node(0, 0); tail new Node(0, 0); head.next tail; tail.prev head; } /** * 获取缓存值 * param key 键 * return 值不存在返回-1 */ public int get(int key) { Node node map.get(key); if (node null) { return -1; } // 将访问的节点移动到头部表示最近使用 moveToHead(node); return node.value; } /** * 存入缓存值 * param key 键 * param value 值 */ public void put(int key, int value) { Node node map.get(key); if (node ! null) { // 键已存在更新值并移动到头部 node.value value; moveToHead(node); return; } // 键不存在创建新节点 Node newNode new Node(key, value); map.put(key, newNode); addToHead(newNode); size; // 超过容量删除最久未使用的节点 if (size capacity) { Node removeNode removeTail(); map.remove(removeNode.key); size--; } } /** * 将节点添加到链表头部 */ private void addToHead(Node node) { node.next head.next; head.next.prev node; head.next node; node.prev head; } /** * 将节点移动到链表头部 */ private void moveToHead(Node node) { removeNode(node); addToHead(node); } /** * 删除指定节点 */ private void removeNode(Node node) { node.prev.next node.next; node.next.prev node.prev; } /** * 删除链表尾部节点最久未使用 * return 被删除的节点 */ private Node removeTail() { Node res tail.prev; removeNode(res); return res; } }4.3 优化与扩展线程安全优化简单实现在get和put方法上加synchronized锁高性能实现使用ConcurrentHashMap替代HashMap并对链表操作加细粒度锁JDK 内置实现 JDK 提供了LinkedHashMap类其本身就实现了 LRU 顺序。 通过重写removeEldestEntry方法可以轻松实现 LRU 缓存import java.util.LinkedHashMap; import java.util.Map; /** * 基于LinkedHashMap的LRU缓存实现 */ public class LinkedHashMapLRUCacheK, V extends LinkedHashMapK, V { private final int capacity; public LinkedHashMapLRUCache(int capacity) { // accessOrder设为true表示按访问顺序排序 super(capacity, 0.75f, true); this.capacity capacity; } /** * 当缓存大小超过容量时返回true删除最久未使用的元素 */ Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() capacity; } }应用场景Redis 的 LRU 淘汰策略MySQL 的 InnoDB 缓冲池本地缓存框架如 Caffeine五、面试手撕题通用技巧与避坑指南5.1 通用解题技巧先理思路再写代码面试时不要急于动笔先向面试官说明你的解题思路确认理解正确后再开始写代码。明确边界条件提前考虑各种边界情况如缓存容量为 1、线程数量为 1、打印到最后一个数等。选择合适的并发机制短时间的锁竞争适合用 CAS复杂的同步场景适合用synchronized或ReentrantLock。代码规范使用驼峰命名法添加必要的注释保持代码结构清晰。边写边解释写代码的同时向面试官解释每一步的作用展示你的思考过程。5.2 常见坑点总结虚假唤醒永远使用while循环检查等待条件不要用if语句。可见性问题共享变量必须用volatile修饰或者在锁内访问。死锁问题避免多个线程互相等待对方释放锁多线程协调时优先使用notifyAll。资源泄漏删除元素时不要忘记从 HashMap 中移除对应的键值对防止内存泄漏。异常处理必须处理InterruptedException并恢复线程的中断状态。5.3 面试加分项指出基础实现的不足并提出优化方案讲解技术背后的底层原理如 CAS 的硬件实现、synchronized 的锁升级过程结合实际项目经验说明这些技术在生产环境中的应用主动扩展问题如如何实现公平的自旋锁、如何实现 LRU-K 淘汰策略结语本文详解了 Java 并发面试中 4 道最高频的手撕题从底层原理到代码实现覆盖了 CAS、线程通信、数据结构等核心知识点。 掌握这些题目不仅能帮助你顺利通过面试更能加深对并发编程的理解提升实际开发能力。 进阶学习可以深入研究 JUC 包的源码了解 AQS、线程池、并发容器等高级特性构建完整的并发编程知识体系。
Java并发手撕题详解:原理、实现与面试避坑指南
发布时间:2026/6/5 1:14:00
你好我是fengxin_rou这是我的个人主页fengxin_rou的主页❄️欢迎查看我的专栏我的专栏《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWebAI的talis学习系统》、《苍穹外卖》目录前言一、Java 并发手撕题的考察核心与价值二、CAS 自旋锁无锁并发的底层实现2.1 CAS 原理与核心组件2.2 CAS 自旋锁的手写实现2.3 优缺点与面试扩展三、线程通信经典题交替打印与顺序执行3.1 线程通信基础synchronizedwait/notify3.2 两线程交替打印 0-100奇偶分离3.3 三线程顺序打印 ABC四、LRU 缓存数据结构与并发设计4.1 LRU 原理与数据结构选择4.2 LRU 缓存的手写实现4.3 优化与扩展五、面试手撕题通用技巧与避坑指南5.1 通用解题技巧5.2 常见坑点总结5.3 面试加分项结语前言Java 并发编程是后端开发的核心能力也是大厂面试的必考点。 手写实现类题目能直接考察开发者对底层原理的理解和代码功底。 本文详解 4 道最高频并发手撕题从原理到实现全面覆盖面试考点。一、Java 并发手撕题的考察核心与价值在多核 CPU 时代并发编程是充分利用硬件资源、提升系统吞吐量的关键技术。 面试中的并发手撕题并非考察代码背诵能力而是验证开发者对原子性、可见性、有序性三大并发特性的理解程度。本文覆盖的 4 道题目是近三年 Java 后端面试中出现频率最高的手撕题CAS 自旋锁考察无锁编程思想和 Unsafe 类的底层应用两线程交替打印奇偶考察 synchronized 锁与 wait/notify 线程通信机制三线程顺序打印 ABC考察多线程协调与执行顺序控制手写 LRU 缓存考察数据结构设计与并发安全基础这些题目不仅是面试的敲门砖其背后的技术原理更是 JUC 并发包、缓存系统、分布式协调等核心技术的基础。掌握这些题目能帮助开发者建立扎实的并发编程思维提升实际开发中的问题解决能力。二、CAS 自旋锁无锁并发的底层实现2.1 CAS 原理与核心组件CASCompare And Swap比较并交换是一种无锁原子操作通过硬件指令保证操作的原子性。 其核心逻辑是先比较内存中的值与期望值是否相等若相等则更新为新值否则不做任何操作。Java 中 CAS 操作依赖Unsafe类实现该类提供了一系列 native 方法直接操作内存。 同时共享变量必须用volatile修饰保证其内存可见性防止指令重排序导致的线程安全问题。2.2 CAS 自旋锁的手写实现以下是基于 CAS 实现的基础自旋锁代码修正了原笔记中的命名规范问题import sun.misc.Unsafe; import java.lang.reflect.Field; /** * CAS自旋锁基础实现 * 原理通过无限循环尝试CAS修改状态变量成功则获取锁 */ public class CasSpinLock { // 锁状态0表示未锁定1表示已锁定 private volatile int state; // Unsafe实例用于执行CAS操作 private static final Unsafe unsafe; // state变量在对象中的偏移量 private static final long stateOffset; static { try { // 通过反射获取Unsafe类的单例实例 Field field Unsafe.class.getDeclaredField(theUnsafe); field.setAccessible(true); unsafe (Unsafe) field.get(null); // 获取state变量的内存偏移量 stateOffset unsafe.objectFieldOffset( CasSpinLock.class.getDeclaredField(state) ); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(Unsafe初始化失败, e); } } /** * 获取锁自旋直到CAS成功 */ public void lock() { // 无限循环自旋尝试获取锁 for (;;) { // CAS操作比较当前state是否为0若是则改为1 if (unsafe.compareAndSwapInt(this, stateOffset, 0, 1)) { break; } } } /** * 释放锁直接将state置为0 * 由于state是volatile变量写操作会立即刷新到主内存 */ public void unlock() { state 0; } }2.3 优缺点与面试扩展优点无阻塞避免了线程上下文切换的开销在锁竞争不激烈的场景下性能优异。缺点不可重入同一线程多次获取锁会导致死锁、非公平等待时间长的线程可能无法优先获取锁、存在 ABA 问题、长时间自旋会消耗大量 CPU 资源。面试常见扩展可重入 CAS 自旋锁添加线程标识和重入计数器ABA 问题解决使用带版本号的 CAS 操作AtomicStampedReference自适应自旋根据历史自旋次数动态调整自旋时间三、线程通信经典题交替打印与顺序执行3.1 线程通信基础synchronizedwait/notifyJava 中线程间通信主要通过wait()、notify()和notifyAll()方法实现这些方法必须在synchronized同步块中调用。wait()释放当前持有的锁使线程进入等待状态notify()随机唤醒一个等待该锁的线程notifyAll()唤醒所有等待该锁的线程重要注意事项必须使用while循环检查等待条件而不是if语句。 这是为了防止虚假唤醒—— 线程可能在没有被notify()的情况下被唤醒此时条件可能仍不满足需要重新检查。3.2 两线程交替打印 0-100奇偶分离需求两个线程交替打印 0 到 100 的整数一个线程只打印奇数另一个线程只打印偶数。/** * 两线程交替打印奇偶数字 * 核心通过共享计数器和对象锁实现线程同步 */ public class PrintOddEven { // 共享计数器 private int count 0; // 锁对象必须是引用类型才能调用wait/notify方法 private final Object lock new Object(); /** * 打印奇数的方法 */ public void printOdd() { while (count 100) { synchronized (lock) { // 等待count变为奇数 while (count % 2 0) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } // 二次检查边界条件防止count超过100 if (count 100) { lock.notify(); return; } System.out.println(Thread.currentThread().getName() : count); count; // 唤醒等待的偶数线程 lock.notify(); } } } /** * 打印偶数的方法 */ public void printEven() { while (count 100) { synchronized (lock) { // 等待count变为偶数 while (count % 2 ! 0) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } // 二次检查边界条件 if (count 100) { lock.notify(); return; } System.out.println(Thread.currentThread().getName() : count); count; // 唤醒等待的奇数线程 lock.notify(); } } } public static void main(String[] args) { PrintOddEven printer new PrintOddEven(); new Thread(printer::printOdd, 奇数线程).start(); new Thread(printer::printEven, 偶数线程).start(); } }关键坑点说明边界条件检查必须放在wait()之后因为当 count100 时线程可能从wait()位置被唤醒不会执行循环开头的判断必须处理InterruptedException并恢复线程的中断状态每次打印完成后必须调用notify()唤醒对方线程3.3 三线程顺序打印 ABC需求三个线程按顺序打印 A、B、C循环执行指定次数。java运行/** * 三线程顺序打印ABC * 核心通过共享标志位控制执行顺序 */ public class PrintABC { // 锁对象 private final Object lock new Object(); // 执行标志1表示打印A2表示打印B3表示打印C private int flag 1; // 循环打印次数 private static final int PRINT_TIMES 10; /** * 打印A的方法 */ public void printA() { for (int i 0; i PRINT_TIMES; i) { synchronized (lock) { // 等待flag变为1 while (flag ! 1) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } System.out.print(A); // 修改标志位允许B线程执行 flag 2; // 唤醒所有等待线程 lock.notifyAll(); } } } /** * 打印B的方法 */ public void printB() { for (int i 0; i PRINT_TIMES; i) { synchronized (lock) { // 等待flag变为2 while (flag ! 2) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } System.out.print(B); // 修改标志位允许C线程执行 flag 3; lock.notifyAll(); } } } /** * 打印C的方法 */ public void printC() { for (int i 0; i PRINT_TIMES; i) { synchronized (lock) { // 等待flag变为3 while (flag ! 3) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断, e); } } System.out.println(C); // 修改标志位允许A线程执行 flag 1; lock.notifyAll(); } } } public static void main(String[] args) { PrintABC printer new PrintABC(); new Thread(printer::printA, A线程).start(); new Thread(printer::printB, B线程).start(); new Thread(printer::printC, C线程).start(); } }关键设计说明使用notifyAll()而不是notify()因为notify()只能随机唤醒一个线程可能唤醒的不是下一个需要执行的线程导致死锁通过循环控制打印次数避免无限循环标志位的修改必须在同步块内完成保证可见性和原子性四、LRU 缓存数据结构与并发设计4.1 LRU 原理与数据结构选择LRULeast Recently Used最近最少使用是一种经典的缓存淘汰策略。 当缓存容量达到上限时会淘汰最久未被访问的元素。为了实现高效的 LRU 缓存需要同时满足以下操作的 O (1) 时间复杂度查找元素使用 HashMap 实现插入元素使用双向链表实现头部插入删除元素使用双向链表实现 O (1) 删除数据结构设计HashMap存储键到节点的映射实现快速查找双向链表维护元素的访问顺序最近访问的元素放在头部最久未访问的放在尾部4.2 LRU 缓存的手写实现以下是基础 LRU 缓存的实现修正了原笔记中的构造方法名错误import java.util.HashMap; /** * LRU缓存实现 * 核心HashMap 双向链表 */ public class LRUCache { /** * 双向链表节点 */ static class Node { int key; int value; Node prev; Node next; public Node(int key, int value) { this.key key; this.value value; } } // 头节点和尾节点哨兵节点简化边界处理 private final Node head; private final Node tail; // 键到节点的映射 private final HashMapInteger, Node map; // 缓存容量 private final int capacity; // 当前缓存大小 private int size; /** * 构造方法初始化缓存 * param capacity 缓存容量 */ public LRUCache(int capacity) { this.capacity capacity; this.map new HashMap(capacity); this.size 0; // 初始化哨兵节点 head new Node(0, 0); tail new Node(0, 0); head.next tail; tail.prev head; } /** * 获取缓存值 * param key 键 * return 值不存在返回-1 */ public int get(int key) { Node node map.get(key); if (node null) { return -1; } // 将访问的节点移动到头部表示最近使用 moveToHead(node); return node.value; } /** * 存入缓存值 * param key 键 * param value 值 */ public void put(int key, int value) { Node node map.get(key); if (node ! null) { // 键已存在更新值并移动到头部 node.value value; moveToHead(node); return; } // 键不存在创建新节点 Node newNode new Node(key, value); map.put(key, newNode); addToHead(newNode); size; // 超过容量删除最久未使用的节点 if (size capacity) { Node removeNode removeTail(); map.remove(removeNode.key); size--; } } /** * 将节点添加到链表头部 */ private void addToHead(Node node) { node.next head.next; head.next.prev node; head.next node; node.prev head; } /** * 将节点移动到链表头部 */ private void moveToHead(Node node) { removeNode(node); addToHead(node); } /** * 删除指定节点 */ private void removeNode(Node node) { node.prev.next node.next; node.next.prev node.prev; } /** * 删除链表尾部节点最久未使用 * return 被删除的节点 */ private Node removeTail() { Node res tail.prev; removeNode(res); return res; } }4.3 优化与扩展线程安全优化简单实现在get和put方法上加synchronized锁高性能实现使用ConcurrentHashMap替代HashMap并对链表操作加细粒度锁JDK 内置实现 JDK 提供了LinkedHashMap类其本身就实现了 LRU 顺序。 通过重写removeEldestEntry方法可以轻松实现 LRU 缓存import java.util.LinkedHashMap; import java.util.Map; /** * 基于LinkedHashMap的LRU缓存实现 */ public class LinkedHashMapLRUCacheK, V extends LinkedHashMapK, V { private final int capacity; public LinkedHashMapLRUCache(int capacity) { // accessOrder设为true表示按访问顺序排序 super(capacity, 0.75f, true); this.capacity capacity; } /** * 当缓存大小超过容量时返回true删除最久未使用的元素 */ Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() capacity; } }应用场景Redis 的 LRU 淘汰策略MySQL 的 InnoDB 缓冲池本地缓存框架如 Caffeine五、面试手撕题通用技巧与避坑指南5.1 通用解题技巧先理思路再写代码面试时不要急于动笔先向面试官说明你的解题思路确认理解正确后再开始写代码。明确边界条件提前考虑各种边界情况如缓存容量为 1、线程数量为 1、打印到最后一个数等。选择合适的并发机制短时间的锁竞争适合用 CAS复杂的同步场景适合用synchronized或ReentrantLock。代码规范使用驼峰命名法添加必要的注释保持代码结构清晰。边写边解释写代码的同时向面试官解释每一步的作用展示你的思考过程。5.2 常见坑点总结虚假唤醒永远使用while循环检查等待条件不要用if语句。可见性问题共享变量必须用volatile修饰或者在锁内访问。死锁问题避免多个线程互相等待对方释放锁多线程协调时优先使用notifyAll。资源泄漏删除元素时不要忘记从 HashMap 中移除对应的键值对防止内存泄漏。异常处理必须处理InterruptedException并恢复线程的中断状态。5.3 面试加分项指出基础实现的不足并提出优化方案讲解技术背后的底层原理如 CAS 的硬件实现、synchronized 的锁升级过程结合实际项目经验说明这些技术在生产环境中的应用主动扩展问题如如何实现公平的自旋锁、如何实现 LRU-K 淘汰策略结语本文详解了 Java 并发面试中 4 道最高频的手撕题从底层原理到代码实现覆盖了 CAS、线程通信、数据结构等核心知识点。 掌握这些题目不仅能帮助你顺利通过面试更能加深对并发编程的理解提升实际开发能力。 进阶学习可以深入研究 JUC 包的源码了解 AQS、线程池、并发容器等高级特性构建完整的并发编程知识体系。