16.ReentrantLock全解读

简介: 大家好,我是王有志。今天和大家一起聊聊ReentrantLock,它是我们最常见的基于AQS实现的互斥锁。

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

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

  • 什么是ReentrantLock?

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

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

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

Tips:本文提供了省流版题解

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~~

目录
相关文章
|
消息中间件 存储 网络协议
【Kafka】Kafka 性能高的原因分析
【4月更文挑战第5天】【Kafka】Kafka 性能高的原因分析
|
26天前
|
SQL Java API
Agent 越用越聪明?AgentScope Java 在线训练插件来了!
使用AgentScope Java + Trinity-RFT 在线训练优化你的Agent,让你的Agent边运行边进化。
644 17
|
缓存 Java 数据库连接
Mybatis一级缓存、二级缓存详讲
本文介绍了MyBatis中的查询缓存机制,包括一级缓存和二级缓存。一级缓存基于同一个SqlSession对象,重复查询相同数据时可直接从缓存中获取,减少数据库访问。执行`commit`操作会清空SqlSession缓存。二级缓存作用于同一namespace下的Mapper对象,支持数据共享,需手动开启并实现序列化接口。二级缓存通过将数据存储到硬盘文件中实现持久化,为优化性能,通常在关闭Session时批量写入缓存。文章还说明了缓存的使用场景及注意事项。
847 7
Mybatis一级缓存、二级缓存详讲
|
7月前
|
测试技术 API 开发者
Postman 旧版本下载方法全解(图文教程)
本文详解如何下载Postman历史版本,涵盖卸载当前版本、查找官方发布记录及旧版下载链接,并探讨版本管理痛点,引出Apifox等集成化接口测试工具的优势,助力开发测试更高效。
Postman 旧版本下载方法全解(图文教程)
|
存储 安全 Java
Spring Security 入门与详解
Spring Security 是 Spring 框架中的核心安全模块,提供认证、授权及防护功能。本文详解其核心概念,包括认证(Authentication)、授权(Authorization)和过滤器链(Security Filter Chain)。同时,通过代码示例介绍基本配置,如 PasswordEncoder、UserDetailsService 和自定义登录页面等。最后总结常见问题与解决方法,助你快速掌握 Spring Security 的使用与优化。
2779 0
|
消息中间件 存储 Kafka
RocketMQ 工作原理图解,看这篇就够了!
本文详细解析了 RocketMQ 的核心架构、消息领域模型、关键特性和应用场景,帮助深入理解消息中间件的工作原理。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
RocketMQ 工作原理图解,看这篇就够了!
|
存储 XML Java
【Maven技术专题】「入门到精通」教你如何使用Maven中引用依赖本地Jar包,并进行打包输出
【Maven技术专题】「入门到精通」教你如何使用Maven中引用依赖本地Jar包,并进行打包输出
3179 0
|
Java 开发者 Spring
深入解析:Spring AOP的底层实现机制
在现代软件开发中,Spring框架的AOP(面向切面编程)功能因其能够有效分离横切关注点(如日志记录、事务管理等)而备受青睐。本文将深入探讨Spring AOP的底层原理,揭示其如何通过动态代理技术实现方法的增强。
660 8
|
Java 测试技术 数据处理
Java Function 接口详解
在 Java 编程中,Function 接口是一个非常重要的函数式接口,它允许你定义一个接受一个参数并产生结果的函数。Function 接口通常在各种数据处理和转换操作中使用,例如集合处理、流处理以及函数式编程。 本文将详细介绍 Function 接口的使用方法,包括如何创建、组合和使用 Function 接口的实例。我们还将讨论一些常见的应用场景,以帮助你更好地理解如何利用 Function 接口来简化代码
506 0
|
C++ Windows
Windows、IDEA、VS Code常用快捷键
Windows、IDEA、VS Code常用快捷键
556 0