Loading

锁是怎么实现的

在Java中,锁通过控制线程对共享资源的访问权限来实现线程的阻塞与等待。其核心机制涉及对象监视器(Monitor)队列管理,以下是具体实现原理及流程:


一、内置锁(synchronized)的线程等待机制

1. 对象监视器(Monitor)模型

每个Java对象都与一个Monitor关联,由对象头中的Mark Word记录锁状态。

  • Mark Word结构:包含锁标志位(如01表示无锁,00表示轻量级锁,10表示重量级锁)和指向Monitor的指针。
  • Monitor核心组件
    • Entry Set(等待队列):线程尝试获取锁失败后进入此队列,处于阻塞状态(BLOCKED)。
    • Owner(持有者):当前持有锁的线程。
    • Wait Set(等待集合):线程调用wait()后释放锁并移入此队列,等待notify()唤醒。

2. 线程阻塞与唤醒流程

  • 加锁(monitorenter)
    1. 线程尝试通过CAS修改Mark Word获取锁。
    2. 若锁已被占用,线程进入Entry Set并阻塞(操作系统级互斥量Mutex实现)。
  • 释放锁(monitorexit)
    1. 持有锁的线程执行完毕或抛出异常时释放锁。
    2. JVM从Entry Set唤醒一个线程重新竞争锁。

示例

synchronized (obj) {
    // 临界区代码
}

编译后对应字节码monitorentermonitorexit指令,控制线程的阻塞与唤醒。


二、显式锁(ReentrantLock)的线程等待机制

显式锁基于AQS(AbstractQueuedSynchronizer)实现,通过内部队列和状态变量管理线程阻塞。

1. AQS核心组件

  • state变量:表示锁的占用状态(0未锁定,≥1被占用,支持可重入)。
  • CLH队列:双向链表结构的等待队列,存储阻塞线程的节点(Node)。

2. 线程阻塞与唤醒流程

  • 加锁(lock())
    1. 尝试通过CAS修改state字段获取锁。
    2. 若失败,将线程包装为Node加入CLH队列尾部,并调用LockSupport.park()挂起线程。
  • 释放锁(unlock())
    1. 持有锁的线程修改state为0。
    2. 唤醒队列中首个等待节点(通过LockSupport.unpark())。

示例

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock();
}

三、锁的线程等待策略对比

特性 synchronized ReentrantLock
阻塞机制 重量级锁依赖操作系统互斥量 基于AQS自旋+CAS,减少上下文切换
灵活性 不支持中断或超时 支持tryLock(timeout)和可中断锁
公平性 非公平锁(默认) 可选公平/非公平模式

四、线程等待的底层实现

1. 操作系统级阻塞

  • 重量级锁(synchronized):依赖内核态互斥量(Mutex),线程切换涉及用户态到内核态转换,开销较大。
  • 轻量级锁优化:通过CAS自旋减少内核态切换,若自旋超过阈值(默认10次)则升级为重量级锁。

2. 线程状态变化

  • BLOCKED:线程竞争锁失败后进入阻塞状态(如Entry Set中的线程)。
  • WAITING:调用wait()后线程进入等待集合(Wait Set),需显式唤醒。

五、避免线程饥饿与死锁

  1. 顺序加锁:多个线程按相同顺序获取锁,避免循环依赖。
  2. 超时机制:使用tryLock(timeout)防止无限等待。
  3. 死锁检测:通过工具(如jstack)分析线程堆栈,定位锁竞争路径。

总结

Java通过Monitor模型(synchronized)和AQS队列(ReentrantLock)实现线程的阻塞与唤醒。

  • synchronized:简单易用,但灵活性差,适合低竞争场景。
  • ReentrantLock:支持高级功能(如中断、超时),适合复杂并发需求。
    理解底层机制有助于优化高并发程序性能,避免死锁等问题。
posted @ 2025-05-13 14:47  我不想学编丿程  阅读(43)  评论(0)    收藏  举报