对线面试官:Java中的锁机制

89 阅读8分钟

锁的定义和核心意义

在多线程并发编程中,锁是协调线程对共享资源访问的核心机制。其核心目标在于:

  1. 原子性:确保临界区代码的不可分割执行
  2. 可见性:保证共享变量的修改对其他线程立即可见
  3. 有序性:防止指令重排序导致的执行顺序问题

Java中的锁实现可分为两大类:

  • JVM内置锁:通过synchronized关键字实现
  • 显式锁java.util.concurrent.locks包下的各种锁实现

锁的分类

一、按照 线程是否阻塞 分类

类型核心思想典型实现适用场景
悲观锁假定一定会发生竞争,每次访问数据前先加锁synchronizedReentrantLock写操作多、竞争激烈场景
乐观锁假定不会发生竞争,通过版本号/CAS机制保证原子性AtomicIntegerStampedLock读多写少、低竞争场景

二、按照 公平性 分类

类型特点实现类性能对比
公平锁严格按请求顺序分配锁,避免线程饥饿new ReentrantLock(true)吞吐量较低,响应时间稳定
非公平锁允许插队获取锁,可能引发线程饥饿synchronized、默认ReentrantLock吞吐量高,响应时间波动较大

三、按照 可重入性 分类

类型特点实现示例优势
可重入锁同一线程可重复获取同一锁(通过计数器实现)ReentrantLocksynchronized避免递归调用时的死锁
不可重入锁同一线程多次获取锁会阻塞自身早期ThreadLocal的部分实现实现简单,但易导致死锁

四、按照 共享性 分类

类型特点典型实现使用场景
独占锁同一时刻只能被一个线程持有ReentrantLocksynchronized写操作等排他性场景
共享锁允许多个线程同时持有锁ReentrantReadWriteLock.ReadLock读操作等共享场景

五、按照 实现原理 分类

类型实现机制特点典型示例
自旋锁通过循环尝试CAS操作(while(!tryLock()))避免线程切换,但消耗CPUAtomicBoolean底层实现
适应性自旋锁根据前次等待时间动态调整自旋次数平衡CPU消耗与等待时间HotSpot JVM优化实现
CLH锁基于队列的公平锁实现(Craig, Landin, Hagersten)公平性好,空间效率高AQS底层实现
MCS锁基于显式链表的自旋锁(改良CLH)减少缓存同步开销部分JVM实现

六、按照 锁优化技术 分类

类型实现原理优化目标
偏向锁记录第一个获取锁的线程ID,后续无竞争直接访问消除无竞争时的同步开销
轻量级锁通过线程栈中的Lock Record进行CAS竞争减少重量级锁的系统调用开销
锁消除JIT编译器消除不可能存在共享竞争的锁减少不必要的同步操作
锁粗化合并多个相邻同步块为单个大同步块减少频繁加锁/解锁的开销

七、按照 锁粒度 分类

类型特点典型实现
粗粒度锁锁住整个数据结构或大范围资源Vector的全表锁
细粒度锁对数据结构的局部加锁ConcurrentHashMap的分段锁
无锁结构完全不使用锁,通过CAS实现线程安全CopyOnWriteArrayList

八、特殊用途锁

类型特点典型实现使用场景
条件锁通过Condition实现线程的精确唤醒ReentrantLock.newCondition()生产者-消费者模型
读写锁分离读锁(共享)与写锁(独占)ReentrantReadWriteLock读多写少场景
信号量控制同时访问资源的线程数量Semaphore连接池、限流场景
倒计时门闩等待多个线程完成指定操作CountDownLatch多线程任务协同

九、按 锁状态 分类

类型特点实现机制
可中断锁等待锁时能响应中断请求lockInterruptibly()
不可中断锁等待锁时不响应中断synchronized

十、按 锁持有时间 分类

类型特点典型场景
短期锁持有时间极短(纳秒级)自旋锁、CAS操作
长期锁持有时间较长(毫秒级以上)数据库事务锁

总结:锁的选择原则

  1. 竞争程度:低竞争用乐观锁,高竞争用悲观锁
  2. 读写比例:读多写少用读写锁,写多用独占锁
  3. 公平需求:严格顺序用公平锁,高吞吐用非公平锁
  4. 锁粒度:根据数据结构特点选择粗/细粒度锁
  5. 可重入性:递归调用必须用可重入锁

不同的锁类型在Java并发包中都有典型实现(如ReentrantLockStampedLock等),理解这些分类有助于在实际开发中选择最合适的同步机制。


锁的原理

synchronized的底层实现

synchronized的底层实现经历了多次优化,核心机制为对象头Mark Word

1. 对象内存布局

|--------------------------------------------------------------|
|       Object Header (64 bits)                |   Instance Data  | 
|------------------------|---------------------|------------------|
|  Mark Word (32bits)    |  Klass Word (32bits)|                  |
|---------------------------------------------------------------|

2. Mark Word结构(32位JVM)

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01              |       Normal       |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01      |       Biased       |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30                         | 00    | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30                 | 10    | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                               | 11    |    Marked for GC   |
|-------------------------------------------------------|--------------------|

3. 锁升级过程

  1. 偏向锁(Biased Locking)

    • 适用场景:单线程访问同步块
    • 原理:在Mark Word中记录线程ID
    • 优势:无竞争时的零成本加锁
  2. 轻量级锁(Lightweight Locking)

    • 适用场景:低竞争的多线程环境
    • 实现:通过CAS操作将Mark Word复制到线程栈中的Lock Record
  3. 重量级锁(Heavyweight Locking)

    • 适用场景:高竞争环境
    • 实现:通过操作系统的互斥量(mutex)实现
    • 特点:线程阻塞和唤醒涉及内核态切换

AQS(AbstractQueuedSynchronizer)核心原理

Java显式锁的基石,采用CLH队列变体实现同步控制。

1. 核心组件

// 同步状态
private volatile int state;

// CLH队列节点
static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
}

2. 关键方法

  • tryAcquire(int):尝试获取独占锁
  • tryRelease(int):尝试释放独占锁
  • tryAcquireShared(int):尝试获取共享锁
  • tryReleaseShared(int):尝试释放共享锁

3. 获取锁流程

         +---------------------+
         |  尝试获取锁(tryAcquire) |
         +----------+----------+
                    |
                    v
         +----------+----------+
         |      成功?         +--否--> 加入等待队列
         +----------+----------+
                    |
                    v
                 获取锁成功

ReentrantLock实现剖析

1. 核心结构

public class ReentrantLock implements Lock {
    private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        // 实现AQS的tryAcquire等方法
    }
    
    static final class NonfairSync extends Sync { /* 非公平实现 */ }
    static final class FairSync extends Sync { /* 公平实现 */ }
}

2. 公平锁 vs 非公平锁

特性公平锁非公平锁
获取顺序严格FIFO允许插队
吞吐量较低较高
实现差异hasQueuedPredecessors()检查直接尝试CAS获取

3. 可重入实现 通过维护当前持有锁的线程和重入次数:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

读写锁(ReentrantReadWriteLock)

1. 状态分割

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

// 高16位:读锁计数
// 低16位:写锁计数

2. 锁降级示例

readLock.lock();
try {
    if (!cacheValid) {
        writeLock.lock();  // 必须先释放读锁才能获取写锁
        try {
            // 更新缓存
            cacheValid = true;
        } finally {
            writeLock.unlock();
        }
    }
    // 使用缓存数据
} finally {
    readLock.unlock();
}

乐观锁与CAS

1. CAS原理

// 伪代码实现
bool compare_and_swap(int *addr, int expected, int new_value) {
    if (*addr == expected) {
        *addr = new_value;
        return true;
    }
    return false;
}

2. Java中的实现

public final class Unsafe {
    public final native boolean compareAndSwapInt(
        Object o, long offset, int expected, int x);
}

3. ABA问题解决方案 使用AtomicStampedReference

AtomicStampedReference<Integer> atomicRef = 
    new AtomicStampedReference<>(100, 0);

int[] stampHolder = new int[1];
int currentStamp = atomicRef.get(stampHolder);
atomicRef.compareAndSet(100, 101, currentStamp, currentStamp + 1);

锁性能优化策略

  1. 减少锁粒度

    • ConcurrentHashMap的分段锁设计
  2. 锁消除

    public String concat(String s1, String s2) {
        return s1 + s2;  // JIT编译器可能消除StringBuffer的同步
    }
    
  3. 锁粗化

    // 多次连续的加锁/解锁操作合并为单次操作
    synchronized(lock) {
        // 多个操作合并
    }
    
  4. 线程本地化

    • 使用ThreadLocal避免共享资源竞争

锁选择决策树

是否需要等待可中断? → 选择Lock
                ↓否
需要尝试非阻塞获取? → 选择tryLock()
                ↓否
需要公平性保证? → 选择公平锁
                ↓否
读多写少场景? → 选择读写锁
                ↓否
短期锁定? → 选择自旋锁
                ↓否
使用synchronized

死锁预防策略

  1. 顺序加锁法
  2. 超时机制
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            // 临界区
        } finally {
            lock.unlock();
        }
    }
    
  3. 资源排序法
  4. 死锁检测(使用jstack或JMX)

总结

Java的锁机制是一个从语言层面到JVM实现的多层次体系:

  • 内置锁:简单易用,自动管理
  • 显式锁:灵活可控,功能丰富
  • 原子变量:无锁编程的基础
  • 锁优化:适应不同场景的性能需求

正确选择和使用锁需要综合考虑:

  • 竞争激烈程度
  • 读写比例
  • 线程等待的容忍度
  • 系统吞吐量要求