并发编程、锁、线程池知识整理<1>
前言
- synchronized 底层原理?锁升级过程(偏向→轻量→重量)?
- ReentrantLock vs synchronized?AQS 是什么?
- volatile 的作用?happens-before 原则?能保证原子性吗?
- 线程池核心参数?execute() vs submit()?拒绝策略?
- CompletableFuture 异步编排?
- 死锁如何产生?如何排查?(jstack + 线程 dump 分析)
如下所示:
1、synchronized 底层原理?锁升级过程?
synchronized 是 JVM 提供的内置锁,基于 Monitor(监视器)对象实现,其底层依赖对象头的 Mark Word 和线程栈中的 Lock Record。
一、底层原理:Monitor 机制
1. Monitor 是什么?
- 每个 Java 对象关联一个 Monitor 对象(C++ 实现,位于 JVM 内部);
- Monitor 包含:
- _owner:当前持有锁的线程;
- _count:重入次数;
- _EntryList:阻塞等待的线程队列;
- _WaitSet:调用 wait()的线程队列。
2. 字节码指令
- 进入同步块:monitorenter
- 退出同步块:monitorexit(正常退出 + 异常退出各一条)
💡 注意:方法级 synchronized 通过 ACC_SYNCHRONIZED 标志实现,无需显式指令。
二、锁升级过程(JDK 6+ 引入,单向不可逆)
JVM 为减少无竞争时的同步开销,设计了 4 级锁状态,根据竞争激烈程度动态升级。
如下所示:

三、详细升级流程
1. 无锁 → 偏向锁
- 第一次加锁时,JVM 将 Mark Word 中的 Thread ID 设为当前线程 ID;
- 后续同一线程加锁:直接比对 Thread ID,无需 CAS;
- 优点:无 CAS、无互斥,性能极高;
- 缺点:多线程竞争时需撤销偏向(STW)。
🔧 参数控制:
- -XX:+UseBiasedLocking(默认开启)
- -XX:+BiasedLockingStartupDelay=0(立即启用,避免 4s 延迟)
2. 偏向锁 → 轻量级锁
- 当其他线程尝试加锁:
- JVM 暂停持有偏向锁的线程;
- 检查是否仍需偏向(如已退出同步块);
- 若需竞争,撤销偏向锁,升级为轻量级锁;
- 轻量级锁实现:
- 线程在栈中创建 Lock Record;
- CAS 将 Mark Word 替换为指向 Lock Record 的指针;
- 成功则获得锁,失败则自旋重试。
3. 轻量级锁 → 重量级锁.
- 自旋一定次数(-XX:PreBlockSpin,默认 10 次)后仍失败;
- 或有多个线程竞争(JVM 检测到竞争激烈);
- 膨胀为重量级锁:
- Mark Word 指向 Monitor 对象;
- 线程进入_EntryList 阻塞(OS 级挂起)。
⚠️ 注意:
- 轻量级锁不支持重入!重入会直接膨胀为重量级锁;
- 偏向锁支持重入(通过 Thread ID + epoch)。
四、最佳实践
- 提到 锁消除(Lock Elimination):JIT 发现锁无竞争,直接移除;
- 提到 锁粗化(Lock Coarsening):合并相邻同步块,减少加锁次数;
- 举例:在无竞争场景,偏向锁性能比 ReentrantLock 高 30%。
2、ReentrantLock vs synchronized?AQS 是什么?
一、ReentrantLock vs synchronized 对比
如下所示:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 类型 | JVM 关键字 | JDK API 类 |
| 实现 | Monitor(对象头 Mark Word) | AQS(AbstractQueuedSynchronizer) |
| 锁获取 | 自动 | 手动(lock()) |
| 锁释放 | 自动(代码块结束/异常) | 手动(unlock(),必须在 finally) |
| 可中断 | ❌ | ✅(lockInterruptibly()) |
| 超时获取 | ❌ | ✅(tryLock(timeout)) |
| 公平锁 | ❌ | ✅(new ReentranLock(true)) |
| 多条件等待 | ❌(仅一个 wait set) | ✅(多个 Condition) |
| 性能(JDK 6+) | ≈(无竞争) | ≈(高竞争略优) |
二、AQS(AbstractQueuedSynchronizer)是什么?
JUC 包的基石,ReentrantLock、CountDownLatch、Semaphore 等都基于它实现。
1. 核心设计
- state:volatile. int,表示同步状态(如重入次数);
- CLH 变种队列:双向链表,存储等待线程(Node);
head:头节点(哑节点)- tail:尾节点
- 模板方法模式:
- 子类实现 tryAcquire()/tryRelease();
- AQS 负责线程排队、唤醒、中断处理。
2. Node 结构
static final class Node {volatile Node prev;volatile Node next;volatile Thread thread;Node nextWaiter; // Condition 队列
}
3. 加锁流程(以 ReentrantLock 为例)
- 调用 lock()→ acquire(1)
- tryAcquire():CAS 修改 state
- 成功:获得锁
- 失败:加入队列,park() 挂起
- 释放锁时:unpark() 唤醒后继节点
三、公平锁 vs 非公平锁
- 非公平锁(默认):
- 新线程可直接抢锁,无需排队;
- 吞吐量高,但可能造成线程饥饿。
- 公平锁:
- 新线程必须排队;
- 保证 FIFO,但吞吐量低(频繁上下文切换)。
代码如下所示:
// 非公平锁
final boolean acquire() {if (compareAndSetState(0, 1)) // 直接抢return true;return acquire(1);
}
四、最佳实践
- 提到 StampedLock:读多写少场景,支持乐观读;
- 举例:在限流器中用 tryLock(100,TimeUnit.MILLISECONDS) 实现非阻塞获取;
- 强调:ReentrantLock 必须在 finally 中 unlock(),否则死锁。
3、volatile 的作用?happens-before 原则?能保证原子性吗?
一、volatile 的三大作用
1. 保证可见性
- 写操作立即刷入主内存;
- 读操作从主内存加载;
- 避免线程读取“过期”副本。
2. 禁止指令重排序
- 插入 内存屏障(Memory Barrier):
- LoadLoad、StoreStore、LoadStore、StoreLoad;
- 保证 volatile 写之前的代码不会重排到写之后。
3. 提供 happens-before 语义
- volatile 写 happens-before 后续的 volatile 读。
二、happens-before 原则(JMM 核心)
happens-before 定义了操作间的可见性顺序,包括:
- 程序顺序规则:一个线程内,前面的操作 happens-before 后面的操作;
- 监视器锁规则:解锁 happens-before 后续加锁;
- volatile 变量规则:volatile 写 happens-before 后续 volatile 读;
- 线程启动规则:Thread.start() happens-before 线程内任何操作;
- 线程 join 规则:线程内所有操作 happens-before join() 返回。
💡 关键:happens-before ≠ 时间先后,而是结果可见性保证。
三、volatile 能保证原子性吗?
不能!
1. 反例:i++
volatile int i = 0;
// 线程 A 和 B 同时执行
i++; // 包含:读 i → i+1 → 写 i
- 两个线程可能同时读到 i = 0,最终 i = 1(应为 2);
- 原因:i++ 是复合操作,volatile 只保证单次读/写的原子性。
2. 何时可用?
- 状态标志(如 volatile. boolean running);
- 单次读/写(如 volatile Config config)。
3. 替代方案
- 原子类:AtomicInteger(CAS + volatile);
- 锁:synchronized / ReentrantLock。
四、最佳实践
- 提到 DCL 单例(Double-Checked Locking) 必须用 volatile 防止重排序:
private volatile static Singleton instance;
public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // 重排序风险!}}}return instance;
}
- 强调:volatile 是轻量级同步机制,但绝不等于“线程安全”。
4、线程池核心参数?execute() vs submit()?拒绝策略?
一、ThreadPoolExecutor 7 大核心参数
new ThreadPoolExecutor(int corePoolSize, // 核心线程数(即使空闲也不销毁)int maximumPoolSize, // 最大线程数long keepAliveTime, // 非核心线程空闲存活时间TimeUnit unit, // 时间单位BlockingQueue<Runnable> workQueue, // 任务队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 拒绝策略
);
1. 执行流程
- 线程数 < corePoolSize→ 创建新线程;
- 线程数 >= corePoolSize→ 入队 workQueue;
- 队列满且线程数 < maximumPoolsize → 创建新线程;
- 队列满且线程数 = maximumPoolsize → 触发拒绝策略。
2. 常见线程池陷阱
- Executors.newFixedThreadPool():无界队列 → OOM;
- Executors.newCachedThreadPool():最大线程数 = Integer.MAX_VALUE → OOM。
最佳实践:手动创建线程池,明确队列大小和拒绝策略。
二、execute() vs submit()
| 方法 | 返回值 | 异常处理 | 适用场景 |
|---|---|---|---|
| execute(Runnable) | void | 未捕获异常会打印堆栈 | 无需返回结果 |
| submit(Runnable/Callable) | Future<?> | 异常封装在 Future.get() | 需要返回结果或异常 |
💡 关键区别:
- submit() 内部将 Runnable 包装为 FutureTasks;
- 调用 Future.get()时才会抛出任务中的异常。
三、4 种拒绝策略
如下所示:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException | 允许任务失败 |
| CallerRunsPolicy | 由调用线程执行任务 | 降低提交速度 |
| DiscardPolicy | 静默丢弃任务 | 任务可丢弃 |
| DiscardOldestPolicy | 丢弃队列最老任务,重试提交 | 保留最新任务 |
💡 自定义策略:实现 RejectedExecutionHandler 接口,如记录日志、降级处理。
四、最佳实践
- 提到 allowCoreThreadTimeOut(true):核心线程也可超时销毁;
- 举例:在秒杀系统中用 CallerRunsPolicy 降级,避免系统崩溃;
- 强调:生产环境必须监控线程池指标(活跃线程数、队列大小、拒绝数)。
5、CompletableFuture 异步编排?
CompletableFuture 是 Java 8 引入的异步编程利器,支持链式调用、组合、异常处理。
一、核心优势
- 非阻塞:避免 Future.get()阻塞;
- 链式编排:thenApply/thenCompose/thenCombine;
- 异常处理:exceptionally/handle;
- 多任务组合:allOf/anyOf。
二、常用方法示例
1. 创建异步任务
代码如下所示:
// 异步执行(默认 ForkJoinPool)
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {return "Hello";
});// 指定线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {return "Hello";
}, executor);
2. 链式处理
代码如下所示:
CompletableFuture<String> result = CompletableFuture.supplyAsync(() -> queryUser()) // 异步查用户.thenApply(user -> user.getName()) // 转换.thenCompose(name -> queryOrder(name)) // 扁平化(返回 CompletableFuture).thenCombine(queryAddress(), (order, addr) -> order + addr); // 合并两个异步结果
3. 异常处理
代码如下所示:
future.exceptionally(ex -> {log.error("Error", ex);return "Default Value";
});// 或 handle(同时处理正常和异常)
future.handle((result, ex) -> {if (ex != null) return "Error";else return result;
});
4. 多任务组合
代码如下所示:
CompletableFuture<Void> allDone = CompletableFuture.allOf(future1, future2, future3);
allDone.join(); // 等待所有完成CompletableFuture<Object> anyDone = CompletableFuture.anyOf(future1, future2);
Object result = anyDone.get(); // 返回第一个完成的结果
三、最佳实践
- 提到 避免默认 ForkJoinPool:生产环境应指定自定义线程池;t
- 举例:在订单服务中用
allOf并行调用库存、优惠券、支付服务,RT 从 600ms 降至 200ms; - 强调:thenApply/thenCompose:
- thenApply:同步转换,返回普通值;
- thenCompose:异步扁平化,返回 CompletableFutute。
6、死锁如何产生?如何排查?(jstack + 线程 dump 分析)
一、死锁产生的 4 个必要条件
- 互斥条件:资源不能共享(如锁);
- 请求与保持:持有资源的同时申请新资源;
- 不剥夺条件:资源不能被强制释放;
- 循环等待:存在进程-资源环形链。
💡 破坏任一条件即可避免死锁,如按序加锁(破坏循环等待)。
二、Java 死锁典型场景
代码如下所示:
// 线程 A
synchronized (lockA) {synchronized (lockB) { ... }
}// 线程 B
synchronized (lockB) {synchronized (lockA) { ... }
}
三、死锁排查步骤
1. 获取线程 dump
代码如下所示:
# 方式 1:jstack
jstack <pid> > thread_dump.txt# 方式 2:kill -3 <pid>(输出到 stdout)
# 方式 3:Arthas
thread -b # 直接定位死锁线程
2. 分析死锁
- 自动检测:jstack 会自动打印死锁信息(Found one Java-level deadlock);
- 手动分析:
- 找到 Blocked 状态的线程;waiting to lock<address>
- 查看 waiting to lock<address> 和 locked <address>;
- 构建锁依赖图,找环。
3. 示例输出
Found one Java-level deadlock:
=============================
"Thread-1":waiting to lock monitor 0x00007f... (object 0x000000076b..., a java.lang.Object),which is held by "Thread-2"
"Thread-2":waiting to lock monitor 0x00007f... (object 0x000000076b..., a java.lang.Object),which is held by "Thread-1"
四、预防与解决
1. 预防
- 按序加锁:所有线程以相同顺序获取锁;
- 超时放弃:tryLock(timeout);
- 避免嵌套锁:尽量减少同步块嵌套。
2. 监控
- 使用 Arthas thread -b实时检测;
- APM 系统监控线程 BLOCKED 状态。
五、最佳实践
- 提到 jconsole/visualVM图形化检测死锁;
- 举例:曾通过 jstack 发现数据库连接池死锁,修复后服务恢复;
- 强调:死锁是“活锁”的一种,但更危险(永久阻塞)。
