一、引入场景:那个让我失眠的线上Bug 😱
还记得去年双十一,我们系统突然出现库存超卖问题。明明数据库里只剩100件商品,结果卖出了150件。领导黑着脸问我:"你不是说加了synchronized吗?"我当时脸都绿了...
后来排查发现,synchronized加的位置不对,而且面对分布式场景根本不够用。那一晚我把Java所有的锁机制都翻了个底朝天。如果你也遇到过类似问题,或者面试被问懵过,这篇文章能帮到你。
二、快速理解:Java锁到底是个啥?
通俗版: 锁就像公共厕所的门锁,同一时刻只能一个人用。在Java里,锁用来保证多个线程不会同时修改同一份数据,避免出现"脏数据"。
严谨定义: Java锁是一种同步机制,用于控制多线程对共享资源的并发访问,通过互斥或读写分离等策略,保证数据的一致性和线程安全性。
三、为什么需要锁?🤔
3.1 不加锁会怎样?
public class CounterWithoutLock {
private int count = 0;
public void increment() {
count++; // 这一行实际上是三个操作:读取、加1、写回
}
public static void main(String[] args) throws InterruptedException {
CounterWithoutLock counter = new CounterWithoutLock();
// 启动1000个线程,每个执行1000次加1
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
System.out.println("期望值: 1000000");
System.out.println("实际值: " + counter.count); // 通常会小于1000000
}
}
运行结果: 你会发现count的值每次都不同,而且都小于1000000。为什么?因为count++不是原子操作!
3.2 核心痛点
| 问题 | 说明 | 后果 |
|---|---|---|
| 竞态条件 | 多线程同时读写共享变量 | 数据不一致,计算错误 |
| 可见性问题 | 线程A修改的值,线程B看不到 | 读到过期数据 |
| 指令重排序 | CPU和编译器优化导致执行顺序改变 | 单例模式失效等 |
3.3 适用场景对比
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单计数器 | AtomicInteger | 性能高,CAS无锁 |
| 复杂业务逻辑 | synchronized/ReentrantLock | 支持代码块保护 |
| 读多写少 | ReentrantReadWriteLock | 读不互斥,性能好 |
| 分布式环境 | Redis分布式锁/Zookeeper | 跨JVM进程 |
四、基础用法:三种常见加锁方式
4.1 synchronized关键字
public class SynchronizedDemo {
private int count = 0;
private final Object lock = new Object();
// 方式1:修饰实例方法,锁对象是this
public synchronized void incrementMethod() {
count++;
}
// 方式2:修饰代码块,锁对象是lock
public void incrementBlock() {
synchronized (lock) { // 🔥面试常考:这里的lock是什么?
count++;
}
}
// 方式3:修饰静态方法,锁对象是Class对象
public static synchronized void incrementStatic() {
// 锁是 SynchronizedDemo.class
}
// ⚠️ 常见错误:锁对象不一致
public void wrongWay() {
synchronized (new Object()) { // 每次都是新对象,锁不住!
count++;
}
}
}
🔥面试高频问题:
- synchronized锁的是什么?(对象/Class)
- synchronized修饰静态方法和实例方法有什么区别?(锁对象不同)
- 为什么局部变量不需要加锁?(线程私有)
4.2 ReentrantLock显式锁
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
public void increment() {
lock.lock(); // 🔥必须手动加锁
try {
count++;
// 业务逻辑
} finally {
lock.unlock(); // ⚠️必须在finally中释放,否则死锁!
}
}
// 高级用法:尝试加锁
public boolean tryIncrement() {
if (lock.tryLock()) { // 非阻塞获取锁
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false; // 获取锁失败
}
}
4.3 读写锁 ReadWriteLock
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
private int value = 0;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读操作:多个线程可以同时读
public int read() {
rwLock.readLock().lock(); // 读锁
try {
return value;
} finally {
rwLock.readLock().unlock();
}
}
// 写操作:只能一个线程写,且写时不能读
public void write(int newValue) {
rwLock.writeLock().lock(); // 写锁
try {
value = newValue;
} finally {
rwLock.writeLock().unlock();
}
}
}
🔥面试考点:
- 读写锁的读锁和写锁是否互斥?(读读不互斥,读写、写写互斥)
- 什么场景适合读写锁?(读多写少场景)
五、⭐ 底层原理深挖(面试重点)
5.1 synchronized的底层实现
JVM层面的实现机制:
synchronized在字节码层面依赖两个指令:
monitorenter:进入同步块,获取监视器锁monitorexit:退出同步块,释放监视器锁
// Java代码
public void syncMethod() {
synchronized (this) {
// 业务代码
}
}
// 对应字节码(简化版)
public void syncMethod();
Code:
0: aload_0 // 加载this
1: dup // 复制栈顶引用
2: astore_1 // 存储引用
3: monitorenter // 🔥获取监视器锁
4: // ... 业务代码
7: aload_1
8: monitorexit // 🔥释放监视器锁
9: goto 17
12: aload_1
13: monitorexit // 异常情况也要释放锁
对象头结构(Hotspot JVM):
每个Java对象在内存中都包含对象头,synchronized就是通过修改对象头实现的:
|----------------------------------------------------------------------|
| Object Header (对象头) |
|----------------------------------------------------------------------|
| Mark Word (标记字段, 64位) | Class Pointer (类型指针) |
|----------------------------------------------------------------------|
Mark Word的结构(64位JVM):
| 锁状态 | 25位 | 31位 | 1位 | 4位 | 1位偏向锁标志 | 2位锁标志 |
|---|---|---|---|---|---|---|
| 无锁 | hashcode | age | 0 | 01 | ||
| 偏向锁 | ThreadID(54位) + Epoch(2位) | age | 1 | 01 | ||
| 轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | ||||
| GC标记 | 11 |
5.2 🔥锁升级机制(JDK 1.6优化,高频考点)
为了减少锁的性能开销,JDK 1.6引入了锁升级机制,按照竞争程度依次升级:
graph LR
A[无锁状态] --> B[偏向锁]
B --> C[轻量级锁]
C --> D[重量级锁]
style B fill:#90EE90
style C fill:#FFD700
style D fill:#FF6347
1. 偏向锁(Biased Locking)
核心思想: 大多数情况下,锁总是由同一个线程多次获得,为了降低获取锁的代价引入偏向锁。
// 场景:单线程反复进入同步块
public class BiasedLockExample {
public synchronized void method() {
// 第一次:升级为偏向锁,记录ThreadID
// 后续:检查ThreadID相同,直接进入,无需CAS
}
}
工作流程:
- 当线程第一次访问同步块时,在对象头的Mark Word中记录该线程ID
- 之后该线程再次进入时,只需检查Mark Word中的ThreadID是否为自己
- 如果是,直接进入;如果不是,说明有竞争,撤销偏向锁,升级为轻量级锁
优点: 几乎无性能损耗 缺点: 有竞争时撤销成本高
2. 轻量级锁(Lightweight Locking)
核心思想: 使用CAS操作避免使用互斥量(mutex)。
工作流程:
- 线程在栈帧中创建锁记录(Lock Record)
- 使用CAS将对象头的Mark Word复制到锁记录中
- 尝试用CAS将对象头的Mark Word替换为指向锁记录的指针
- 成功:获得锁;失败:自旋重试
- 自旋一定次数后仍失败:升级为重量级锁
// 轻量级锁适用场景:同步块执行速度快,线程交替执行
public class LightweightLockExample {
private int count = 0;
public void increment() {
synchronized (this) { // 短时间持有锁
count++;
}
}
}
3. 重量级锁(Heavyweight Locking)
核心思想: 基于操作系统的互斥量(Mutex)实现,会导致线程在内核态和用户态之间切换。
工作流程:
- 未获得锁的线程进入阻塞状态(BLOCKED)
- 等待持有锁的线程释放后,由操作系统唤醒
- 涉及用户态和内核态切换,性能开销大
sequenceDiagram
participant T1 as 线程1
participant OBJ as 同步对象
participant T2 as 线程2
participant OS as 操作系统
T1->>OBJ: synchronized获取锁
OBJ->>T1: 成功(重量级锁)
T2->>OBJ: 尝试获取锁
OBJ->>OS: 线程2阻塞
OS->>T2: 进入BLOCKED状态
T1->>OBJ: 释放锁
OBJ->>OS: 通知唤醒
OS->>T2: 唤醒线程2
T2->>OBJ: 获取锁成功
5.3 ReentrantLock底层原理(AQS)
ReentrantLock基于**AbstractQueuedSynchronizer(AQS)**实现。
AQS核心数据结构:
// AQS简化源码(JDK 8)
public abstract class AbstractQueuedSynchronizer {
// 同步状态(0表示未锁定,>0表示锁定)
private volatile int state;
// CLH队列的头节点
private transient volatile Node head;
// CLH队列的尾节点
private transient volatile Node tail;
// 等待队列中的节点
static final class Node {
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 等待的线程
volatile int waitStatus; // 等待状态
}
}
加锁流程(公平锁):
graph TD
A[调用lock] --> B{state == 0?}
B -->|是| C[CAS设置state=1]
C -->|成功| D[获取锁成功]
C -->|失败| E[加入等待队列]
B -->|否| F{当前线程是持有者?}
F -->|是| G[state++, 可重入]
F -->|否| E
E --> H[park阻塞]
H --> I[等待前驱节点释放]
源码片段(ReentrantLock.lock):
// ReentrantLock的lock方法
public void lock() {
sync.lock(); // 调用AQS的子类
}
// 公平锁的实现
final void lock() {
acquire(1); // AQS的模板方法
}
// AQS的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 🔥尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加入队列并阻塞
selfInterrupt();
}
// FairSync的tryAcquire实现(公平锁)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取同步状态
if (c == 0) { // 锁未被占用
if (!hasQueuedPredecessors() && // 🔥检查队列中是否有等待线程
compareAndSetState(0, acquires)) { // CAS设置state
setExclusiveOwnerThread(current); // 设置当前线程为持有者
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 🔥可重入
int nextc = c + acquires;
if (nextc < 0) // 溢出检查
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
🔥非公平锁 vs 公平锁:
// 非公平锁的tryAcquire(性能更好)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// ⚠️注意:没有检查队列,直接CAS抢锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ... 可重入逻辑同上
}
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 实现 | 检查队列后排队 | 直接抢锁,失败再排队 |
| 优点 | 不会饥饿 | 吞吐量高,性能好 |
| 缺点 | 性能较低 | 可能导致线程饥饿 |
| 适用场景 | 需要严格按顺序 | 一般业务场景(默认) |
5.4 版本演进的重要变化
JDK 1.5: 引入ReentrantLock和Lock接口 JDK 1.6: synchronized大幅优化,引入偏向锁、轻量级锁、自旋锁、锁消除、锁粗化 JDK 15: 默认禁用偏向锁(-XX:+UseBiasedLocking),因为维护成本高于收益
六、性能分析与优化
6.1 性能对比
测试场景: 1000个线程,每个线程对共享变量执行10000次自增操作
| 锁类型 | 平均耗时 | 吞吐量 | 适用场景 |
|---|---|---|---|
| 无锁(错误) | 50ms | 最高 | ❌数据不一致 |
| synchronized | 150ms | 较高 | ✅简单场景,JVM自动优化 |
| ReentrantLock(非公平) | 120ms | 高 | ✅需要高级特性(tryLock, 中断) |
| ReentrantLock(公平) | 300ms | 较低 | ✅严格顺序要求 |
| AtomicInteger | 80ms | 很高 | ✅简单原子操作 |
测试代码:
// 性能测试框架(JMH)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class LockBenchmark {
@Benchmark
public void testSynchronized(SharedState state) {
synchronized (state.lock) {
state.count++;
}
}
@Benchmark
public void testReentrantLock(SharedState state) {
state.reentrantLock.lock();
try {
state.count++;
} finally {
state.reentrantLock.unlock();
}
}
}
6.2 性能优化技巧
1. 减小锁粒度
// ❌ 不好:锁粒度太大
public synchronized void process() {
// 耗时的IO操作
String data = readFromFile();
// 需要同步的操作
sharedList.add(data);
}
// ✅ 好:只锁关键代码
public void process() {
String data = readFromFile(); // IO操作在锁外
synchronized (sharedList) {
sharedList.add(data); // 只锁这一行
}
}
2. 锁分段技术(ConcurrentHashMap的思想)
// JDK 1.7的ConcurrentHashMap使用Segment分段锁
// 多个线程可以同时访问不同的段
class SegmentedCounter {
private static final int SEGMENT_COUNT = 16;
private final Object[] locks = new Object[SEGMENT_COUNT];
private final int[] counts = new int[SEGMENT_COUNT];
public SegmentedCounter() {
for (int i = 0; i < SEGMENT_COUNT; i++) {
locks[i] = new Object();
}
}
public void increment(int index) {
int segment = index % SEGMENT_COUNT;
synchronized (locks[segment]) { // 只锁一个段
counts[segment]++;
}
}
}
3. 使用读写锁替代独占锁
// 读多写少场景,用ReadWriteLock提升并发度
public class CachedData {
private Object data;
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public Object getData() {
rwLock.readLock().lock(); // 多个线程可以同时读
try {
return data;
} finally {
rwLock.readLock().unlock();
}
}
public void setData(Object newData) {
rwLock.writeLock().lock(); // 写时独占
try {
data = newData;
} finally {
rwLock.writeLock().unlock();
}
}
}
4. 避免在循环中加锁
// ❌ 不好:每次循环都加锁
for (int i = 0; i < 1000; i++) {
synchronized (this) {
count++;
}
}
// ✅ 好:锁粗化,一次加锁
synchronized (this) {
for (int i = 0; i < 1000; i++) {
count++;
}
}
6.3 时间/空间复杂度分析
| 操作 | synchronized | ReentrantLock | 空间复杂度 |
|---|---|---|---|
| 加锁 | O(1)(偏向/轻量) O(线程数)(重量) | O(1)(无竞争) O(n)(CLH队列) | O(1) |
| 解锁 | O(1) | O(1) | O(1) |
| 等待队列 | - | O(n) | O(等待线程数) |
七、易混淆概念对比
7.1 synchronized vs ReentrantLock
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM层面(字节码指令) | JDK层面(基于AQS) |
| 锁释放 | 自动释放(异常也释放) | 手动释放(必须在finally) |
| 可中断性 | ❌不可中断 | ✅lockInterruptibly() |
| 公平性 | ❌非公平 | ✅可选公平/非公平 |
| 尝试加锁 | ❌不支持 | ✅tryLock() |
| 条件变量 | 1个(wait/notify) | 多个(Condition) |
| 性能 | JDK 1.6后相当 | 略高(无竞争时) |
| 适用场景 | 简单同步 | 需要高级特性 |
代码对比:
// synchronized等待/通知
public synchronized void waitMethod() throws InterruptedException {
while (condition) {
wait(); // 只有一个等待队列
}
}
public synchronized void notifyMethod() {
notifyAll();
}
// ReentrantLock的Condition(多个等待队列)
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition(); // 条件1:队列未满
Condition notEmpty = lock.newCondition(); // 条件2:队列非空
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 在notFull条件上等待
}
queue.add(item);
notEmpty.signal(); // 唤醒notEmpty条件上的线程
} finally {
lock.unlock();
}
}
7.2 悲观锁 vs 乐观锁
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 核心思想 | 先加锁,再操作 | 先操作,提交时检查冲突 |
| 实现方式 | synchronized, Lock | CAS, 版本号 |
| 冲突处理 | 阻塞等待 | 重试或失败 |
| 适用场景 | 写多读少,冲突频繁 | 读多写少,冲突少 |
| 典型应用 | 数据库行锁 | AtomicInteger, Git |
乐观锁示例(AtomicInteger):
public class OptimisticLockExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
int oldValue, newValue;
do {
oldValue = count.get(); // 读取当前值
newValue = oldValue + 1;
} while (!count.compareAndSet(oldValue, newValue)); // CAS更新
// 如果失败,说明有其他线程修改了,重试
}
}
7.3 可重入锁 vs 不可重入锁
可重入锁: 同一个线程可以多次获取同一把锁(synchronized和ReentrantLock都是)
public class ReentrantExample {
public synchronized void methodA() {
System.out.println("methodA");
methodB(); // 🔥同一线程再次获取this的锁,可重入
}
public synchronized void methodB() {
System.out.println("methodB");
}
}
为什么需要可重入? 如果不可重入,上述代码会在methodA调用methodB时死锁!
实现原理: 锁关联一个计数器和持有线程
- 首次获取:计数器=1,记录线程ID
- 再次获取(同一线程):计数器+1
- 释放:计数器-1,为0时真正释放
八、常见坑与最佳实践
8.1 常见错误
❌ 错误1:锁对象选择不当
// 错误:锁对象是String常量
public class WrongLock {
private String lock = "LOCK"; // ⚠️String会被缓存
public void method() {
synchronized (lock) { // 可能和其他地方的"LOCK"是同一个对象!
// ...
}
}
}
// 正确:使用Object或this
public class CorrectLock {
private final Object lock = new Object();
public void method() {
synchronized (lock) {
// ...
}
}
}
❌ 错误2:忘记释放锁
// 错误:异常时锁未释放
Lock lock = new ReentrantLock();
lock.lock();
if (condition) {
return; // ⚠️提前返回,锁未释放,导致死锁!
}
lock.unlock();
// 正确:使用try-finally
lock.lock();
try {
if (condition) {
return; // ✅finally会执行
}
} finally {
lock.unlock();
}
❌ 错误3:锁粒度过大
// 错误:整个方法都加锁
public synchronized void processOrder(Order order) {
// 1. 验证订单(耗时,不需要锁)
validate(order);
// 2. 调用外部服务(耗时,不需要锁)
paymentService.pay(order);
// 3. 扣减库存(需要锁)
inventory.decrease(order.getProductId());
}
// 正确:只锁关键部分
public void processOrder(Order order) {
validate(order);
paymentService.pay(order);
synchronized (inventory) { // 只锁这一步
inventory.decrease(order.getProductId());
}
}
❌ 错误4:死锁
// 经典死锁场景
public class DeadLockDemo {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) { // 线程1持有A
synchronized (lockB) { // 等待B
// ...
}
}
}
public void method2() {
synchronized (lockB) { // 线程2持有B
synchronized (lockA) { // 等待A → 死锁!
// ...
}
}
}
}
解决方案:
- 固定加锁顺序
- 使用tryLock设置超时
- 使用jstack检测死锁
// 方案1:固定顺序
synchronized (lockA) {
synchronized (lockB) {
// ...
}
}
// 方案2:超时机制
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 业务逻辑
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
8.2 最佳实践
✅ 1. 优先使用并发工具类
// 不要:自己实现计数器
private int count = 0;
public synchronized void increment() {
count++;
}
// 推荐:使用AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
✅ 2. 使用并发容器
// 不要:手动加锁的HashMap
private Map<String, String> map = new HashMap<>();
public synchronized String get(String key) {
return map.get(key);
}
// 推荐:ConcurrentHashMap
private Map<String, String> map = new ConcurrentHashMap<>();
public String get(String key) {
return map.get(key); // 内部已优化并发
}
✅ 3. 锁对象声明为final
private final Object lock = new Object(); // ✅防止被重新赋值
✅ 4. 文档化锁的保护范围
/**
* 库存管理器
* 线程安全:使用inventoryLock保护inventory字段
*/
public class InventoryManager {
private final Object inventoryLock = new Object();
@GuardedBy("inventoryLock") // Google Guava注解
private Map<Long, Integer> inventory = new HashMap<>();
}
九、⭐ 面试题精选(必看!)
⭐ 基础题
Q1:synchronized和volatile的区别是什么?
标准答案:
-
功能:
- volatile:保证可见性和有序性(禁止指令重排),但不保证原子性
- synchronized:保证原子性、可见性、有序性
-
实现:
- volatile:通过内存屏障,轻量级
- synchronized:通过监视器锁,涉及线程阻塞
-
使用场景:
- volatile:状态标志、双重检查锁定的单例模式
- synchronized:复杂的同步逻辑
// volatile典型应用:状态标志
private volatile boolean running = true;
public void stop() {
running = false; // 立即对其他线程可见
}
public void run() {
while (running) { // 能及时看到running的变化
// ...
}
}
Q2:synchronized锁的是对象还是代码?
标准答案: synchronized锁的是对象(Object),不是代码。
- 修饰实例方法: 锁的是
this对象 - 修饰静态方法: 锁的是
Class对象(类名.class) - 修饰代码块: 锁的是括号里的对象
public class LockTarget {
// 锁的是this
public synchronized void method1() { }
// 锁的是LockTarget.class
public static synchronized void method2() { }
private final Object lock = new Object();
// 锁的是lock对象
public void method3() {
synchronized (lock) { }
}
}
⚠️ 面试陷阱: 两个线程分别调用同一个对象的两个不同的synchronized实例方法,会互斥吗? 答案: 会!因为锁的都是同一个this对象。
Q3:什么是可重入锁?为什么需要可重入?
标准答案: 可重入锁: 同一个线程可以多次获取同一把锁,不会被自己阻塞。
为什么需要可重入:
public class ReentrantExample {
public synchronized void a() {
System.out.println("a");
b(); // 如果不可重入,这里会死锁!
}
public synchronized void b() {
System.out.println("b");
}
}
实现原理:
- 锁关联一个持有线程ID和计数器
- 同一线程再次获取:计数器+1(不阻塞)
- 释放锁:计数器-1,为0时真正释放
Java中的可重入锁: synchronized、ReentrantLock、ReentrantReadWriteLock
⭐⭐ 进阶题
Q4:详细说说synchronized的锁升级过程?(高频!)
标准答案:
JDK 1.6为了提高性能,引入了锁升级机制,按竞争激烈程度依次升级:
1. 偏向锁(Biased Lock)
- 场景: 只有一个线程访问同步块
- 原理: 在对象头Mark Word中记录线程ID,下次该线程进入时检查ID即可
- 优点: 几乎无性能损耗(单线程场景)
- 升级时机: 其他线程尝试获取锁
2. 轻量级锁(Lightweight Lock)
- 场景: 多线程交替执行,竞争不激烈
- 原理: 使用CAS操作替换对象头的Mark Word
- 优点: 避免了线程阻塞,使用自旋
- 升级时机: 自旋超过一定次数(默认10次)
3. 重量级锁(Heavyweight Lock)
- 场景: 多线程竞争激烈
- 原理: 基于操作系统Mutex互斥量
- 缺点: 线程阻塞,涉及用户态和内核态切换
流程图:
graph TD
A[无锁] --> B[偏向锁<br/>单线程访问]
B --> C{其他线程竞争?}
C -->|是| D[轻量级锁<br/>CAS+自旋]
D --> E{竞争激烈?}
E -->|是| F[重量级锁<br/>阻塞等待]
style A fill:#E8E8E8
style B fill:#90EE90
style D fill:#FFD700
style F fill:#FF6347
🔥 面试追问: 锁能降级吗? 答案: 一般不会降级(JVM不支持),但JDK 15后偏向锁默认被禁用。
Q5:ReentrantLock和synchronized的区别?什么时候用ReentrantLock?
标准答案:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 使用 | 关键字,自动释放 | 类,需手动释放 |
| 性能 | JDK 1.6后相当 | 略优(无竞争时) |
| 功能 | 基础 | 丰富 |
ReentrantLock的高级特性:
1. 可中断等待(lockInterruptibly)
Lock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 可响应中断
// 业务代码
} catch (InterruptedException e) {
// 被中断后的处理
} finally {
lock.unlock();
}
2. 尝试获取锁(tryLock)
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 等待3秒
try {
// 获取到锁
} finally {
lock.unlock();
}
} else {
// 未获取到锁的降级处理
}
3. 公平锁
Lock fairLock = new ReentrantLock(true); // 公平锁
4. 多个条件变量(Condition)
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 可以精确唤醒特定条件的线程
使用建议:
- 简单同步 → synchronized(代码简洁,自动优化)
- 需要高级特性 → ReentrantLock(超时、中断、公平性)
Q6:什么是死锁?如何避免?
标准答案:
死锁定义: 两个或多个线程互相持有对方需要的资源,导致都无法继续执行。
死锁的四个必要条件:
- 互斥: 资源同时只能被一个线程持有
- 持有并等待: 持有至少一个资源,并等待获取其他资源
- 不可剥夺: 资源不能被强制释放
- 循环等待: 存在资源的循环等待链
经典死锁代码:
// 线程1
synchronized (A) {
synchronized (B) { // 等待B
// ...
}
}
// 线程2
synchronized (B) {
synchronized (A) { // 等待A → 死锁!
// ...
}
}
避免死锁的方法:
1. 固定加锁顺序(破坏循环等待)
// 统一按照对象hashCode的顺序加锁
Object first = System.identityHashCode(A) < System.identityHashCode(B) ? A : B;
Object second = first == A ? B : A;
synchronized (first) {
synchronized (second) {
// ...
}
}
2. 使用tryLock超时(破坏持有并等待)
if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 业务逻辑
} finally {
lockB.unlock();
}
} else {
// 获取B失败,释放A,重试
}
} finally {
lockA.unlock();
}
}
3. 使用jstack检测死锁
jstack <pid> # 会输出死锁信息
⭐⭐⭐ 高级题
Q7:AQS的底层原理是什么?(深度好问!)
标准答案:
AQS(AbstractQueuedSynchronizer) 是J.U.C包中大部分同步器的基础框架,包括ReentrantLock、Semaphore、CountDownLatch等。
核心组成:
1. 同步状态(state)
private volatile int state; // 0表示未锁定,>0表示锁定
2. CLH队列(双向链表)
static final class Node {
volatile Node prev; // 前驱
volatile Node next; // 后继
volatile Thread thread; // 等待的线程
volatile int waitStatus; // 等待状态:SIGNAL、CANCELLED等
}
工作流程(以ReentrantLock为例):
加锁:
graph TD
A[调用lock] --> B{CAS设置state=1}
B -->|成功| C[获取锁成功]
B -->|失败| D[加入CLH队列尾部]
D --> E[park阻塞当前线程]
E --> F[等待被唤醒]
解锁:
graph TD
A[调用unlock] --> B[state减1]
B --> C{state == 0?}
C -->|是| D[唤醒队列头节点的后继]
C -->|否| E[可重入,继续持有]
D --> F[后继线程被唤醒]
F --> G[尝试CAS获取锁]
关键源码:
// 获取锁(独占模式)
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 1. 尝试获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 加入队列并阻塞
selfInterrupt();
}
// 释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) { // 1. 尝试释放
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 2. 唤醒后继节点
return true;
}
return false;
}
🔥 AQS的核心思想:
- 模板方法模式: AQS定义框架,子类实现tryAcquire/tryRelease
- CLH队列: 用于线程排队等待
- CAS+volatile: 保证并发安全
- LockSupport: park/unpark实现线程阻塞和唤醒
Q8:ConcurrentHashMap在JDK 1.7和1.8的实现有什么区别?
标准答案:
| 维度 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | Segment数组 + HashEntry数组 | Node数组 + 链表/红黑树 |
| 锁粒度 | Segment分段锁 | CAS + synchronized(锁桶) |
| 并发度 | Segment数量(默认16) | 桶数量(更高) |
| 扩容 | 单个Segment扩容 | 整体扩容,支持并发 |
| 性能 | 好 | 更好 |
JDK 1.7的Segment分段锁:
ConcurrentHashMap
└── Segment[] (16个)
├── Segment[0] (ReentrantLock)
│ └── HashEntry[]
├── Segment[1]
└── ...
每个Segment是一个独立的锁,最多支持16个线程并发写。
JDK 1.8的Node + CAS:
// put操作的核心逻辑(简化)
final V putVal(K key, V value) {
Node<K,V>[] tab;
if ((tab = table) == null)
tab = initTable(); // 初始化
int hash = spread(key.hashCode());
int i = (n - 1) & hash; // 计算桶位置
if ((f = tabAt(tab, i)) == null) {
// 桶为空,CAS直接插入
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
} else {
// 桶不为空,synchronized锁住桶
synchronized (f) {
// 链表或红黑树插入
}
}
}
优势:
- 锁粒度更小(锁单个桶,而不是Segment)
- 无竞争时使用CAS,有竞争时才用synchronized
- JDK 1.6后synchronized性能提升,不再是重量级
Q9:如何实现一个简单的读写锁?(设计题)
思路分析:
读写锁特性:
- 读-读:不互斥
- 读-写:互斥
- 写-写:互斥
实现要点:
- 计数器:记录读锁数量和写锁持有状态
- 等待队列:写线程等待读锁释放,读线程等待写锁释放
- 条件变量:用于唤醒等待的线程
简化实现:
public class SimpleReadWriteLock {
private int readers = 0; // 当前读线程数
private int writers = 0; // 当前写线程数(0或1)
private int writeRequests = 0; // 等待写的线程数
// 获取读锁
public synchronized void lockRead() throws InterruptedException {
while (writers > 0 || writeRequests > 0) {
wait(); // 有写锁或有写请求,等待
}
readers++;
}
// 释放读锁
public synchronized void unlockRead() {
readers--;
notifyAll(); // 唤醒等待的写线程
}
// 获取写锁
public synchronized void lockWrite() throws InterruptedException {
writeRequests++;
while (readers > 0 || writers > 0) {
wait(); // 有读锁或写锁,等待
}
writeRequests--;
writers++;
}
// 释放写锁
public synchronized void unlockWrite() {
writers--;
notifyAll(); // 唤醒所有等待的线程
}
}
改进点:
- 防止写饥饿: 有写请求时,新的读请求不应该获取锁
- 可重入: 同一线程可以多次获取读锁或写锁
- 锁降级: 持有写锁时可以获取读锁,然后释放写锁
Q10:什么是StampedLock?和ReadWriteLock有什么区别?
标准答案:
StampedLock 是JDK 1.8引入的高性能读写锁,支持三种模式:
- 写锁(writeLock): 独占锁
- 悲观读锁(readLock): 和写锁互斥
- 乐观读(tryOptimisticRead): 不加锁,通过版本号检查数据一致性
核心优势: 乐观读模式性能极高(无锁)
代码示例:
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 乐观读
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 🔥获取乐观读票据
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 🔥检查票据是否有效
// 有写操作,升级为悲观读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 写锁
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
}
与ReadWriteLock的区别:
| 特性 | ReadWriteLock | StampedLock |
|---|---|---|
| 乐观读 | ❌不支持 | ✅支持(核心优势) |
| 可重入 | ✅支持 | ❌不支持 |
| 条件变量 | ✅支持 | ❌不支持 |
| 性能 | 好 | 更好(乐观读无锁) |
| 适用场景 | 一般读多写少 | 读远多于写 |
⚠️ 注意事项:
- StampedLock不可重入,容易死锁
- 不能用于可重入场景
- 读多写少且读操作非常频繁时才用
十、总结与延伸
10.1 核心要点回顾
🎯 五大核心知识点:
-
synchronized的本质
- 基于对象头的Monitor实现
- JDK 1.6引入锁升级:偏向锁 → 轻量级锁 → 重量级锁
- 自动释放,异常安全
-
ReentrantLock的优势
- 基于AQS实现,功能更丰富
- 支持tryLock、lockInterruptibly、公平锁
- 必须手动释放,需在finally中unlock
-
锁的分类体系
- 悲观锁 vs 乐观锁(CAS)
- 独占锁 vs 共享锁(读写锁)
- 公平锁 vs 非公平锁
- 可重入锁 vs 不可重入锁
-
性能优化技巧
- 减小锁粒度
- 锁分段(ConcurrentHashMap)
- 读写锁分离
- 使用并发工具类(AtomicInteger、ConcurrentHashMap)
-
避免死锁
- 固定加锁顺序
- 使用tryLock超时
- 减少锁持有时间
- jstack检测死锁
10.2 技术选型指南
graph TD
A[需要并发控制] --> B{简单原子操作?}
B -->|是| C[AtomicXXX]
B -->|否| D{需要高级特性?}
D -->|不需要| E[synchronized]
D -->|需要| F{什么特性?}
F -->|超时/中断/公平| G[ReentrantLock]
F -->|读多写少| H[ReadWriteLock]
F -->|读特别多| I[StampedLock]
style C fill:#90EE90
style E fill:#87CEEB
style G fill:#FFD700
style H fill:#FFA07A
style I fill:#DDA0DD
决策树:
- 简单计数、标志位 →
AtomicInteger、volatile - 简单同步,代码简洁 →
synchronized - 需要超时、中断、公平性 →
ReentrantLock - 读多写少(10:1以上) →
ReentrantReadWriteLock - 读远多于写(100:1以上) →
StampedLock - 集合类并发 →
ConcurrentHashMap、CopyOnWriteArrayList - 线程协作 →
CountDownLatch、CyclicBarrier、Semaphore
10.3 相关技术栈
深入学习方向:
1. Java并发包(J.U.C)
java.util.concurrent.locks:Lock接口、Conditionjava.util.concurrent.atomic:原子类java.util.concurrent:并发容器、线程池、Future
2. JVM内存模型
- happens-before规则
- volatile的内存语义
- final的内存语义
3. 并发工具
- CountDownLatch:等待多个线程完成
- CyclicBarrier:多个线程互相等待
- Semaphore:控制并发访问数量
- Phaser:多阶段同步
4. 分布式锁
- Redis分布式锁(SET key NX EX)
- Zookeeper分布式锁(临时顺序节点)
- Redisson框架
5. 无锁编程
- CAS(Compare And Swap)
- ABA问题(AtomicStampedReference)
- Disruptor框架(无锁队列)
10.4 推荐阅读
书籍:
- 《Java并发编程实战》(Brian Goetz)—— 必读经典
- 《Java并发编程的艺术》(方腾飞)—— 深入源码
- 《深入理解Java虚拟机》(周志明)—— 理解底层
源码阅读:
java.util.concurrent.locks.ReentrantLockjava.util.concurrent.locks.AbstractQueuedSynchronizerjava.util.concurrent.ConcurrentHashMap
工具:
- jstack:死锁检测
- jconsole:线程监控
- VisualVM:性能分析
- Arthas:在线诊断
🎉 结语
Java的锁机制看似复杂,但掌握了底层原理后,你会发现它们都是为了解决并发安全和性能这两个核心问题。
面试建议:
- 基础题: 必须秒答,synchronized、volatile、死锁是高频考点
- 进阶题: 理解原理,能说清楚AQS、锁升级、ConcurrentHashMap
- 高级题: 有自己的思考,能对比不同方案的优劣,结合项目经验
实战建议:
- 优先使用JDK提供的并发工具类,不要重复造轮子
- 简单场景用synchronized,复杂场景用ReentrantLock
- 读多写少用读写锁,别把所有场景都用独占锁
- 性能测试要在真实环境下进行,不要过早优化
最后一句话: 锁是为了解决并发问题,但最好的锁是不用锁!能用无锁方案(Atomic、CAS、ThreadLocal)的场景,就别加锁。🚀
文章完成时间: 2025年 适用JDK版本: JDK 8 ~ JDK 17 作者建议: 建议配合实际代码调试加深理解,尤其是AQS和ConcurrentHashMap的源码