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

目录
相关文章
|
存储 SQL 数据库
面试官:索引失效场景有哪些?
以下是内容的摘要: 本文列举了可能导致数据库索引失效的16种情况:全表扫描、索引列使用计算或函数、LIKE查询条件不匹配、未遵循联合索引最左前缀原则、索引列参与排序无筛选、隐式类型转换、OR条件连接索引、IN子句大量值、NOT操作、数据分布不均的JOIN、数据过于分散的查询、大结果集、临时表或派生表操作、索引维护不及时以及不等于比较和IS NOT NULL条件。这些情况都可能使查询优化器放弃使用索引,影响查询性能。
1016 1
|
存储 SQL 关系型数据库
MySQL中的当前读和快照读及其区别
MySQL中的当前读和快照读及其区别
1243 0
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
Java 编译器 Spring
面试突击78:@Autowired 和 @Resource 有什么区别?
面试突击78:@Autowired 和 @Resource 有什么区别?
16072 6
|
安全 Java
JUC锁: ReentrantReadWriteLock详解
`ReentrantReadWriteLock` 主要用于实现高性能的并发读取,而在写操作相对较少的场景中表现尤为突出。它保证了数据的一致性和线程安全,在合适的场合合理使用 `ReentrantReadWriteLock`,可以实现更加细粒度的控制,并显著提升应用性能。然而,需要注意它的复杂度较一般的互斥锁高,因此在选择使用时要仔细考虑其适用场景。
123 1
1.5w字,30图带你彻底掌握 AQS!(建议收藏)
AQS( AbstractQueuedSynchronizer )是一个用来构建锁和同步器(所谓同步,是指线程之间的通信、协作)的框架,Lock 包中的各种锁(如常见的 ReentrantLock, ReadWriteLock), concurrent它包中的各种同步器(如 CountDownLatch, Semaphore, CyclicBarrier)都是基于 AQS 来构建,所以理解 AQS 的实现原理至关重要,AQS 也是面试中区分候选人的常见考点,我们务必要掌握,本文将用循序渐进地介绍 AQS,相信大家看完一定有收获。文章目录如下
|
Java C++
【Java 并发秘籍】synchronized vs ReentrantLock:揭秘线程同步神器的对决!
【8月更文挑战第24天】本文详细对比了Java并发编程中`synchronized`关键字与`ReentrantLock`的不同之处。`synchronized`作为内置关键字,提供自动锁管理但不支持中断或公平锁;`ReentrantLock`则通过显式调用方法控制锁,具备更多高级功能如可中断、公平锁及条件变量。文章通过两个计数器类实例展示了两种机制的具体应用,帮助读者理解其差异及适用场景。掌握这两者对于提升多线程程序设计能力至关重要。
199 0
|
缓存 监控 Java
Java一分钟之-JCache:JSR-107缓存标准
【6月更文挑战第17天】**JCache (JSR-107)** 是Java的缓存标准,提供统一接口用于集成缓存,支持配置过期策略、监听器、事务和不同缓存实现的切换。常见问题包括过度依赖缓存导致数据不一致、缓存雪崩和击穿。要避免这些问题,需确保回源机制、分散过期时间、使用锁和监控。示例展示了如何配置和使用JCache,强调了合理设计缓存的重要性。
316 1
|
安全 关系型数据库 MySQL
使用Docker-compose快速构建Nacos服务
【1月更文挑战第1天】 在微服务架构中,服务的注册与发现扮演着至关重要的角色。Nacos(Naming and Configuration Service)是阿里巴巴开源的服务注册与发现组件,致力于支持动态配置管理和服务发现。
3421 2
|
图形学 Python 容器
【PyQt5桌面应用开发】3.Qt Designer快速入门(控件详解)
【PyQt5桌面应用开发】3.Qt Designer快速入门(控件详解)
2845 0