为什么都说Java锁难懂?看完这篇你就明白了

0 阅读23分钟

一、引入场景:那个让我失眠的线上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位锁标志
无锁hashcodeage001
偏向锁ThreadID(54位) + Epoch(2位)age101
轻量级锁指向栈中锁记录的指针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
    }
}

工作流程:

  1. 当线程第一次访问同步块时,在对象头的Mark Word中记录该线程ID
  2. 之后该线程再次进入时,只需检查Mark Word中的ThreadID是否为自己
  3. 如果是,直接进入;如果不是,说明有竞争,撤销偏向锁,升级为轻量级锁

优点: 几乎无性能损耗 缺点: 有竞争时撤销成本高

2. 轻量级锁(Lightweight Locking)

核心思想: 使用CAS操作避免使用互斥量(mutex)。

工作流程:

  1. 线程在栈帧中创建锁记录(Lock Record)
  2. 使用CAS将对象头的Mark Word复制到锁记录中
  3. 尝试用CAS将对象头的Mark Word替换为指向锁记录的指针
  4. 成功:获得锁;失败:自旋重试
  5. 自旋一定次数后仍失败:升级为重量级锁
// 轻量级锁适用场景:同步块执行速度快,线程交替执行
public class LightweightLockExample {
    private int count = 0;
    
    public void increment() {
        synchronized (this) {  // 短时间持有锁
            count++;
        }
    }
}

3. 重量级锁(Heavyweight Locking)

核心思想: 基于操作系统的互斥量(Mutex)实现,会导致线程在内核态和用户态之间切换。

工作流程:

  1. 未获得锁的线程进入阻塞状态(BLOCKED)
  2. 等待持有锁的线程释放后,由操作系统唤醒
  3. 涉及用户态和内核态切换,性能开销大
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最高❌数据不一致
synchronized150ms较高✅简单场景,JVM自动优化
ReentrantLock(非公平)120ms✅需要高级特性(tryLock, 中断)
ReentrantLock(公平)300ms较低✅严格顺序要求
AtomicInteger80ms很高✅简单原子操作

测试代码:

// 性能测试框架(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 时间/空间复杂度分析

操作synchronizedReentrantLock空间复杂度
加锁O(1)(偏向/轻量)
O(线程数)(重量)
O(1)(无竞争)
O(n)(CLH队列)
O(1)
解锁O(1)O(1)O(1)
等待队列-O(n)O(等待线程数)

七、易混淆概念对比

7.1 synchronized vs ReentrantLock

对比维度synchronizedReentrantLock
实现层面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, LockCAS, 版本号
冲突处理阻塞等待重试或失败
适用场景写多读少,冲突频繁读多写少,冲突少
典型应用数据库行锁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 → 死锁!
                // ...
            }
        }
    }
}

解决方案:

  1. 固定加锁顺序
  2. 使用tryLock设置超时
  3. 使用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的区别是什么?

标准答案:

  1. 功能:

    • volatile:保证可见性和有序性(禁止指令重排),但不保证原子性
    • synchronized:保证原子性、可见性、有序性
  2. 实现:

    • volatile:通过内存屏障,轻量级
    • synchronized:通过监视器锁,涉及线程阻塞
  3. 使用场景:

    • volatile:状态标志、双重检查锁定的单例模式
    • synchronized:复杂的同步逻辑
// volatile典型应用:状态标志
private volatile boolean running = true;

public void stop() {
    running = false;  // 立即对其他线程可见
}

public void run() {
    while (running) {  // 能及时看到running的变化
        // ...
    }
}

Q2:synchronized锁的是对象还是代码?

标准答案: synchronized锁的是对象(Object),不是代码。

  1. 修饰实例方法: 锁的是this对象
  2. 修饰静态方法: 锁的是Class对象(类名.class)
  3. 修饰代码块: 锁的是括号里的对象
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?

标准答案:

维度synchronizedReentrantLock
使用关键字,自动释放类,需手动释放
性能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. 互斥: 资源同时只能被一个线程持有
  2. 持有并等待: 持有至少一个资源,并等待获取其他资源
  3. 不可剥夺: 资源不能被强制释放
  4. 循环等待: 存在资源的循环等待链

经典死锁代码:

// 线程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.7JDK 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:如何实现一个简单的读写锁?(设计题)

思路分析:

读写锁特性:

  • 读-读:不互斥
  • 读-写:互斥
  • 写-写:互斥

实现要点:

  1. 计数器:记录读锁数量和写锁持有状态
  2. 等待队列:写线程等待读锁释放,读线程等待写锁释放
  3. 条件变量:用于唤醒等待的线程

简化实现:

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();  // 唤醒所有等待的线程
    }
}

改进点:

  1. 防止写饥饿: 有写请求时,新的读请求不应该获取锁
  2. 可重入: 同一线程可以多次获取读锁或写锁
  3. 锁降级: 持有写锁时可以获取读锁,然后释放写锁

Q10:什么是StampedLock?和ReadWriteLock有什么区别?

标准答案:

StampedLock 是JDK 1.8引入的高性能读写锁,支持三种模式:

  1. 写锁(writeLock): 独占锁
  2. 悲观读锁(readLock): 和写锁互斥
  3. 乐观读(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的区别:

特性ReadWriteLockStampedLock
乐观读❌不支持✅支持(核心优势)
可重入✅支持❌不支持
条件变量✅支持❌不支持
性能更好(乐观读无锁)
适用场景一般读多写少读远多于写

⚠️ 注意事项:

  • StampedLock不可重入,容易死锁
  • 不能用于可重入场景
  • 读多写少且读操作非常频繁时才用

十、总结与延伸

10.1 核心要点回顾

🎯 五大核心知识点:

  1. synchronized的本质

    • 基于对象头的Monitor实现
    • JDK 1.6引入锁升级:偏向锁 → 轻量级锁 → 重量级锁
    • 自动释放,异常安全
  2. ReentrantLock的优势

    • 基于AQS实现,功能更丰富
    • 支持tryLock、lockInterruptibly、公平锁
    • 必须手动释放,需在finally中unlock
  3. 锁的分类体系

    • 悲观锁 vs 乐观锁(CAS)
    • 独占锁 vs 共享锁(读写锁)
    • 公平锁 vs 非公平锁
    • 可重入锁 vs 不可重入锁
  4. 性能优化技巧

    • 减小锁粒度
    • 锁分段(ConcurrentHashMap)
    • 读写锁分离
    • 使用并发工具类(AtomicInteger、ConcurrentHashMap)
  5. 避免死锁

    • 固定加锁顺序
    • 使用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

决策树:

  1. 简单计数、标志位AtomicIntegervolatile
  2. 简单同步,代码简洁synchronized
  3. 需要超时、中断、公平性ReentrantLock
  4. 读多写少(10:1以上)ReentrantReadWriteLock
  5. 读远多于写(100:1以上)StampedLock
  6. 集合类并发ConcurrentHashMapCopyOnWriteArrayList
  7. 线程协作CountDownLatchCyclicBarrierSemaphore

10.3 相关技术栈

深入学习方向:

1. Java并发包(J.U.C)

  • java.util.concurrent.locks:Lock接口、Condition
  • java.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.ReentrantLock
  • java.util.concurrent.locks.AbstractQueuedSynchronizer
  • java.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的源码