← 返回 Java核心图解 目录

☕ Java并发编程图解

synchronized锁升级 · volatile语义 · 线程池 · CAS与AQS · JUC三剑客 · Thread生命周期 · ThreadLocal

🔒 S1: synchronized锁升级全过程

synchronized = 公共厕所
🔒 无锁 → 走进去发现没人,直接用(偏向锁:贴上你的名字牌)
🔄 轻量锁 → 第二个人来了,你们在门口"谦让"(CAS自旋:不用蹲下等人出来,站着等就行)
⚖️ 重量锁 → 人太多了,谦让太累,保安来了(OS互斥量:排队拿号,蹲着等叫号)

为什么升级?因为大多数时候根本没人抢!如果一开始就叫保安(重量锁),太浪费了。先贴名字牌(偏向),人多了再谦让(轻量),真挤了才叫保安(重量)。
无锁 对象刚创建 Mark Word=001 偏向锁 第一个线程来 Mark Word=101 轻量锁 第二个线程来 Mark Word=00 重量锁 自旋失败 Mark Word=10 首次访问 第二个线程CAS 自旋>阈值 Mark Word 锁标志位变化: 🔒 无锁(001):hashcode + 年龄 → 没人用这把锁 🏷️ 偏向锁(101):线程ID + 年龄 → 贴了名字牌,同一个人反复进出 🔄 轻量锁(00):指向栈中Lock Record → 门口谦让(CAS自旋) ⚖️ 重量锁(10):指向Monitor对象 → 保安来了,排队等叫号
synchronized 锁升级流程:无锁 → 偏向 → 轻量 → 重量(只升级不降级)

偏向锁撤销:什么时候"名字牌"会被撕掉?

第二个线程尝试获取偏向锁时,需要等到全局安全点(STW)才能撤销偏向:

STW暂停 检查持有线程 还在用?→轻量锁 不用了?→无锁
批量重偏向:如果一个类的对象频繁撤销偏向(>20次),JVM会直接把新对象偏向到新线程,不再走撤销流程。
批量撤销:如果撤销更频繁(>40次),这个类直接禁用偏向锁,新对象一开始就是轻量锁。
SynchronizedLockUpgrade.java
📁 src/main/java
📁 concurrent
SynchronizedLockUpgrade.java
LockComparison.java
ThreadPoolDemo.java
public class SynchronizedLockUpgrade { // 对象头 Mark Word 结构(64位JVM) // |---------------------------------------------| // | 无锁: unused(25) | hashcode(31) | unused(1) | age(4) | 0(1) | 01(2) | // | 偏向锁: threadID(54) | epoch(2) | unused(1) | age(4) | 1(1) | 01(2) | // | 轻量锁: 指向栈中Lock Record的指针(62) | 00(2) | // | 重量锁: 指向Monitor对象的指针(62) | 10(2) | // |---------------------------------------------| private static Object lock = new Object(); public static void main(String[] args) throws Exception { // Step1: 无锁状态 — 对象刚创建 System.out.println(ClassLayout.parseInstance(lock).toPrintable()); // Step2: 偏向锁 — 只有一个线程反复进入 synchronized (lock) { System.out.println(ClassLayout.parseInstance(lock).toPrintable()); // Mark Word = 101 (偏向锁标志位) } // Step3: 轻量锁 — 第二个线程竞争 Thread t1 = new Thread(() -> { synchronized (lock) { System.out.println("t1持有锁"); } }); t1.start(); t1.join(); // Step4: 重量锁 — 自旋失败后膨胀 for (int i = 0; i < 100; i++) { new Thread(() -> { synchronized (lock) { // 大量竞争 → 重量锁 } }).start(); } } }

💨 S2: volatile关键字 — 可见性 + 禁止重排

volatile = 公告栏
没有volatile → 每个人在自己的草稿纸上记数据(CPU缓存),改了别人看不到
有了volatile → 数据写在公告栏上(主内存),一改所有人都看得到

禁止重排 = 烹饪菜谱的步骤不能乱:先放油→再放菜→最后放盐,不能先放盐再放油!
CPU为了快会偷偷换顺序(指令重排),volatile说:这条线之前/之后的操作不能跨过去
❌ 没有 volatile 线程A缓存 flag=false(旧) 线程B缓存 flag=false(旧) 主内存 flag=false A改了flag=true,B看不到! ✅ 有 volatile 线程A缓存 读主内存 线程B缓存 读主内存 主内存 flag=true A改了flag=true,B立即看到! 🔒 volatile 内存屏障(四大屏障) LoadLoad → Load1; LoadLoad; Load2 (Load1必须在Load2之前完成读) StoreStore → Store1; StoreStore; Store2 (Store1必须在Store2之前完成写) LoadStore → Load1; LoadStore; Store2 (读必须在写之前完成) StoreLoad → Store1; StoreLoad; Load2 (写必须在读之前完成) ↑ 这个最贵!volatile写后面会插StoreLoad
volatile 可见性:所有线程直接读写主内存(公告栏),不再用CPU缓存(草稿纸)

volatile 为什么不能保证原子性?

经典反例:i++ 不是原子操作!它分三步:读 → +1 → 写回

volatile int i = 0; 两个线程同时 i++ 线程A: 读i=0 → 计算0+1=1 → 写回1 线程B: 读i=0 → 计算0+1=1 → 写回1(覆盖了A的写入!) 结果: i=1 ❌ 期望: i=2 ✅ → 需要用 AtomicIntegerFieldUpdater 或 synchronized
volatile 适用场景:一个线程写、多个线程读(状态标志位 flag);
volatile 不适用:多线程同时写(i++、count++),需要用 AtomicInteger 或 synchronized。
VolatileVisibility.java
📁 src/main/java
📁 concurrent
SynchronizedLockUpgrade.java
VolatileVisibility.java
ThreadPoolDemo.java
public class VolatileVisibility { // ❌ 没有 volatile — 程序永远不会停止! private static boolean running = true; // ✅ 有 volatile — 主线程改了,子线程立即看到 private static volatile boolean runningV = true; public static void main(String[] args) throws Exception { // ✅ volatile版本:1秒后正常停止 Thread reader = new Thread(() -> { int count = 0; while (runningV) { // 每次都从主内存读 count++; } System.out.println("读线程停止,共循环 " + count); }); reader.start(); Thread.sleep(1000); runningV = false; // 写入主内存 + StoreLoad屏障 // reader线程会立即感知到 runningV == false reader.join(); } } // ⚡ DCL单例为什么要volatile? class Singleton { private static volatile Singleton INSTANCE; // ← 必须volatile! public static Singleton getInstance() { if (INSTANCE == null) { // 1.第一次检查 synchronized (Singleton.class) { // 2.加锁 if (INSTANCE == null) { // 3.第二次检查 INSTANCE = new Singleton(); // 4.创建对象 // ⚠️ new不是原子操作!分3步: // ① 分配内存空间 // ② 初始化对象 // ③ 引用指向内存地址 // 没volatile: ②③可能重排 → 别人拿到未初始化的对象! } } } return INSTANCE; } }

🏗️ S3: 线程池全解 — 7参数 + 4拒绝策略

线程池 = 任务处理工厂
⚙️ 核心线程(corePoolSize) = 常驻工作线程,始终待命
⚙️ 最大线程(maximumPoolSize) = 常驻+临时线程,高峰期扩容
存活时间(keepAliveTime) = 临时线程空闲多久后回收
📋 任务队列(workQueue) = 待处理任务的缓冲队列
🚫 拒绝策略(handler) = 线程和队列都满了怎么办

执行流程:提交任务→核心线程处理→都忙→入队列→队列满→创建临时线程→临时线程也满→执行拒绝策略
execute(task) 核心线程池 (corePoolSize=5) T1 T2 T3 T4 T5 任务队列 (workQueue, 容量=100) 📋📋📋📋📋... (排队的任务) 非核心线程 (max-core=5, 临时线程) T6 T7 T8 T9 T10 拒绝策略 (RejectedExecutionHandler) 🚫 队列满+线程满 → 怎么办? ①核心线程没满? ②核心满了→入队 ③队列满→创建非核心 ④都满→拒绝策略 四种拒绝策略: 1. AbortPolicy(默认)→ 直接抛 RejectedExecutionException(拒绝新任务!) 2. CallerRunsPolicy → 让提交任务的线程自己执行(调用者亲自干活!) 3. DiscardPolicy → 默默丢弃,不报错(悄悄扔了任务) 4. DiscardOldestPolicy → 丢弃队列最老的任务(把最早的任务扔了)
ThreadPoolExecutor 执行流程:核心线程 → 队列 → 非核心线程 → 拒绝策略
ThreadPoolDemo.java
📁 src/main/java
📁 concurrent
SynchronizedLockUpgrade.java
VolatileVisibility.java
ThreadPoolDemo.java
// ThreadPoolExecutor 7大参数详解 ThreadPoolExecutor pool = new ThreadPoolExecutor( 5, // ① corePoolSize: 核心线程数(常驻线程) 10, // ② maximumPoolSize: 最大线程数(正式+临时) 60, TimeUnit.SECONDS, // ③ keepAliveTime: 临时线程空闲多久后回收 new LinkedBlockingQueue(<>100), // ④ workQueue: 任务队列(容量100) Executors.defaultThreadFactory(), // ⑤ threadFactory: 创建线程的工厂 new ThreadPoolExecutor.CallerRunsPolicy() // ⑥ handler: 拒绝策略 ); // ⚠️ 阿里巴巴规范:不要用Executors创建线程池! // Executors.newFixedThreadPool() → 队列无界 → OOM! // Executors.newCachedThreadPool() → 最大线程=Integer.MAX_VALUE → OOM! // ✅ 手动创建,明确指定每个参数 // 💡 线程数怎么定? // CPU密集型: corePoolSize = CPU核数 + 1 // IO密集型: corePoolSize = CPU核数 * 2 (或 CPU核数 / (1-阻塞系数)) int cpuCores = Runtime.getRuntime().availableProcessors(); System.out.println("当前CPU核数: " + cpuCores);

🔄 S4: CAS与Atomic原子类

CAS = 抢红包
你打开红包 → 看到5元 → 点领取 → 系统检查"还是5元吗?"
✅ 还是5元 → 领走!钱变成0
❌ 已经被别人领了(变成0)→ 重新看下一个红包(自旋重试

ABA问题 = 红包被人领了5元,又有人发了5元,你看还是5元以为没变过!
解决:AtomicStampedReference = 红包加版本号("第3个红包"不是"第1个红包")
CAS (Compare And Swap) 流程 ① 读取当前值V V = 主内存中的值 ② 计算新值N N = V + 1 ③ CAS(V, E, N) V==E ? 写入N : 重试 ✅ V==E 写入N ❌ V≠E 自旋 ⚠️ ABA问题 Thread1: 读V=A → 被挂起 → 恢复后CAS(A,A,C) → ✅成功(但A中间被改过B又改回A!) 解决: AtomicStampedReference 加版本号 stamp → CAS(V,expectedV, newV, expectedStamp, newStamp)
CAS乐观锁:不加锁,先比较再写入,失败就重试(自旋)

Atomic原子类家族

类别代表类原理适用场景
基本类型AtomicInteger
AtomicLong
AtomicBoolean
CAS自旋计数器、状态标志
引用类型AtomicReference
AtomicStampedReference
CAS+版本号解决ABA问题
数组类型AtomicIntegerArray
AtomicReferenceArray
CAS数组元素并发数组操作
字段更新器AtomicIntegerFieldUpdater反射CAS已有类的字段原子化
累加器LongAdder
LongAccumulator
分段CAS高并发计数(比AtomicLong快)
LongAdder 为什么比 AtomicLong 快?
AtomicLong:所有线程CAS同一个value → 高并发时大量自旋失败
LongAdder:把value分成多个Cell → 不同线程CAS不同Cell → 最后sum()求和 → 减少了CAS冲突
类比:AtomicLong=一个收银台排队;LongAdder=多个收银台同时收,最后汇总

📐 S5: AQS原理 — JUC的基石

AQS = 银行取号系统
state = 柜台状态(0=空闲 / 1=有人正在办理 / N=重入次数)
CLH队列 = 取号后的等候区,按先来后到排队
独占模式(ReentrantLock) = 一个柜台只能一个人用
共享模式(Semaphore) = N个柜台可以N个人同时用

公平 vs 非公平
公平锁 = 严格按取号顺序叫号(新来的不能插队)
非公平锁 = 新来的人直接冲向柜台,有人就取号排着(默认,性能更好)
state (volatile int) 0=空闲 | 1=占用 | >1=重入 CLH 双向队列(FIFO等待区) Head(哨兵) Node-1 Node-2 Node-3 Tail 🔒 加锁流程 (ReentrantLock.lock()) ① CAS尝试把 state 从 0→1 ② 成功 → 设置exclusiveOwnerThread=当前线程 ③ 失败 → 加入CLH队列 → park()挂起 → 等待unpark() 🔓 解锁流程 (ReentrantLock.unlock()) ① state - 1(支持重入,减到0才算真正释放) ② state==0 → 设置exclusiveOwnerThread=null ③ unpark队列中下一个等待线程 → 它继续CAS竞争
AQS = state变量 + CLH双向队列,所有JUC锁的基础
AQSPrinciple.java
📁 src/main/java
📁 concurrent
AQSPrinciple.java
JUCSyncDemo.java
ThreadLifecycleDemo.java
// AQS 核心源码简化版 (java.util.concurrent.locks.AbstractQueuedSynchronizer) public abstract class AbstractQueuedSynchronizer { private volatile int state; // 同步状态(核心!) private volatile Node head; // CLH队列头 private volatile Node tail; // CLH队列尾 private transient volatile Thread exclusiveOwnerThread; // 独占线程 // 加锁核心:tryAcquire (子类实现) protected boolean tryAcquire(int arg) { int c = getState(); if (c == 0) { // state=0 → 无人持有 if (compareAndSetState(0, arg)) { // CAS抢锁 setExclusiveOwnerThread(Thread.currentThread()); return true; } } else if (Thread.currentThread() == getExclusiveOwnerThread()) { setState(c + arg); // 重入: state+1 return true; } return false; } // 解锁核心:tryRelease protected boolean tryRelease(int arg) { int c = getState() - arg; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // state减到0才算释放 free = true; setExclusiveOwnerThread(null); } setState(c); return free; } }

🎯 S6: JUC三剑客 — CountDownLatch / CyclicBarrier / Semaphore

三剑客类比
🔔 CountDownLatch = 游乐园关门等所有游客出来 → 一次性的,计数到0就开门,不能重置
🤝 CyclicBarrier = 同学聚会等人齐了再开饭 → 可循环的,人到齐了就出发,还能等下一批
🎫 Semaphore = 停车场的车位牌 → 只有N个牌,拿到牌才能停,还牌给别人停

记忆口诀:CountDown=倒计时(一锤子买卖) Cyclic=循环(可重复) Semaphore=信号量(限量供应)
🔔 CountDownLatch 游乐园关门等游客出来 游客A 游客B 游客C count = 3 → 2 → 1 → 0 count==0 → 门开了! ⚠️ 一次性,不能reset 🤝 CyclicBarrier 同学聚会等人齐开饭 小明 小红 小刚 人到齐了 → 开饭! 吃完还能等下一批! ✅ 可循环,自动reset 🎫 Semaphore 停车场车位牌 permits = 3 3个车位牌 acquire()拿牌 → release()还牌 没牌了就等着! 三者对比: CountDownLatch: 1个线程等N个线程 → countDown()减计数 / await()等待 CyclicBarrier: N个线程互相等 → await()等待到齐 → 自动触发barrierAction Semaphore: 控制并发数 → acquire()获取许可 / release()释放许可 ⚠️ 关键区别:CountDownLatch是1等N(主线程等子线程);CyclicBarrier是N互相等(线程间同步) 如果需要"等所有人到齐再做某事" → CyclicBarrier;如果需要"等所有任务完成再汇总" → CountDownLatch
JUC三剑客:CountDownLatch(倒计时) / CyclicBarrier(循环栅栏) / Semaphore(信号量)
JUCSyncDemo.java
📁 src/main/java
📁 concurrent
AQSPrinciple.java
JUCSyncDemo.java
ThreadLifecycleDemo.java
// ① CountDownLatch: 主线程等所有子任务完成 CountDownLatch latch = new CountDownLatch(3); // 3个子任务 for (int i = 0; i < 3; i++) { new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) {} System.out.println(Thread.currentThread().getName() + " 完成"); latch.countDown(); // 计数-1 }).start(); } latch.await(); // 主线程等计数归0 System.out.println("所有子任务完成,开始汇总!"); // ② CyclicBarrier: 线程间互相等待 CyclicBarrier barrier = new CyclicBarrier(3, () -> { System.out.println("=== 所有人到齐,出发! ==="); // 到齐后自动执行 }); for (int i = 0; i < 3; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 到了"); barrier.await(); // 等其他人到齐 System.out.println(Thread.currentThread().getName() + " 出发"); }).start(); } // ③ Semaphore: 控制并发访问数 Semaphore semaphore = new Semaphore(3); // 最多3个线程同时执行 for (int i = 0; i < 10; i++) { new Thread(() -> { try { semaphore.acquire(); // 拿车位牌 System.out.println(Thread.currentThread().getName() + " 停车"); Thread.sleep(2000); } finally { semaphore.release(); // 还车位牌 } }).start(); }

🔄 S7: Thread生命周期 — 6种状态转换

Thread生命周期 = 打工人的一天
🆕 NEW = 刚入职,还没上班
🏃 RUNNABLE = 在工位干活(可能正在做,也可能等CPU分配时间片)
🔒 BLOCKED = 等公共厕所(synchronized锁被别人占着)
WAITING = 在休息室等通知(wait()/join()/park(),不知道要等多久)
TIMED_WAITING = 设了闹钟午休(sleep(1000)/wait(1000),到时间自动醒)
💀 TERMINATED = 下班了(run()执行完毕或异常退出)
🆕 NEW start() 🏃 RUNNABLE 就绪 + 运行中 🔒 BLOCKED 等synchronized锁 ⏳ WAITING wait()/join()/park() ⏰ TIMED_WAITING sleep(ms)/wait(ms) 💀 TERMINATED run()结束 start() 等锁 获锁 wait() notify() sleep(ms) 超时 run()完成/异常
Thread 6种状态转换图(实线=进入,虚线=恢复)

BLOCKED vs WAITING vs TIMED_WAITING 三者区别

状态触发方式恢复方式是否释放锁类比
BLOCKED synchronized锁被占 锁释放自动竞争 ❌ 没锁可释放 等公共厕所
WAITING wait()/join()/park() notify()/unpark() ✅ 释放锁 休息室等通知
TIMED_WAITING sleep(ms)/wait(ms) 超时自动醒 sleep❌ / wait✅ 设闹钟午休
经典面试坑:sleep vs wait
sleep() → 不释放锁,Thread方法,不需要在synchronized中
wait() → 释放锁,Object方法,必须在synchronized中调用
共同点:都让线程暂停执行,都会响应interrupt()
ThreadLifecycleDemo.java
📁 src/main/java
📁 concurrent
AQSPrinciple.java
JUCSyncDemo.java
ThreadLifecycleDemo.java
public class ThreadLifecycleDemo { public static void main(String[] args) throws Exception { Object lock = new Object(); Thread t1 = new Thread(() -> { synchronized (lock) { try { System.out.println("t1: 我在wait了..."); lock.wait(); // → 进入WAITING,释放lock // lock.wait(3000); → 进入TIMED_WAITING System.out.println("t1: 被唤醒了!"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }, "等待线程"); Thread t2 = new Thread(() -> { synchronized (lock) { // t1在wait,t2拿到了lock → t1在WAITING System.out.println("t2: 我拿到锁了,3秒后唤醒t1"); try { Thread.sleep(3000); } // → TIMED_WAITING catch (InterruptedException e) {} lock.notify(); // 唤醒t1 → t1进入BLOCKED(等t2释放锁) // t2退出synchronized → t1获得锁 → RUNNABLE } }, "通知线程"); // 状态转换: NEW → start() → RUNNABLE → wait() → WAITING // → notify() → BLOCKED → 获锁 → RUNNABLE → run()结束 → TERMINATED System.out.println("t1状态: " + t1.getState()); // NEW t1.start(); Thread.sleep(100); System.out.println("t1状态: " + t1.getState()); // WAITING } }

📦 S8: ThreadLocal — 不共享,就不用锁

ThreadLocal = 每个人自己家有个厕所
synchronized = 公共厕所,大家排队用同一个(共享 → 互斥)
ThreadLocal = 每人自己家一个厕所,各用各的(不共享 → 不用锁)

核心思想:并发问题的根源是"多个线程改同一个变量"。
synchronized 的思路:大家还是改同一个,但要排队。
ThreadLocal 的思路:别共享了,给每个线程发一份自己的副本,各改各的,天然安全!
ThreadLocal 内部结构:每个线程有自己的 Map 🏃 Thread-1 ThreadLocalMap (Thread的成员变量!) Entry[] table Key=userTL → Value:张三 Key=dateTL → Value:fmt1 🏃 Thread-2 ThreadLocalMap (Thread的成员变量!) Entry[] table Key=userTL → Value:李四 Key=dateTL → Value:fmt2 📦 ThreadLocal 对象(全局共享,但是每个线程的Map里各有自己的Value) userTL.get() → Thread-1得到"张三",Thread-2得到"李四" → 天然隔离! ⚠️ 内存泄漏链(面试高频!) Key=WeakRef GC回收Key→null Value还在(强引用) 永远无法回收→泄漏!
ThreadLocal原理:Map是Thread的成员变量,不是ThreadLocal的 → 天然隔离;但Key弱引用被GC后Value还在 → 内存泄漏

synchronized vs ThreadLocal — 两种思路对比

维度🔒 synchronized📦 ThreadLocal
核心思路共享 → 互斥排队不共享 → 各用各的
类比公共厕所(排队用)自家厕所(各用各的)
是否加锁✅ 需要加锁❌ 不需要锁
适用场景必须操作同一个对象(扣余额、写同一张表)每个线程需要独立副本(用户上下文、日期格式化)
性能竞争激烈时慢(锁等待)无竞争,速度快
风险死锁、锁粗化内存泄漏(忘记remove)
典型代码synchronized(lock) { count++; }userTL.set(user); ... userTL.get();
选哪个?
• 如果必须操作同一个共享对象 → synchronized / Lock
• 如果每个线程只需要自己的副本 → ThreadLocal(零竞争)
生产中最常见的ThreadLocal场景:用户上下文(UserContext)、数据库连接、SimpleDateFormat、链路追踪TraceId
ThreadLocalUserContext.java
📁 src/main/java
📁 concurrent
SynchronizedLockUpgrade.java
VolatileVisibility.java
ThreadPoolDemo.java
ThreadLocalUserContext.java
// ✅ 生产场景1:用户上下文传递(最常见!) public class UserContext { private static final ThreadLocal<User> currentUser = new ThreadLocal<>(); public static void set(User user) { currentUser.set(user); } public static User get() { return currentUser.get(); } public static void clear() { currentUser.remove(); } // ⚠️ 必须! } // 拦截器:请求进来时设置用户,请求结束时清理 public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, ...) { User user = parseToken(req.getHeader("Authorization")); UserContext.set(user); // ← Thread-1设"张三", Thread-2设"李四" return true; } @Override public void afterCompletion(...) { UserContext.clear(); // ⚠️ 必须!否则线程池复用→脏数据+内存泄漏 } } // 任何地方都能拿到当前用户,不用层层传参! public class OrderService { public void createOrder(OrderDTO dto) { User user = UserContext.get(); // 直接拿,不用传参 order.setUserId(user.getId()); orderDao.save(order); } }
ThreadLocalDateFormat.java
📁 src/main/java
📁 concurrent
ThreadLocalUserContext.java
ThreadLocalDateFormat.java
ThreadLocalLeakDemo.java
// ✅ 生产场景2:SimpleDateFormat线程安全 // ❌ SimpleDateFormat不是线程安全的!多线程共享会出错 // private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // ↑ 多线程并发调用sdf.parse() → 数据错乱! // ✅ 用ThreadLocal:每个线程一份,各用各的 public class DateUtils { private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public static Date parse(String dateStr) throws ParseException { return sdf.get().parse(dateStr); // 每个线程拿到自己的实例 } public static String format(Date date) { return sdf.get().format(date); } } // ⚠️ JDK 8+ 更推荐 DateTimeFormatter(天然线程安全) private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // ↑ DateTimeFormatter是不可变对象,天生线程安全,不需要ThreadLocal
ThreadLocalLeakDemo.java
📁 src/main/java
📁 concurrent
ThreadLocalUserContext.java
ThreadLocalDateFormat.java
ThreadLocalLeakDemo.java
// ⚠️ 内存泄漏演示:线程池 + ThreadLocal = 重灾区! public class ThreadLocalLeakDemo { private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(5); for (int i = 0; i < 100; i++) { pool.submit(() -> { // ❌ 每次都set一个1MB的数组,但从不remove threadLocal.set(new byte[1024 * 1024]); // 1MB // ... 业务逻辑 ... // ❌ 忘了 remove()! // 线程池的线程被复用,ThreadLocalMap一直存在 // → 100次 × 1MB = 100MB 泄漏! }); } // ✅ 正确写法:必须在 finally 中 remove pool.submit(() -> { try { threadLocal.set(new byte[1024 * 1024]); // ... 业务逻辑 ... } finally { threadLocal.remove(); // ← 绝对不能忘! } }); } } // 💡 泄漏原理: // ThreadLocalMap.Entry extends WeakReference<ThreadLocal<?>> // → Key是弱引用(GC会回收),Value是强引用(GC不会回收) // → Key被GC回收变成null,但Value还被Entry引用着 // → 这个null→Value的Entry永远无法被访问到,也永远无法被GC回收 // → 这就是泄漏! // // 💡 为什么用WeakReference还要泄漏? // WeakReference只保证"ThreadLocal对象本身"能被回收 // 但Value不是WeakReference!Value是强引用! // → ThreadLocal对象被回收了,但Value还在Map里占着内存
⚠️ 线程池 + ThreadLocal = 内存泄漏重灾区!
线程池的线程会被复用,上一个请求set的ThreadLocal数据还在Map里:
脏数据:下一个请求的线程可能拿到上一个请求的用户信息!
内存泄漏:Value是强引用,永远不会被GC回收,越积越多直到OOM

铁律:使用完必须在finally中 remove()!
ThreadLocal 三原则速记

不共享就不用锁 → 每个线程自己一份副本,天然线程安全
用完必须remove() → 线程池场景尤其重要,否则脏数据+内存泄漏
Map是Thread的不是ThreadLocal的 → 这是隔离的关键,也是泄漏的根源

❓ 面试高频FAQ

synchronized和ReentrantLock的区别?
维度synchronizedReentrantLock
实现JVM层面(monitorenter/monitorexit)API层面(AQS)
锁类型非公平(不可选)默认非公平,可设公平
可中断不可lockInterruptibly()可中断
超时不可tryLock(timeout)可超时
条件变量只有一个waitSet多个Condition,精准唤醒
释放锁自动(代码块结束)必须手动unlock()(finally中)

选哪个?简单同步用synchronized(JVM优化好),需要高级功能(公平/可中断/多条件)用ReentrantLock。

synchronized锁升级能降级吗?
不能降级!一旦升级到重量锁,就不会再退回到轻量锁或偏向锁。这是JVM的设计决策——因为能升级到重量锁说明竞争激烈,降级也没有意义。

唯一的例外:批量撤销偏向锁后,新创建的对象直接从轻量锁开始(跳过偏向锁),但这不是"降级"而是"禁用偏向"。
volatile能保证线程安全吗?
不能完全保证!volatile只保证:
✅ 可见性:一个线程修改,其他线程立即可见
✅ 有序性:禁止指令重排
❌ 原子性:不保证复合操作(如i++)的原子性

经典场景
✅ volatile boolean running → 一个线程写,多个线程读 → 安全
❌ volatile int count; count++ → 多线程同时写 → 不安全(用AtomicInteger)
线程池核心线程会被回收吗?
默认不会!核心线程即使空闲也不会被回收(keepAliveTime只对非核心线程生效)。

但是可以设置:pool.allowCoreThreadTimeOut(true),这样核心线程空闲超过keepAliveTime后也会被回收。

原理:核心线程在getTask()中,如果allowCoreThreadTimeOut==false,会一直take()阻塞等待任务;如果设为true,会用poll(timeout)超时返回null,线程退出。
CAS有什么缺点?
三大缺点
1️⃣ 自旋开销:CAS失败会一直重试,如果竞争激烈,CPU空转浪费资源
2️⃣ 只能保证一个变量的原子性:多个变量需要AtomicReference包装
3️⃣ ABA问题:值从A→B→A,CAS认为没变过。用AtomicStampedReference(版本号)解决

LongAdder怎么优化CAS?:把一个value拆成多个Cell,不同线程CAS不同Cell,减少冲突,最后sum()汇总。
CountDownLatch和CyclicBarrier到底怎么选?
看场景:
CountDownLatch:1个线程等N个线程完成 → 主线程await(),子线程countDown() → 一次性的
例子:游戏加载,等所有资源加载完再开始

CyclicBarrier:N个线程互相等到齐 → 各自await() → 到齐后执行barrierAction → 可循环
例子:多人游戏,每轮等所有人准备好再开始下一轮

关键区别:CountDownLatch是"减法"(countDown递减),CyclicBarrier是"加法"(人到齐触发)
什么是线程死锁?怎么排查?
死锁 = 两个人互等让路:线程A持有锁1等锁2,线程B持有锁2等锁1

四个必要条件:互斥、持有并等待、不可抢占、循环等待

排查方法
1️⃣ jstack pid → 找到 "Found one Java-level deadlock"
2️⃣ jconsole → 线程页 → 检测死锁
3️⃣ Arthas: thread -b → 找阻塞线程

预防:固定加锁顺序、tryLock(timeout)、避免嵌套锁
sleep(0)有什么用?
主动让出CPU时间片!sleep(0)不会真的让线程睡眠,而是触发一次CPU调度——当前线程放弃剩余时间片,让其他同优先级线程有机会执行。

核心机制:你正在执行任务,执行到一半说"我先让别的线程执行一会儿"(当前线程仍活着,只是让出了CPU时间片)。
用途:减少线程饥饿,给低优先级线程机会。
类似API:Thread.yield() 也有类似效果,但不保证。
为什么wait()必须在synchronized中调用?
因为wait()的前提是持有锁!wait()会释放锁,如果不在synchronized中调用,就没有锁可释放,会抛IllegalMonitorStateException。

设计原因:wait/notify是线程间协作机制——你必须在"拥有某个对象的锁"的前提下才能"等待这个对象的通知"。

标准写法
synchronized(lock) {
  while(!condition) lock.wait(); // 必须用while不用if(防止虚假唤醒)
  // 执行逻辑
}
公平锁和非公平锁性能差多少?
非公平锁吞吐量更高(约高20-30%)
非公平锁:新来的线程直接CAS抢锁,抢到就执行,不用排队 → 减少线程上下文切换
公平锁:新来的线程先看队列有没有人,有人就乖乖排队 → 线程切换开销大

但是:公平锁不会"饿死"线程——每个线程都能等到执行机会;非公平锁可能导致某些线程永远抢不到锁。

默认:ReentrantLock默认非公平,synchronized也是非公平。
线程池的submit()和execute()有什么区别?
三个区别
1️⃣ 返回值:execute()无返回值(void),submit()返回Future<?>
2️⃣ 异常处理:execute()的异常直接抛出,submit()的异常被封装在Future中(get()时抛ExecutionException)
3️⃣ 参数类型:execute(Runnable),submit(Runnable/Callable)

重要坑:submit()执行Runnable时,如果run()抛异常,异常会被吞掉!必须future.get()才能发现。
最佳实践:需要返回值或异常感知 → submit();简单执行 → execute()
ThreadLocal为什么用WeakReference还是会内存泄漏?
Key是弱引用,Value是强引用!

ThreadLocalMap.Entry继承WeakReference,Key(ThreadLocal对象)会被GC回收变成null,但Value是强引用,还被Entry引用着 → 这个Entry的Key=null,Value无法访问也无法回收 → 泄漏。

为什么设计成WeakReference?为了ThreadLocal对象本身能被回收。如果Key是强引用,即使你不再使用ThreadLocal对象,它也无法被GC(因为Map里还引用着)。

ThreadLocal自身的清理机制:在get()/set()/remove()时,会顺带清理Key=null的Entry(expungeStaleEntry)。但这只是"顺便",如果你不再调用这些方法,脏Entry就永远留着。

结论:WeakReference减少了ThreadLocal对象本身的泄漏风险,但没有消除Value的泄漏风险。必须手动remove()。
ThreadLocal和InheritableThreadLocal有什么区别?
ThreadLocal:父子线程不共享,子线程拿不到父线程的ThreadLocal值。

InheritableThreadLocal:创建子线程时,子线程会复制父线程的所有InheritableThreadLocal值。
使用场景:链路追踪TraceId需要从主线程传到子线程。

⚠️ 坑:线程池场景不适用!线程池的线程是复用的,不是新建的,所以InheritableThreadLocal不会复制。需要用TransmittableThreadLocal(阿里开源)来解决这个问题。

总结:new Thread() → InheritableThreadLocal够用;线程池 → TransmittableThreadLocal。
什么时候用synchronized,什么时候用ThreadLocal?
核心区别:要不要共享?

必须共享 → synchronized:多个线程必须操作同一个对象。比如扣余额(大家的余额是同一个数字)、计数器(数的是同一个数)。

可以不共享 → ThreadLocal:每个线程只需要自己的副本,不需要看到别人的数据。比如当前登录用户、日期格式化器、数据库连接。

判断口诀:如果最终结果"必须合并"(计数、余额)→ synchronized;如果最终结果"各自独立"(用户上下文、TraceId)→ ThreadLocal。

常见错误:用synchronized保护SimpleDateFormat → 可以,但性能差(所有线程排队格式化);用ThreadLocal → 每个线程一份,零竞争,快得多。
什么是虚假唤醒(spurious wakeup)?
线程在wait()时可能被"莫名其妙"地唤醒,即使没有人调用notify()!这就是虚假唤醒。

解决方案:永远用while不用if检查条件:
synchronized(lock) { if(!condition) lock.wait(); }
synchronized(lock) { while(!condition) lock.wait(); }

类比:你在休息室等人通知开会。如果有人恶作剧敲了一下门(虚假唤醒),你出去一看没人开会,就回去继续等。用while就是"醒来后再确认一次"。

📋 并发工具速查表

需求工具一句话
互斥访问synchronized / ReentrantLock公共厕所 vs 银行取号
状态标志volatile公告栏,改了大家都看得到
原子计数AtomicInteger / LongAdderCAS自旋 vs 分段CAS
等N个任务完成CountDownLatch倒计时到0开门
N个线程互等到齐CyclicBarrier人齐了开饭,可循环
控制并发数Semaphore车位牌,拿牌才能停
读写分离ReentrantReadWriteLock读共享,写独占
线程间交换数据Exchanger两个人互换礼物
异步计算CompletableFuture外卖配送链(链式编排)
线程变量隔离ThreadLocal自家厕所,各用各的,不用锁
定时任务ScheduledThreadPool闹钟,定时执行

— 全栈资料库 · Java并发编程图解 —

核心三原则速记

并发编程三原则:① 共享数据优先用 concurrent 包,别自己 synchronized 粗粒度锁 → ② 线程池参数要压测,corePoolSize 按公式算不如按监控调 → ③ volatile 保证可见性不保证原子性,复合操作用 Atomic 类