← 返回 Java核心图解 目录
☕ Java并发编程图解
synchronized锁升级 · volatile语义 · 线程池 · CAS与AQS · JUC三剑客 · Thread生命周期 · ThreadLocal
🔒 S1: synchronized锁升级全过程
synchronized = 公共厕所
🔒 无锁 → 走进去发现没人,直接用(偏向锁:贴上你的名字牌)
🔄 轻量锁 → 第二个人来了,你们在门口"谦让"(CAS自旋:不用蹲下等人出来,站着等就行)
⚖️ 重量锁 → 人太多了,谦让太累,保安来了(OS互斥量:排队拿号,蹲着等叫号)
为什么升级?因为大多数时候根本没人抢!如果一开始就叫保安(重量锁),太浪费了。先贴名字牌(偏向),人多了再谦让(轻量),真挤了才叫保安(重量)。
synchronized 锁升级流程:无锁 → 偏向 → 轻量 → 重量(只升级不降级)
偏向锁撤销:什么时候"名字牌"会被撕掉?
当第二个线程尝试获取偏向锁时,需要等到全局安全点(STW)才能撤销偏向:
STW暂停
→
检查持有线程
→
还在用?→轻量锁
→
不用了?→无锁
批量重偏向:如果一个类的对象频繁撤销偏向(>20次),JVM会直接把新对象偏向到新线程,不再走撤销流程。
批量撤销:如果撤销更频繁(>40次),这个类直接禁用偏向锁,新对象一开始就是轻量锁。
SynchronizedLockUpgrade.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 可见性:所有线程直接读写主内存(公告栏),不再用CPU缓存(草稿纸)
volatile 为什么不能保证原子性?
经典反例:i++ 不是原子操作!它分三步:读 → +1 → 写回
volatile 适用场景:一个线程写、多个线程读(状态标志位 flag);
volatile 不适用:多线程同时写(i++、count++),需要用 AtomicInteger 或 synchronized。
VolatileVisibility.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) = 线程和队列都满了怎么办
执行流程:提交任务→核心线程处理→都忙→入队列→队列满→创建临时线程→临时线程也满→执行拒绝策略
ThreadPoolExecutor 执行流程:核心线程 → 队列 → 非核心线程 → 拒绝策略
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乐观锁:不加锁,先比较再写入,失败就重试(自旋)
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 非公平:
公平锁 = 严格按取号顺序叫号(新来的不能插队)
非公平锁 = 新来的人直接冲向柜台,有人就取号排着(默认,性能更好)
AQS = state变量 + CLH双向队列,所有JUC锁的基础
AQSPrinciple.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=信号量(限量供应)
JUC三剑客:CountDownLatch(倒计时) / CyclicBarrier(循环栅栏) / Semaphore(信号量)
JUCSyncDemo.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()执行完毕或异常退出)
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
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的成员变量,不是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
// ✅ 生产场景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
// ✅ 生产场景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
// ⚠️ 内存泄漏演示:线程池 + 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的区别?
| 维度 | synchronized | ReentrantLock |
| 实现 | 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 / LongAdder | CAS自旋 vs 分段CAS |
| 等N个任务完成 | CountDownLatch | 倒计时到0开门 |
| N个线程互等到齐 | CyclicBarrier | 人齐了开饭,可循环 |
| 控制并发数 | Semaphore | 车位牌,拿牌才能停 |
| 读写分离 | ReentrantReadWriteLock | 读共享,写独占 |
| 线程间交换数据 | Exchanger | 两个人互换礼物 |
| 异步计算 | CompletableFuture | 外卖配送链(链式编排) |
| 线程变量隔离 | ThreadLocal | 自家厕所,各用各的,不用锁 |
| 定时任务 | ScheduledThreadPool | 闹钟,定时执行 |
— 全栈资料库 · Java并发编程图解 —
核心三原则速记
并发编程三原则:① 共享数据优先用 concurrent 包,别自己 synchronized 粗粒度锁 → ② 线程池参数要压测,corePoolSize 按公式算不如按监控调 → ③ volatile 保证可见性不保证原子性,复合操作用 Atomic 类