ReentrantLock原理分析

ReentrantLock原理分析

一、包含的知识点

  • Lock
  • ReentrantLock 重入锁
  • ReentrantLock 加锁源码分析
  • ReentrantLock 解锁源码分析

二、Lock

​ 前面文章我们讲解了在多线程情况下,synchronized是如何保证线程安全的。在Java5之前, 只能通过synchronized来解决多线程并发安全问题,并且synchronized在某些场景下存在短板, Java5(含)之后出现的Lock,很好的弥补了synchronized存在的短板,它比synchronized更灵活。

synchronized短板:

  • synchronized是不可中断锁, lock是可中断锁
  • synchronized由JVM控制锁的获取/释放, lock由开发者控制锁的获取、释放

2.1 常见的锁的实现

​ Lock本质上是一个接口,它定义了释放锁、获取锁的抽象方法(意味着定义了一个规范),请看下面的截图

在这里插入图片描述

​ Lock的实现类有很多, 以下是几个常见的锁实现

  • ReentrantLock:表示重入锁,实现了 Lock 。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数

  • ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock接口。读写锁是一种适合读多写少的场景

    ReentrantReadWriteLock
    
      
    
  • StampedLock: JDK8引入的新的锁机制,是读写锁的一个改进版本,ReentrantReadWriteLock在大量读线程场景下, 可能引起写线程饥饿, StampedLock通过乐观锁策略, 使得乐观锁不会阻塞写线程。

2.2 Lock类关系图

在这里插入图片描述

​ 下面对Lock常见接口进行简单说明

  • void lock(), 如果锁可用就获得锁, 如果锁不可用就阻塞直到锁释放
  • void lockInterruptibly(), 和lock()方法类似, 但是阻塞线程可以中断, 抛出InterruptedException异常
  • boolean tryLock(), 非阻塞获取锁, 如果成功返回true
  • boolean tryLock(long timeout, TimeUnit timeUnit), 带有超时时间获取锁
  • void unlock(), 锁释放方法

三、ReentrantLock 重入锁

3.1 为什么需要重入锁?

​ 在进行讲解前请先看下下面代码

public class ReentrantDemo{
  public synchronized void method01(){
    System.out.println("method01");
    method02();
  }
  public void method02(){
    System.out.println("method02");
    synchronized (this){
    } 
  }
  public static void main(String[] args) {
    ReentrantDemo rd=new ReentrantDemo();
    new Thread(rd::demo).start();
  } 
}

​ 如果调用method01方法获取了当前对象的锁, 但是如果锁不具有重入性情况下, 执行method02时因无法获取对象锁会被阻塞, 会产生死锁, 而重入锁解决了这个问题,如果是同一个线程需要获取锁对象,会直接获取锁,避免死锁, 这就是锁重入性的必要性。

3.2 ReentrantReadWriteLock

​ 根据多线程是否能共享一把锁,可以将锁分为

  • 共享锁

    同一时刻允许多个线程进行访问

  • 排它锁

    同一时刻只允许一个线程进行访问,其它线程会被阻塞

ReentrantReadWriteLock维护了一对**读写锁(其中读是共享锁、写是排它锁)**, 因为大多数场景下读多于写, 使用读写锁 比 使用排它锁并发性、吞吐量高

private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;

读操作, 获取读锁, 在并发访问情况下, 读锁不会被阻塞, 因为读操作不会影响操作结果,

写操作, 需先获取写锁, 当已经有线程获取了写锁的情况下,其它写操作线程必须被阻塞 , 只有当锁释放后才能继续执行。

  • 读锁和读锁可以共享
  • 读锁和写锁不可以共享(排它)
  • 写锁和写锁不可以共享(排它)

3.3 AQS(AbstractQuenedSynchronized)

​ AQS(AbstractQueuedSynchronized)是Lock用来实现线程同步的核心组件, AQS从功能的角度分为:

  • 独占

    每次只能有一个线程持有锁, 比如: ReentrantLock 就是以独占方式实现的互斥锁

  • 共享

    允许多个线程同时获取锁, 并发访问资源, 比如:ReentrantReadWriteLock 读锁就是共享方式实现的共享锁

AQS内部实现

​ AQS内部维护了一个FIFO的双向队列, 这种结构含有两个指针, 分别指向当前节点的后继节点、前驱节点, 双向队列可以从任意一个节点方便的访问前驱、后继

  1. 当线程竞争锁失败后会被封装成Node, 然后加入到AQS队列中去,

  2. 当获取锁的线程释放锁后, 会从队列唤醒一个阻塞节点(即线程)

下面是Node节点包含元素的截图 和 双向队列结构图

在这里插入图片描述

在这里插入图片描述

AQS添加节点

​ 当出现竞争锁资源失败情况时, 需要构造一个Node节点, 然后添加到AQS队列中, 这里会涉及到下面的变化

  • 新的线程封装成Node节点追加到同步队列中, 当前节点设置prev节点、修改前置节点的后继节点指向自己
  • 通过CAS 将tail重新指向新的队尾节点

下面是添加节点的图示

在这里插入图片描述

AQS删除节点

​ head指向的节点表示当前获取锁的节点, 当前节点在释放同步状态时, 会唤醒后继节点, 如果后继节点获取锁成功, 会将当前节点设置为新的head节点(即设置为头节点)

  • 修改head节点指向下一个获得锁的节点
  • 新的获得锁的节点,将prev设置为null

Note: 设置head节点不需要通过CAS, 因为设置head节点是通过获得锁的线程来完成的, 而同步锁只能由一个线程获得, 不需要CAS保证。

下面是删除节点图示

在这里插入图片描述

四、ReentrantLock 加锁源码分析

4.1 加锁的入口

​ 根据ReentrantLock的使用方式和源码内容, 获取锁的入口是lock()方法, 代码如下,其中Sync是继承自AbstractQueuedSynchronizer的抽象类

public void lock() {
  sync.lock(); // private final Sync sync;
}

//Sync是继承自AbstractQueuedSynchronizer的抽象类
abstract static class Sync extends AbstractQueuedSynchronizer {
 		abstract void lock(); 
}  

​ AQS(AbstractQueuedSynchronizer) 是一个同步队列, 它能够实现线程的阻塞和唤醒, 但是并不具有业务能力, 根据不同场景Sync有两个具体的实现类

  • NonFairSync 非公平锁

    不管当前队列上是否存在其它等待线程, 新线程都会尝试抢占锁

  • FairSync

    所有线程都需要严格按照队列FIFO规则来获取锁

4.2 NonfairSync.lock()

​ 这里以非公平锁实现来说明lock, NonfairSync.lock()

  • 非公平锁和公平锁最大的区别在于, 非公平锁在抢占锁时不管有没有线程队列, 都会通过CAS进行锁获取操作
  • CAS成功, 表示成功获得了锁
  • CAS失败, 调用acquire(1) 走锁竞争逻辑
final void lock() {
  if (compareAndSetState(0, 1))
    setExclusiveOwnerThread(Thread.currentThread());
  else
    acquire(1);
}

compareAndSetState实现原理

首先看下CAS实现代码

protected final boolean compareAndSetState(int expect, int update) {
  // See below for intrinsics setup to support this
  return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

​ 这个操作的意思是, 如果当前内存中state的值和期望的expect值相同,则修改值为update, 更新成功返回true, 失败返回false,这个操作是原子的,不会出现线程安全问题。

​ state是AQS中的一个属性, 对于重入锁来说, 其含义表示

  • state = 0 ,表示无锁状态
  • state > 0 , 表示已经有线程获取到了锁, 但是因为ReentrantLock允许重入,如果同一个线程多次获得同步锁的时候,state会递增, 比如重入3次, 则state = 3; 在释放锁的时候, 同样需要释放3次, 直到state=0

​ compareAndSetState调用了底层unsafe类的compareAndSwapInt方法, 将state值和expect进行比较, 进行值修改, 下面是unsafe、compareAndSwapInt 说明

  • Unsafe 类, 提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障

  • compareAndSwapInt, 在unsafe.cpp 文件中可以找到 compareAndSwapInt方法, 下面是具体代码

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
      UnsafeWrapper("Unsafe_CompareAndSwapInt");
      oop p = JNIHandles::resolve(obj); // 将java对象解析为jvm的oop
      jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);//根据对象p和地址偏移找到地址
      return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 基于cas比较并替换, x表示需要替换的新值,addr表示state在内存中的地址, e表示预期值
    UNSAFE_END
    

4.3 AQS.acquire

​ 4.2 节我们分析了线程成功竞争到锁资源的处理方式(compareAndSetState() == true), 如果没有竞争到锁资源则调用acquire(1)方法, 本小节我们分析acquire方法。

​ 如果CAS不成功, 说明state != 0, 且当前线程非持有锁的线程, 又acquire是AQS中的方法, 它的主要逻辑是

  • 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
  • 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
  • acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁

下面是具体代码

public final void acquire(int arg) {
  if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

​ acquire是抽象方法, 作用是尝试获取锁,如果成功返回 true,不成功返回 false,它的具体实现如下,

NonfairSync.tryAcquire

protected final boolean tryAcquire(int acquires) {
  return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread(); // 获取当前执行线程
  int c = getState(); // 获取state的值
  if (c == 0) { // 表示无锁状态
    if (compareAndSetState(0, acquires)) { // cas 替换 state 的值,cas 成功表示获取锁成功
      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;
}

4.4 AQS.addWaiter

​ 当tryAcquire获取锁失败(即返回false), 则会调用 addWaiter方法, 将线程封装成Node节点, 并添加到等待队列中,下面是方法具体代码, 其中入参mode表示当前节点状态, 这里传递的是 Node.EXCLUSIVE, 表示独占状态, 意味着重入锁用到了AQS的独占锁功能。

private Node addWaiter(Node mode) {// mode, 表示当前节点状态, 这里传递的是 Node.EXCLUSIVE
  Node node = new Node(Thread.currentThread(), mode); // 把当前线程封装成node节点
  // Try the fast path of enq; backup to full enq on failure
  Node pred = tail; // 表示AQS队列中的队尾, 初始值为null
  if (pred != null) { // tail != null 说明队列中存在节点
    node.prev = pred; // 把当前节点node的prev指向tail
    if (compareAndSetTail(pred, node)) { // 通过cas方式将node加入AQS队列, 也就是设置为tail
      pred.next = node; // 设置成功后, 原tail节点的next指向当前node节点
      return node;
    }
  }
  enq(node); // tail==null或cas失败, 把node添加到同步队列
  return node;
}

​ 代码逻辑表示

  • 将当前线程封装成Node节点
  • 判断tail节点是否为空, 如果不为空, 将当前节点的prev节点指向tail节点, 并通过CAS操作把当前节点node添加到AQS队列, 最后将原tail节点的next节点指向node节点
  • 如果tail节点为空或CAS操作失败, 调用enq方法将node节点添加到同步队列(AQS)中

enq逻辑

private Node enq(final Node node) {
  for (;;) {
    Node t = tail;
    if (t == null) { // Must initialize
      if (compareAndSetHead(new Node()))
        tail = head;
    } else {
      node.prev = t;
      if (compareAndSetTail(t, node)) {
        t.next = node;
        return t;
      }
    }
  }
}

  • 如果tail节点为空, 创建空节点Node, 通过cas设置head节点, 并让tail、head置向同一节点
  • 如果tail节点不为空, 通过cas将当前节点设置为tail节点

这里我们通过三个线程获取锁的流程图来说明acquire方法锁竞争情况,

在这里插入图片描述

4.5 AQS.acquireQueued

​ 通过 addWaiter 方法把线程添加到链表后,接着会把 Node 作为参数传递给acquireQueued 方法,去竞争锁

final boolean acquireQueued(final Node node, int arg) {
  boolean failed = true;
  try {
    boolean interrupted = false;
    for (;;) {
      final Node p = node.predecessor(); //获得当前节点的prev节点
      if (p == head && tryAcquire(arg)) { // 如果p节点是头节点, 尝试竞争锁
        setHead(node); // 获得锁成功, 设置当前节点是head节点
        p.next = null; // help GC //把原head节点从链表中移除
        failed = false;
        return interrupted;
      }
      // p不是head节点或尝试获取锁失败
      if (shouldParkAfterFailedAcquire(p, node) &&
          parkAndCheckInterrupt())
        interrupted = true; // 返回当前线程在等待过程中有没有中断过
    }
  } finally {
    if (failed)
      cancelAcquire(node); //取消获取锁
  }
}
  • 获取当前节点的prev节点
  • 如果prev节点是head节点, 那么执行tryAcquire方法尝试获取锁
  • 抢占锁成功后, 把获得锁的节点设置为head, 并且移除原来的head节点
  • 如果获取锁失败, 执行shouldParkAfterFailedAcquire方法根据waitStatus决定是否需要挂起线程, 执行parkAndCheckInterrupt方法决定是否需要响应中断
  • 最后通过cancelAcquire取消获取锁的操作

shouldParkAfterFailedAcquire

​ 我们以4.4节中 图 – tryAcquire多线程竞争情况下线程入队流程 来分析说明, 如果ThreadA的锁还没有释放的情况下, AQS队列中 ThreadB、ThreadC竞争锁肯定会失败, 失败后会调用 shouldParkAfterFailedAcquire 方法, 下面是具体代码

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  int ws = pred.waitStatus; // 前置节点的waitStatus
  if (ws == Node.SIGNAL) // 如果前置节点为SIGNAL, 意味着需要等待其它前置节点线程被释放
    return true; // 可以将节点挂起
  if (ws > 0) { // ws>0, 意味着prev节点取消了排队, 直接移除这个节点就行
    do {
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0); //循环遍历, 从双向队列移除 CANCELLED 节点
    pred.next = node;
  } else {
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 利用CAS方式, 将prev状态设置为 SIGNAL
  }
  return false;
}

​ Node有5种状态, 下面是源码中对相关状态的说明

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
 * waitStatus value to indicate the next acquireShared should
 * unconditionally propagate
 */
static final int PROPAGATE = -3;


/**
         * Status field, taking on only the values:
         *   SIGNAL:     The successor of this node is (or will soon be)
         *               blocked (via park), so the current node must
         *               unpark its successor when it releases or
         *               cancels. To avoid races, acquire methods must
         *               first indicate they need a signal,
         *               then retry the atomic acquire, and then,
         *               on failure, block.
         *   CANCELLED:  This node is cancelled due to timeout or interrupt.
         *               Nodes never leave this state. In particular,
         *               a thread with cancelled node never again blocks.
         *   CONDITION:  This node is currently on a condition queue.
         *               It will not be used as a sync queue node
         *               until transferred, at which time the status
         *               will be set to 0. (Use of this value here has
         *               nothing to do with the other uses of the
         *               field, but simplifies mechanics.)
         *   PROPAGATE:  A releaseShared should be propagated to other
         *               nodes. This is set (for head node only) in
         *               doReleaseShared to ensure propagation
         *               continues, even if other operations have
         *               since intervened.
         *   0:          None of the above
         *
         * The values are arranged numerically to simplify use.
         * Non-negative values mean that a node doesn't need to
         * signal. So, most code doesn't need to check for particular
         * values, just for sign.
         *
         * The field is initialized to 0 for normal sync nodes, and
         * CONDITION for condition nodes.  It is modified using CAS
         * (or when possible, unconditional volatile writes).
         */
  • CANCELLED: 即结束状态, 同步队列中的线程等待超时或者被中断, 会从同步队列中取消该Node节点,进入该状态的节点状态不会再发生变化
  • SIGNAL: 只要前置节点释放锁, 就会通知标志为SIGNAL状态的后续线程节点
  • CONDITION: 和Condition有关系
  • PROPAGATE: 共享模式下, PROPAGATE状态的线程处于可运行状态
  • 0: 初始状态

​ 有了上面的知识, 我们来分析shouldParkAfterFailedAcquire方法, 确定ThreadB竞争锁失败后是否需要被挂起

  • 如果ThreadB的prev节点状态为SIGNAL,那么表示当前线程可以被挂起
  • 通过do…while 方式将状态为CANCELLED的节点移除
  • 修改prev节点的状态SIGNAL, 返回false(false 表示不用挂起, true 表示需要通过parkAndCheckInterrupt方法挂起)

parkAndCheckInterrupt

​ 方法作用, 对当前线程进行挂起操作

private final boolean parkAndCheckInterrupt() {
  LockSupport.park(this);
  return Thread.interrupted();
}
  1. LockSupport.park , 挂起当前线程

  2. LockSupport.park , 返回当前线程是否被其他线程触发过中断请求(即thread.interrupt()), 如果有触发过中断请求,那么这个方法会返回当前的中断标识为true,并且对中断标识进行复位,标识已经响应过中断请求

五、ReentrantLock 解锁源码分析

5.1 释放锁的入口

​ 根据ReentrantLock使用方式和源码分析, 释放锁的入口是unlock(), 代码如下,其中Sync是继承自AbstractQueuedSynchronizer的抽象类

public void unlock() {
  sync.release(1);
}

public final boolean release(int arg) {
  if (tryRelease(arg)) { // 释放锁
    Node h = head; // 释放锁成功, 得到AQS中head节点
    if (h != null && h.waitStatus != 0) // 如果head节点不为空,并且 waitStatus != 0, 唤醒后续节点
      unparkSuccessor(h);
    return true;
  }
  return false; // 释放锁失败
}

tryRelease

​ 这个方法作用是设置锁的状态, 通过将state值减掉参数arg值(这里是1), 查看结果状态是否为0,如果是0, 就将ownerThread设置为null, 这样其它线程就有机会竞争锁然后执行。

​ 在排它锁中, 加锁的时候state加1, 解锁的时候state减1, 同一个锁重入N次后, state被叠加为N, 解锁的时候只有unlock次数和重入(lock)次数相对应,ownerThread才会被设置为null, 同时只有这种情况下才会返回true

下面是tryRelease源码

protected final boolean tryRelease(int releases) {
  int c = getState() - releases; // state值减去释放参数releases
  if (Thread.currentThread() != getExclusiveOwnerThread()) //锁以独占的方式占有, 如果不是同一个线程, 抛出异常
    throw new IllegalMonitorStateException();
  boolean free = false; // 默认释放状态为false
  if (c == 0) { // 状态state=0, 表示其它线程可以获取锁
    free = true;
    setExclusiveOwnerThread(null); // 设置owenerThread=null
  }
  setState(c);
  return free;
}

unparkSuccessor

​ 这个方法的作用是, 在head节点不为空,并且 waitStatus != 0, 唤醒后续节点, 下面是具体的代码

/**
  * Wakes up node's successor, if one exists.
  */
private void unparkSuccessor(Node node) {
  int ws = node.waitStatus; // 获得head节点的状态
  if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0); // 设置head节点状态为0
  
  Node s = node.next;
  if (s == null || s.waitStatus > 0) { // 如果下一个节点为null 或 waitStatus>0 表示 CANCELLED状态
    s = null;
    for (Node t = tail; t != null && t != node; t = t.prev) // 从尾部开始扫描, 找到距离head最近的一个 waitStatus<0的节点
      if (t.waitStatus <= 0)
        s = t;
  }
  if (s != null) //next节点不为空, 直接唤醒这个线程
    LockSupport.unpark(s.thread);
}

​ 从上面的代码, 可以看出AQS队列是从尾部开始扫描的, 那么为什么需要从尾部开始扫描呢?

在4.4节, 我们分析了Node添加到队列的流程, 其中原尾节点的后继节点(next)的指定是最后一步, 如果从首节点遍历, 可能后继节点还没有构建好, 可能会出现问题, 但是prev节点已经构建好了, 所以这里从尾节点开始遍历。这里我们再整理一下节点Node添加到AQS流程

  • 将新的节点的prev指向tail
  • 通过cas将tail设置为新节点, 因为cas是原子操作所以能够保证线程安全性
  • 将原tail的next节点指向新的节点

​ 从操作流程看, 在cas操作之后, tail的next指定之前, 如果存在其它节点unlock操作, 从head节点开始遍历的话,可能存在节点“中断”问题

5.2 挂起的线程被唤醒后从何处执行

​ 在4.5节我们分析了在addWaiter之后需要通过acquireQueued尝试竞争锁, 锁释放后, 后续获取到锁的线程会从这里继续执行, 我们再看下这部分代码

final boolean acquireQueued(final Node node, int arg) {
  boolean failed = true;
  try {
    boolean interrupted = false;
    for (;;) {
      final Node p = node.predecessor(); //获得当前节点的prev节点
      if (p == head && tryAcquire(arg)) { // 如果p节点是头节点, 尝试竞争锁
        setHead(node); // 获得锁成功, 设置当前节点是head节点
        p.next = null; // help GC //把原head节点从链表中移除
        failed = false;
        return interrupted;
      }
      // p不是head节点或尝试获取锁失败
      if (shouldParkAfterFailedAcquire(p, node) &&
          parkAndCheckInterrupt())
        interrupted = true; // 返回当前线程在等待过程中有没有中断过
    }
  } finally {
    if (failed)
      cancelAcquire(node); //取消获取锁
  }
}

5.3 公平锁和非公平锁的区别

​ 锁的公平性是相对于获取锁的顺序而言的,如果是公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。下面是区别

  • 非公平锁在获取锁之前会先通过CAS进行抢占, 公平锁不会
  • 公平锁在tryAquire时会多执行hasQueuedPredecessors方法, 用于判断同步队列AQS中当前节点是否有前驱节点
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ReentrantLock是一个可重入锁,实现了Lock接口,并且支持重新进入的特性。当一个线程通过调用lock方法获取了锁之后,如果再次调用lock方法,该线程不阻塞,而是增加了重试次数。 在ReentrantLock内部,它维护了一个Sync内部类,该类实现了Lock接口的方法,并且通过调用AQS(AbstractQueuedSynchronizer)的Acquire方法来实现加锁的操作。具体而言,根据ReentrantLock初始化时选择的公平锁或非公平锁,Sync的Lock方法执行相关的内部类Lock方法,最终调用AQS的Acquire方法。 因此,ReentrantLock底层的原理是通过Sync内部类的Lock方法调用AQS的Acquire方法来实现加锁操作的。这一过程实现了可重入的特性,使得同一个线程可以多次获取锁而不阻塞。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [7-ReentrantLock底层原理分析](https://blog.csdn.net/weixin_45596022/article/details/113817683)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [ReentrankLock的底层原理](https://blog.csdn.net/qq_45974547/article/details/123486390)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值