Skip to content

锁机制全景

字数: 0 字 时长: 0 分钟

操作系统的锁

要想吃透 Java 中的锁,就必须先了解操作系统的锁。

互斥量 mutex

操作系统使用 互斥量(mutex)来防止多个线程同时进入临界区,确保共享资源的线程安全。Linux 系统使用 pthread_mutex_t 实现:

  • 加锁(lock):如果 pthread_mutex_t 值为 1 ,获取锁,并将值设为 0 ;如果锁被占用,线程阻塞,加入等待队列
  • 解锁(unlock):将 pthread_mutex_t 值设为 1,唤醒等待队列中的一个线程(或所有线程,取决于实现)
c
// Linux pthread 示例
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void critical_section() {
    pthread_mutex_lock(&mutex); // 阻塞直到获取锁
    // ... 临界区代码 ...
    pthread_mutex_unlock(&mutex);
}

条件变量 cond

条件变量 cond 必须与互斥量配合使用,允许线程在某个条件不满足时主动阻塞,并在条件满足时被唤醒

操作系统提供如下 api:

c
pthread_cond_wait(&cond,&mtx);//---内部处理流程:解锁—>阻塞休眠—>唤醒—>加锁 
pthread_cond_signal(&cond);//--激活一个等待线程。 
pthread_cond_broadcast(&cond);//--则激活所有等待线程

操作示例

c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool condition = false;

// 等待线程
void* waiter(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!condition) { // ⚠️ 必须用循环检查条件(防止虚假唤醒)
        pthread_cond_wait(&cond, &mutex); // 自动释放锁并阻塞
    }
    // 条件满足后重新持有锁
    pthread_mutex_unlock(&mutex);
    return NULL;
}

// 通知线程
void* notifier(void* arg) {
    pthread_mutex_lock(&mutex);
    condition = true;
    pthread_cond_signal(&cond); // 唤醒一个等待线程
    // pthread_cond_broadcast(&cond); // 唤醒所有等待线程
    pthread_mutex_unlock(&mutex);
    return NULL;
}

Synchronized

Synchronized 关键字是 JDK1.0 就自带的内置锁,利用 monitor 管程模式将同步机制(加锁/解锁)和线程通信(等待/通知)统一封装,避免开发者直接操作底层系统的API(如 pthread_mutex_t),降低复杂性。

当创建一个重量级锁时(jdk 1.6 引入了锁升级),JVM 创建 ObjectMonitor 对象,将对象头的 Mark Word 替换为指向 ObjectMonitor 的指针。

JVM 底层 C++ 代码

c++
class ObjectMonitor {
  void*   volatile _header;       // 存储对象头的 Mark Word(在重量级锁时被覆盖)
  volatile intptr_t  _count;      // 锁重入次数
  void*   volatile _owner;        // 持有锁的线程指针
  ObjectWaiter* volatile _EntryList; // 竞争锁的阻塞线程队列(Contending Threads)
  ObjectWaiter* volatile _WaitSet;   // 因 wait() 而等待的线程队列(Waiting Threads)
  volatile intptr_t  _waiters;    // 等待线程数
  // 其他字段...
};

Monitor 与 synchronized 的隐式绑定

java
//随便创建一个对象
Object object = new Object();
public void m1() {
    //对象锁
    synchronized (object) {
        System.out.println("hello synchronized lock");
    }
}

这段代码反编译后,可以看到 JVM 通过 monitorentermonitorexit 来实现同步(即 Monitor 机制)。

管程字节码.webp

monitorenter的底层逻辑

重量级锁阶段,调用 ObjectMonitor::enter() ,将线程加入 _EntryList 阻塞队列

c
void ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD;
  if (TryFastEnter(Self)) return; // 尝试快速获取(偏向锁/轻量级锁路径)
  slow_enter(Self); // 进入重量级锁逻辑(最终创建或绑定 ObjectMonitor)
}

Monitor 的线程协作

当调用 Java 中的 Object.wait() 方法时,JVM 会调用 ObjectMonitor::wait() 方法,将线程加入 _WaitSet 等待队列。本质是调用操作系统的 pthread_cond_wait() 让出 CPU,线程状态转换为 WAITING

java
synchronized (lock) {
    while (!condition) {
        lock.wait(); // ⚠️ 必须在 synchronized 块内调用
    }
}

当调用 Object.notify() 方法时, JVM 会调用 ObjectMonitor::notify() 方法,将线程从 _WaitSet 移到 _EntryList 参与锁竞争。本质是调用操作系统的 pthread_cond_signal(),唤醒一个等待线程参与锁竞争。

对象结构

想要理解 synchronized 的锁升级过程,必须先了解对象结构,特别是对象头。

在 64 位 JVM 中,普通对象的对象头通常为 128 位 (16 字节),数组对象另有 32 位记录数组长度。

  • Mark Word(64 bits):是对象头的核心部分,存储锁/GC/哈希码相关的所有元数据
  • Klass Pointer(64 bits):类型指针,指向方法区中的 Class 元数据

对象结构.webp

使用 JOL 查看 JVM 对象内存布局

JOL (Java Object Layout) 是 openjdk 团队提供的一个查看 JVM 对象内存布局的工具

JOL 依赖

xml
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.17</version>
</dependency>

查看 java.lang.Object 与自定义的 MyObject 对象的内存布局

java
public static void main(String[] args) {
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
        MyObject myObject = new MyObject();
        System.out.println(ClassLayout.parseInstance(myObject).toPrintable());
}

// 对象头 16 字节 + 实例数据 4 + 1 字节 = 21 个字节 + 对齐填充 = 24 个字节
class MyObject {  
    int age;
    boolean flag;
}

//java.lang.Object object internals:
//OFF  SZ   TYPE DESCRIPTION               VALUE
//  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
//  8   4        (object header: class)    0x00000e80
// 12   4        (object alignment gap)
//Instance size: 16 bytes
//Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
//com.ttdxg.juc.MyObject object internals:
//OFF  SZ      TYPE DESCRIPTION               VALUE
//  0   8           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
//  8   4           (object header: class)    0x0109c400
// 12   4       int MyObject.age              0
// 16   1   boolean MyObject.flag             false
// 17   7           (object alignment gap)
//Instance size: 24 bytes

JVM 默认开启压缩指针

shell
// 对象压缩指针
-XX:+UseCompressedOops
// 类压缩指针
-XX:+UseCompressedClassPointers

Synchronized 锁升级

Synchronized 锁升级过程中会通过对象头的 Mark Word 来记录当前锁的状态:

64位虚拟机.webp

1. 无锁

当对象未发送同步操作时,对象头中的锁标志为 01(算上偏向锁就是 001,JDK 18 已经完全移除了偏向锁,现代应用多核并行特性下,无竞争场景极少,偏向锁本身有开销却没有达到效果是一种负优化)

2. 轻量级锁

当线程首次尝试通过 synchronized 进入同步块时,JVM 在线程栈帧中创建锁记录 Lock Record,并尝试通过 CAS 操作将对象头中的 Mark Word 置为指向该锁记录的指针

  • 若成功,则将锁标志位置为 00
  • 若 CAS 操作失败达到上限,则升级为重量级锁

3. 重量级锁

轻量级锁 CAS 自旋次数达到上限时,升级为重量级锁,对象头中的 Mark Word 更新为指向互斥量的指针 (也就是 JDK 1.0 基于操作系统互斥量的 Synchronized 实现);锁标记位置为 10

锁升级的实际示例

java
Object lock = new Object();
// 线程1
new Thread(() -> {
    synchronized (lock) { // 首次进入,升级为轻量级锁
        // 业务逻辑
    }
}).start();

// 线程2
new Thread(() -> {
    synchronized (lock) { // 轻量级锁自旋失败后,膨胀为重量级锁
        // 业务逻辑
    }
}).start();

锁升级过程中 hashCode 去哪儿了?

原本对象头中存储了 hashCode ,但锁升级过程中,原本存储的 hashCode 位置被置为了指向锁的指针:

  • 无锁状态下:当对象的 hashCode() 方法第一次被调用时, hashCode 会存储在对象头的 MarkWord 中;计算过 hashCode 的对象无法升级为偏向锁,直接升级为轻量级锁
  • 轻量级锁状态下MarkWord 存储指向栈帧中的 Lock Record 的指针,Lock Record 中存储了 hashCode
  • 重量级锁状态下MarkWord 存储的重量级锁指针,指向的 Monitor 有字段记录非加锁状态下的 MarkWord ,锁释放后会写回对象头

锁升级过程中 hashCode 去哪儿了?

  1. 无锁状态下:当对象的 hashCode() 方法第一次被调用时, hashCode 会存储在对象头的 MarkWord 中;计算过 hashCode 的对象无法升级为偏向锁,直接升级为轻量级锁
  2. 偏向锁状态下:在偏向锁状态下调用 hashCode(),会立即撤销偏向锁,升级为重量级锁
  3. 轻量级锁状态下MarkWord 存储指向栈帧中的 Lock Record 的指针,Lock Record 中存储了 hashCode
  4. 重量级锁状态下MarkWord 存储的重量级锁指针,指向的 Monitor 有字段记录非加锁状态下的 MarkWord ,锁释放后会写回对象头

CAS 比较并交换

CAS (Compare And Swap) 比较并交换,是一种无锁原子操作,不需要传统锁限制,是 CPU 提供的原语指令 cmpxchg,它包含三个操作数:

  1. 内存位置 (V)
  2. 旧的值 (A)
  3. 更新值 (B)

CAS 是将工作内存的值和旧的值作比较,如果相同则更新内存位置的值为更新值,否则不更新或重试,重试的行为称为 自旋

CAS操作.webp

JAVA 通过Unsafe 类实现 CAS ,其内部方法是 native 方法,使用 C 的指针,可以直接操作内存。不过 JDK 23 就要弃用了,改为 VarHandle 类实现 CAS:

Unsafe.webp

CAS 自旋锁

java
public class SpinLock {
    // 原子引用(指向当前持有锁的线程,用于实现自旋锁)
    private AtomicReference<Thread> owner = new AtomicReference<>();
    /**
     * 尝试获取锁(自旋)
     */
    public void lock() {
        Thread current = Thread.currentThread();
        // 🔥 自旋:通过 CAS 尝试将 owner 从 null 置为当前线程
        while (!owner.compareAndSet(null, current)) {
            // 自旋中(CPU 忙等,适合短时间锁竞争)
        }
        // 成功获取锁后退出循环
    }

    /**
     * 释放锁
     */
    public void unlock() {
        Thread current = Thread.currentThread();
        // 只有锁持有者才能解锁(防止非法释放)
        owner.compareAndSet(current, null);
    }
}

ABA 问题

CAS 自旋比传统锁更高效,其高效性来自 CPU 硬件的直接支持,但也带来了 ABA 问题:

  1. 线程 1 从内存中取出 A
  2. 这时候线程 2 也从内存中取出 A ,并且线程 2 进行一些操作将值变为 B ,然后线程 2 又将内存中的数据变为 A
  3. 线程 1 进行 CAS 操作发现内存中仍然是 A ,以为数据没问题

使用 AtomicStampedReference 解决 ABA 问题

java
// 使用AtomicStampedReference
AtomicStampedReference<String> ref = new AtomicStampedReference<>("初始值", 0);

// 更新时检查值和版本号
int[] stampHolder = new int[1];
String current = ref.get(stampHolder);
if (ref.compareAndSet(current, "新值", stampHolder[0], stampHolder[0] + 1)) {
    System.out.println("更新成功");
}

AQS 抽象队列同步器

Doug Lee 提出 AQSAbstractQueuedSynchronizer,抽象的队列同步器)作为统一规范并简化了锁的实现,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是 JUC 中锁和同步组件实现的基石。ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatch 等都依赖 AQS。

ReentrantLock.webp

AQS 使用改进的 CLH 队列作为等待队列,是一种特殊的双向链表

CLH队列注释.webpCLH队列.webp

AQS 通过 state 状态位表示同步状态,通过 CAS 实现原子更新,AQS 的实质就是 state 状态位 + CLH 双端队列

java
// 状态操作方法
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

state状态位.webp

AQS 源码分析

接下来通过 ReentrantLock 的非公平锁为例,分析源码过程

总体流程

java
ReentrantLock.lock() 
    -> NonfairSync.lock()
        -> 直接尝试 CAS 抢锁(非公平性)
        -> 若抢锁成功:设置当前线程为独占所有者
        -> 若失败:调用 AQS.acquire(1)
            -> tryAcquire(): 非公平方式再次尝试获取锁
            -> addWaiter(): 将线程封装为 Node 加入 CLH 队列
            -> acquireQueued(): 自旋或阻塞线程,等待唤醒
  1. NonfairSync.lock() :尝试 CAS 操作去修改锁状态,如果成功则设置当前线程为独占线程;失败则进入 AQS 队列流程
java
final void lock() {
    if (compareAndSetState(0, 1))  // 直接 CAS 尝试修改锁状态(0 → 1)
        setExclusiveOwnerThread(Thread.currentThread()); // 成功则设置独占线程
    else
        acquire(1);  // 失败则进入 AQS 队列流程
}
  1. AQS.acquire(1) :在进入队列之前还会再调用 tryAcquire() 尝试获取锁,如果依然失败则加入 CLH 队列阻塞
java
public final void acquire(int arg) {
    if (!tryAcquire(arg) && //尝试获取锁
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //加入队列并阻塞
        selfInterrupt();
}
  1. tryAcquire() 尝试获取锁逻辑:会根据 state 状态选择是否通过 CAS 尝试获取锁,并且支持可重入锁
java
// AQS 中使用模板模式不提供实现
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
//ReentrantLock 进行提供具体实现
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

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;
        }
    } //如果锁已被持有,判断当前线程是否为锁持有者 (可重入锁逻辑)
    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;
}
  1. addWaiter():线程封装为 Node 并入队
java
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

// 入队 双向链表结构
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { //头节点未初始化
            // 初始化头节点 哨兵节点
            if (compareAndSetHead(new Node())) /
                tail = head;
        } else {
            node.prev = t;  
            if (compareAndSetTail(t, node)) { // CAS 插入队尾
                t.next = node;
                return t;
            }
        }
    }
}
  1. acquireQueued():自旋或阻塞线程,等待唤醒
java
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获得当前节点的前置节点
            final Node p = node.predecessor(); 
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • shouldParkAfterFailedAcquire():判断当前线程是否需要阻塞
java
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; //获取前置节点的等待状态 1 取消 -1 等待 -2 阻塞 
    if (ws == Node.SIGNAL) // SIGNAL (-1)状态,线程已经准备好,就等资源释放了
        return true;
    if (ws > 0) { // 前置节点状态为取消,则从队列中删除该节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { // CAS 设置前置节点为 SIGNAL (-1)状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  • parkAndCheckInterrupt():阻塞当前线程,等待唤醒
java
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

至此,其他线程已经放入 CLH 队列,并被 LockSupport.park(this) 阻塞

AQS队列.webp

  1. unlock() 释放资源
java
public void unlock() {
        sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        // h 头节点此时为哨兵节点 waitStatus = -1  true
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); 
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread); //哨兵节点的 next 节点被唤醒
}
  1. tryRelease()
java
protected final boolean tryRelease(int releases) { 
        int c = getState() - releases; // 1 - 1 = 0
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null); //释放锁
        }
        setState(c); // 锁状态重新设置为 0
        return free;
}

非公平锁情况

按照上面的流程, CLH 队列中的线程会排队 FIFO 唤醒,但是这是非公平锁,允许插队

AQS非公平锁.webp

线程取消流程

当线程因中断或超时放弃等待时会触发线程取消流程:将节点标记为取消状态,并尝试从 CLH 队列移除

java
private void cancelAcquire(Node node) {
    ...
    // --- 步骤1:找到有效前驱节点 ---
    Node pred = node.prev;
    // 跳过所有已被取消的前驱节点(waitStatus>0表示CANCELLED状态)
    while (pred.waitStatus > 0) {
        // 更新当前节点的前驱指针,形成新的链表关系
        node.prev = pred = pred.prev;
    }
    // predNext是前驱节点原来的下一跳(可能是当前节点)
    Node predNext = pred.next;
    // --- 步骤2:标记当前节点为取消状态 ---
    // 直接写入,无需CAS,因为必须确保标记成功
    node.waitStatus = Node.CANCELLED;
    // --- 步骤3:处理当前节点是队尾的情况 ---
    // 如果当前节点是队列尾部,尝试通过CAS更新尾指针到前驱节点
    if (node == tail && compareAndSetTail(node, pred)) {
        // 更新前驱节点的next指针为null(移除当前节点)
        compareAndSetNext(pred, predNext, null);
    } else {
        // --- 步骤4:非队尾节点的处理逻辑 ---
        int ws;  // 前驱节点的状态
        if (pred != head &&  // 前驱不是头节点(头节点为虚节点)
                ( // 检查前驱状态是否有效:
                        (ws = pred.waitStatus) == Node.SIGNAL ||  // 前驱状态已是SIGNAL
                                (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)) // 尝试设置为SIGNAL成功
                ) &&
                pred.thread != null) { // 前驱关联的线程存活
            // 获取当前节点的后继
            Node next = node.next;
            if (next != null && next.waitStatus <= 0) {
                // 将前驱的next指针直接绕过当前节点,指向其后继
                compareAndSetNext(pred, predNext, next);
            }
        } else {
            // 不满足条件时,唤醒后继线程(确保状态传播)
            unparkSuccessor(node);
        }
        // --- 步骤5:清理操作帮助GC ---
        // 将节点的next指向自己,断开外部引用链(防止内存泄漏)
        node.next = node; // help GC
    }
}

ReentrantLock

ReentrantLock 是 JUC 提供的可重入互斥锁,它提供了比 synchronized 关键字更灵活、更强大的锁操作能力。

  1. 可重入性:同一个线程可以多次获取同一把锁
java
lock.lock();
try {
    // 在锁内部可以再次获取同一把锁
    lock.lock(); 
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
} finally {
    lock.unlock();
}
  1. 公平性选择:支持公平和非公平两种模式
  2. 尝试获取锁tryLock()
  3. 可中断获取 lockInterruptibly()
  4. 条件变量 Condition 协助线程通信

ReentrantReadWriteLock 可重入读写锁

读写锁(ReentrantReadWriteLock) 是普通锁 (ReentryLock)的一种演进,主要在读多写少场景下实现更高的吞吐量。它通过读锁(共享锁)和写锁(排他锁)实现更高的并发性:

  • 读锁(ReadLock):允许多个线程同时读取共享资源,但阻塞所有写操作
  • 写锁(WriteLock):独占资源,同一时间仅允许一个线程执行写操作,阻塞所有读/写操作

在持有写锁的情况下,主动获取读锁,随后释放写锁的过程,将锁的权限从写锁降级为读锁的过程,称为锁降级,保证后续读到的数据是最新的数据。

java
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();       // 1. 获取写锁
try {
    // 更新数据...
    lock.readLock().lock();    // 2. 在持有写锁时获取读锁(锁降级关键步骤)
} finally {
    lock.writeLock().unlock(); // 3. 释放写锁(此时仍持有读锁)
}
// 如果不在释放写锁之前获取读锁,则没办法确定释放写锁之后自己能第一时间抢到读锁
// 如果自己没有第一时间抢到,则无法保证后续读到的数据是自己最新更新的数据        
try {
    // 安全读取数据...
} finally {
    lock.readLock().unlock();  // 4. 释放读锁
}

为什么不能锁升级?

线程A 持有读锁,尝试获取写锁,需等待所有读锁释放;若线程B也如此,会导致死锁!

锁饥饿

读多写少的情况下,大量线程尝试获取读锁,读写互斥导致读锁没有释放完的情况下,写锁永远无法获取到。

StampedLock 邮戳锁

邮戳锁是一种更高效的读写锁,在ReentrantReadWriteLock的基础上添加了乐观读模式(无锁化读,通过检查邮戳(时间戳)验证数据是否被修改)

java
StampedLock lock = new StampedLock();

// 获取写锁(返回一个邮戳标识)
long writeStamp = lock.writeLock();
try {
    // 执行写操作
} finally {
    lock.unlockWrite(writeStamp); // 释放写锁需要对应邮戳
}

// 获取悲观读锁(阻塞写锁)
long readStamp = lock.readLock();
try {
    // 执行读操作
} finally {
    lock.unlockRead(readStamp);
}

// 尝试乐观读(不阻塞写锁)
long optimisticStamp = lock.tryOptimisticRead();
// 读取数据...
if (!lock.validate(optimisticStamp)) { // 验证邮戳是否过期
    // 数据被修改,乐观读锁升级为悲观读锁
    readStamp = lock.readLock();
    try {
        // 重新读取数据
    } finally {
        lock.unlockRead(readStamp);
    }
}

Semaphore 信号量

Semaphore 信号量可以控制同时访问特定资源的线程数量,非常适合限制数据库连接数、文件操作数(防止内存溢出)等场景

java
Semaphore sem = new Semaphore(3); // 允许3个线程同时访问
sem.acquire(); // 获取许可
try {
    // 临界区代码
} finally {
    sem.release(); // 释放许可
}

CountDownLatch 计数器

CountDownLatch 计数器允许一个或多个线程等待,直到其他线程完成操作