16.ReentrantLock全解读

这篇具有很好参考价值的文章主要介绍了16.ReentrantLock全解读。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人。

经历了AQS的前世和今生后,我们已经知道AQS是Java中提供同步状态原子管理,线程阻塞/唤醒,以及线程排队功能的同步器基础框架。那么我们今天就来学习通过AQS实现的ReentrantLock。按照惯例,先来看3道关于ReentrantLock的常见面试题:

  • 什么是ReentrantLock?

  • ReentrantLock内部原理是怎样的?如何实现可重入性?

  • ReentrantLocksynchronized有什么区别?该如何选择?

接下来,我会尽可能的通过剖析源码的方式为大家解答以上的题目。

ReentrantLock是什么?

ReentrantLock译为可重入锁,在《一文看懂并发编程中的锁》中我们解释过锁的可重入特性:同一线程可以多次加锁,即可以重复进入被锁定的逻辑中

Doug Lea是这样描述ReentrantLock的:

A reentrant mutual exclusion {@link Lock} with the same basic behavior and semantics as the implicit monitor lock accessed using {@code synchronized} methods and statements, but with extended capabilities.

“A reentrant mutual exclusion Lock”说明ReentrantLock除了具有可重入的特性,还是一把互斥锁。接着看后面的内容,ReentrantLock与使用synchronized方法/语句有相同的基本行为和语义。最后的" but with extended capabilities"则表明了ReentrantLock具有更好的拓展能力。

那么可重入互斥锁就是ReentrantLock的全部吗?别急,我们接着往后看:

The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order.

ReentrantLock提供了公平/非公平两种模式默认非公平模式,可以通过构造器参数指定公平模式。

好了,目前为止我们已经对ReentrantLock有了比较清晰的认知了,按照《一文看懂并发编程中的锁》中的分类,ReentrantLock本质是互斥锁,具有可重入特性,此外ReentrantLock还实现了公平和非公平两种模式

ReentrantLock怎么用?

ReentrantLocak的使用非常简单:

ReentrantLock lock = new ReentrantLock();
lock.lock();
// 业务逻辑
lock.unlock();

通过无参构造器创建ReentrantLock对象后,调用lockunlock进行加锁和解锁的操作。除了无参构造器外,ReentrantLock还提供了一个有参构造器:

// 无参构造器
public ReentrantLock() {
  sync = new NonfairSync();
}

// 有参构造器
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

FairSyncNonfairSyncReentrantLock的内部类,可以通过构造器来指定ReentrantLock的公平模式或非公平模式。具体实现我们先按下不表,先来看ReentrantLock中提供的其它方法。

加锁方法

除了常用的lock外,ReentrantLock还提供了3个加锁方法:

// 尝试获取锁
public boolean tryLock();

// 尝试获取锁,否则排队等候指定时间
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException;

// 尝试获取锁
public void lockInterruptibly() throws InterruptedException;

tryLock直接尝试获取锁,特点在于竞争失败时直接返回false,不会进入队列等待。重载方法tryLock(long timeout, TimeUnit unit)增加的在队列中的最大等待时间,如果锁竞争失败,会加入到等待队列中,再次尝试获取锁,直到超时或中断

lockInterruptibly的特点是,调用thread.interrupt中断线程后抛出InterruptedException异常,结束竞争。虽然lock也允许中断线程,但它并不会抛出异常

其他方法

除了常用的加锁方法外,ReentrantLock还提供了用于分析锁的方法:

方法声明 作用
public int getHoldCount() 返回当前线程持有锁的次数,即当前线程重入锁的次数
public final int getQueueLength() 返回等待获取锁的线程数量估算值
public final boolean hasQueuedThread(Thread thread) 查询当前线程是否在等待获取锁
public final boolean hasQueuedThreads() 是否有线程在等待获取锁
public final boolean isFair() 是否为公平锁
public boolean isHeldByCurrentThread() 当前线程是否持有锁
public boolean isLocked() 锁是否被线程持有,即锁是否被使用
public Condition newCondition() 创建条件对象
public int getWaitQueueLength(Condition condition) 等待在该条件上的线程数量
public boolean hasWaiters 是否有线程在等待在该条件上

ReentrantLock源码分析

接下来,我们通过源码来分析ReentrantLock的公平/非公平模式,以及重入性的实现原理,并对比不同的加锁方法的实现差异。

ReentrantLock的结构

我们先来来了解下ReentrantLock的结构:

public class ReentrantLock implements Lock, java.io.Serializable {

  private final Sync sync;
  
  // 同步器
  abstract static class Sync extends AbstractQueuedSynchronizer {}
  
  // 非公平模式同步器
  static final class NonfairSync extends Sync {}
  
  // 公平模式同步器
  static final class FairSync extends Sync {}
}

ReentrantLock仅仅实现了Lock接口,并没有直接继承AbstractQueuedSynchronizer,其内部类Sync继承AbstractQueuedSynchronizer,并提供了FairSyncNonfairSync两种实现,分别是公平锁和非公平锁。

公平/非公平模式

我们已经知道,可以指定不同的参数来创建公平/非公平模式的ReentrantLock,反应到源码中是使用不同的Sync的实现类:

public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

并且在加锁/解锁操作中,均由Sync的实现类完成,ReentrantLock只是对Lock接口的实现:

public class ReentrantLock implements Lock, java.io.Serializable {
  public void lock() {
    sync.acquire(1);
  }
  
  public void unlock() {
    sync.release(1);
  }
}

先来回想下《AQS的今生,构建出JUC的基础》中的acquire方法:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  
  public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
      selfInterrupt();
    }
  }
}

AQS自身仅实现了将线程添加到等待队列中的acquireQueued方法,而预留了获取锁的tryAcquire方法。

那么我们不难想到,ReentrantLock的作用机制:继承自AQS的Sync,实现了tryAcquire方法来获取锁,并借助AQS的acquireQueued实现排队的功能,而ReentrantLock的公平与否,与tryAcquire的实现方式是息息相关的。

公平锁FairSync

FairSync非常简单,仅做了tryAcquire方法的实现:

static final class FairSync extends Sync {
  @ReservedStackAccess
  protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取同步状态,AQS实现
    int c = getState();
    // 判断同步状态
    // c == 0时,表示没有线程持有锁
    // c != 0时,表示有线程持有锁
    if (c == 0) {
      // hasQueuedPredecessors判断是否有已经在等待锁的线程
      if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
      }
    } else if (current == getExclusiveOwnerThread()) {
      // 线程重入,同步状态+1
      int nextc = c + acquires;
      if (nextc < 0) {
        throw new Error("Maximum lock count exceeded");
      }
      // 更新同步状态
      setState(nextc);
      return true;
    }
    return false;
  }
}

c == 0时,锁未被任何线程持有,通过hasQueuedPredecessors判断是否已经有等待锁的线程,如果没有正在等待的线程,则通过compareAndSetState(0, acquires)尝试替换同步状态来获取锁;当c != 0时,锁已经被线程持有,通过current == getExclusiveOwnerThread判断是否为当前线程持有,如果是则认为是重入,执行int nextc = c + acquires,更新同步状态setState(nextc),并返回成功。

FairSync的公平性体现在获取锁前先执行hasQueuedPredecessors,确认是否已经有线程在等待锁,如果有则tryAcquire执行失败,默默的执行AQS的acquireQueued加入等待队列中即可。

非公平锁NonfairSync

NonfairSync也只是做了tryAcquire的实现,而且还只是掉用了父类的nonfairTryAcquire方法:

static final class NonfairSync extends Sync {
  protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
  }
}

abstract static class Sync extends AbstractQueuedSynchronizer {
  
  @ReservedStackAccess
  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) {
        throw new Error("Maximum lock count exceeded");
      }
      setState(nextc);
      return true;
    }
    return false;
  }
}

nonfairTryAcquireFairSync#tryAcquire简直是一模一样,忽略方法声明,唯一的差别就在于,当c == 0时,nonfairTryAcquire并不会调用hasQueuedPredecessors确认是否有线程正在等待获取锁,而是直接通过compareAndSetState(0, acquires)尝试替换同步状态来获取锁。

NonfairSync的不公平体现在获取锁前不会不会确认是否有线程正在等待锁,而是直接获取锁,如果获取失败,依旧会执行AQS的acquireQueued加入等待队列。

可重入性的实现

《AQS的今生,构建出JUC的基础》中,提到过ReentrantLock的重入性依赖于同步状态state作为计数器的特性实现,在公平锁FairSync和非公平锁NonfairSync的实现中我们也看到,线程重入时会执行同步状态+1的操作:

int nextc = c + acquires;
setState(nextc);

既然lock操作中有同步状态+1的操作,那么unlock操作中就一定有同步状态-1的操作:

public class ReentrantLock implements Lock, java.io.Serializable {
  public void unlock() {
    sync.release(1);
  }
  
  abstract static class Sync extends AbstractQueuedSynchronizer {
    @ReservedStackAccess
    protected final boolean tryRelease(int releases) {
      // 线程退出,同步状态-1
      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
      }
      boolean free = false;
      if (c == 0) {
        // 同步状态为0,锁未被持有,释放独占锁
        free = true;
        setExclusiveOwnerThread(null);
      }
      setState(c);
      return free;
    }
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final boolean release(int arg) {
    if (tryRelease(arg)) {
      Node h = head;
      if (h != null && h.waitStatus != 0){
        unparkSuccessor(h);
      }
      return true;
    }
    return false;
  }
}

tryRelease的实现并不复杂,同步状态-1后,如果同步状态为0,表示锁未被持有,修改锁的独占线程,然后更新同步状态。

我们再来看ReentrantLock的可重入性的实现,是不是非常简单了?判断是否是线程重入依赖的是getExclusiveOwnerThread方法,获取当前独占锁的线程,记录重入次数依赖的是同步状态作为计数器的特性。

现在能够理解为什么ReentrantLocklock要与unlock操作成对出现了吧?最后,提个小问题,为什么lockunlock操作中,只有当c == 0时的lock操作需要使用CAS?

加锁方法的差异

我们前面已经了解过ReentrantLock提供的4个加锁方法了,分别是:

  • public void lock(),最常用的加锁方法,允许中断,但不会抛出异常,加锁失败进入等待队列;

  • public void lockInterruptibly(),允许中断,抛出InterruptedException异常,加锁失败进入队列直到被唤醒或者被中断;

  • public boolean tryLock(),尝试直接加锁,加锁失败不会进入队列,而是直接返回false;

  • public boolean tryLock(long timeout, TimeUnit unit),尝试直接加锁,中断时抛出InterruptedException异常,加锁失败进入队列,直到指定时间内加锁成功,或者超时。

lock与lockInterruptibly

lock方法的调用:

public class ReentrantLock implements Lock, java.io.Serializable {
  public void lock() {
    sync.acquire(1);
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
      selfInterrupt();
    }
  }
}

lockInterruptibly方法的调用:

public class ReentrantLock implements Lock, java.io.Serializable {
  public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final void acquireInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    
    if (!tryAcquire(arg)) {
      doAcquireInterruptibly(arg);
    }
  }
}

可以看到,差异主要体现在acquireQueueddoAcquireInterruptibly的实现上:

final boolean acquireQueued(final Node node, int arg) {
  boolean interrupted = false;
  try {
    for (;;) {
      final Node p = node.predecessor();
      if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null;
        return interrupted;
      }
      // 当parkAndCheckInterrupt为true时,修改interrupted标记为中断
      if (shouldParkAfterFailedAcquire(p, node))
        interrupted |= parkAndCheckInterrupt();
    }
  } catch (Throwable t) {
    cancelAcquire(node);
    if (interrupted)
      selfInterrupt();
    throw t;
  }
}

private void doAcquireInterruptibly(int arg) throws InterruptedException {
  final Node node = addWaiter(Node.EXCLUSIVE);
  try {
    for (;;) {
      final Node p = node.predecessor();
      if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null;
        return;
      }
      // 当parkAndCheckInterrupt为true时,抛出异常
      if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
        throw new InterruptedException();
    }
  } catch (Throwable t) {
    cancelAcquire(node);
    throw t;
  }
}

从源码上来看,差异体现在对parkAndCheckInterrupt结果的处理方式上,acquireQueued只标记中断状态,而doAcquireInterruptibly直接抛出异常。

tryLock与它的重载方法

public boolean tryLock()的实现非常简单:

  public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

直接调用Sync#nonfairTryAcquire,在前面非公平锁的内容中我们已经知道nonfairTryAcquire只是进行了一次非公平的加锁尝试,如果没有调用AQS的acquireQueued不会加入到等待队列中。

tryLock的重载方法也并不复杂,按照之前的习惯,应该是有着特殊的acquireQueued实现:

public class ReentrantLock implements Lock, java.io.Serializable {
  public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
  }
  
  private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (nanosTimeout <= 0L)
      return false;
    // 计算超时时间
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    try {
      for (;;) {
        final Node p = node.predecessor();
        if (p == head && tryAcquire(arg)) {
          setHead(node);
          p.next = null;
          return true;
        }
        // 判断超时时间
        nanosTimeout = deadline - System.nanoTime();
        if (nanosTimeout <= 0L) {
          cancelAcquire(node);
          return false;
        }
        
        // 调用LockSupport.parkNanos暂停指定时间
        if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
          LockSupport.parkNanos(this, nanosTimeout);
        
        // 线程中断抛出异常
        if (Thread.interrupted())
          throw new InterruptedException();
      }
    } catch (Throwable t) {
      cancelAcquire(node);
      throw t;
    }
  }
}

public boolean tryLock(long timeout, TimeUnit unit)的特性依赖于LockSupport.parkNanos暂停线程指定时间的能力。另外,我们可以注意到在判断是否需要park时,对nanosTimeoutSPIN_FOR_TIMEOUT_THRESHOLD的判断:

  • nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响小于自旋nanosTimeout纳秒;

  • nanosTimeout < SPIN_FOR_TIMEOUT_THRESHOLD时,认为一次park和upark对性能的影响大于自旋nanosTimeout纳秒。

到这里我们就把4个加锁方法的差异讲完了,大体逻辑是相似的(如,唤醒头节点),只是为了实现某些特性添加了一些细节,大家可以认真阅读源码,很容易就能看出差异。

结语

关于ReentrantLock的内容到这里就结束了,因为已经把AQS的部分单独拆了出来,所以今天并没有太复杂的内容。大家的重点可以放在ReentrantLock是如何借助AQS实现公平/非公平模式,以及可重入的特性上,诸如getHoldCountisFair这类方法,相信大家已经能够想象到是如何实现的了,可以结合源码验证自己的想法。

最后,希望今天的内容能够帮助你更清晰的理解ReentrantLock,如果文章中出现错误,也希望大家不吝赐教。


好了,今天就到这里了,Bye~~文章来源地址https://www.toymoban.com/news/detail-434793.html

到了这里,关于16.ReentrantLock全解读的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 操作系统的“冷板凳”要坐多久?万字长文解读16年开源老兵的坚持

    想知道内核研发是怎样的体验?操作系统的“冷板凳”得坐多久才有春天?本文对话龙蜥社区理事长马涛,畅所欲言聊开源,一起来看看那些开源润物细无声背后的故事以及龙蜥社区运营的道法术。 高门槛的 Linux 内核研发,如何支棱起来? 提问:首先想请马涛聊一聊自己的

    2023年04月09日
    浏览(42)
  • 【Spring教程16】Spring框架实战:详解解读AOP配置管理中AOP切入点表达式和通知类型

    欢迎大家回到《 Java教程之Spring30天快速入门》,本教程所有示例均基于Maven实现,如果您对Maven还很陌生,请移步本人的博文《 如何在windows11下安装Maven并配置以及 IDEA配置Maven环境》,本文的上一篇为《 AOP的工作流程和AOP的核心概念》 前面的案例中,有涉及到如下内容: 对于

    2024年02月04日
    浏览(40)
  • 面试之ReentrantLock

    ReentrantLock实现了Lock接口,是一个可重入且独占式的锁,和Synchronized类似,不过ReentrantLock更灵活,更强大, 增加了轮询、超时、中断、公平锁和非公平锁等高级功能。 ReentrantLock 里面有一个内部类 ,Sync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实

    2024年02月12日
    浏览(37)
  • 《ReentrantLock与synchronized的区别》

    目录 Synchronized同步锁 synchronized的用法: ReentrantLock ReentrantLock和Synchronized的区别   Synchronized同步锁         使用Synchronized将一段代码锁起来,同一时间只允许一个线程访问。只有获取了这把锁的线程才能访问这段代码,并且只有一个线程拥有这把锁。这样就保证了代

    2024年02月16日
    浏览(35)
  • ReentrantLock介绍及使用(超详细)

    点击  Mr.绵羊的知识星球  解锁更多优质文章。 目录 一、介绍 1. 简介 2. 是什么类型的锁 3. 优点 4. 原理 5. 主要方法 6. 使用时注意事项 二、实际应用 1. 案例一 2. 案例二     ReentrantLock是一种基于AQS(Abstract Queued Synchronizer)框架的应用实现,是JDK中一种线程并发访问的同步手段

    2024年02月07日
    浏览(31)
  • 从ReentrantLock角度解析AQS

    是它,是它,就是它,并发包的基石; 闲来不卷,随便聊一点。 一般情况下,大家系统中至少也是JDK8了,那想必对于JDK5加入的一系列功能并不陌生吧。那时候重点加入了 java.util.concurrent 并发包,我们简称为JUC。JUC下提供了很多并发编程实用的工具类,比如并发锁lock、原子

    2023年04月14日
    浏览(40)
  • Java中的ReentrantLock实现原理

    在并发编程中,线程安全问题一直是非常重要的问题。Java中提供了多种解决线程安全问题的机制,其中一个比较常用的就是ReentrantLock。本文将介绍ReentrantLock的实现原理,从原子性、可见性等方面解释并结合源码分析,以便更好地理解在多线程环境下实现线程安全的过程。

    2024年02月01日
    浏览(44)
  • ReentrantLock与synchronized的区别

    synchronized 基于代码块的方式控制加锁与解锁 ReentrantLock 基于 lock(加锁) 与 unlock(解锁) 方式控制加锁与解锁 synchronized 只是单纯的加锁与解锁 ReentrantLock 对于 加锁成功 与synchronized 并无区别 ReentrantLock 对于 加锁失败 提供了额外的 tryLock方法 通过tryLock方法可以直接返回false, 由程

    2023年04月10日
    浏览(62)
  • 结合ReentrantLock来看AQS的原理

    ​ 队列同步器 AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。 同步

    2024年02月16日
    浏览(35)
  • AQS源码分析——以ReentrantLock为例

    AQS自身属性: Node属性: 1. 以ReentrantLock为例,其他类举一反三,方法lock() 2. Lock接口实现类,基本都是通过【聚合】了一个 【队列同步器】的子类完成线程访问控制的Sync实现lock;Sync继承AQS;Sync又有两个实现类。  公平锁与非公平锁的实现: 接下来以非公平锁为突破口:

    2024年02月10日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包