首页 > 其他分享 >16.ReentrantLock全解读

16.ReentrantLock全解读

时间:2023-05-06 09:22:47浏览次数:37  
标签:加锁 16 ReentrantLock final 解读 boolean 线程 public

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的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~~

标签:加锁,16,ReentrantLock,final,解读,boolean,线程,public
From: https://www.cnblogs.com/wyz1994/p/17375492.html

相关文章

  • 16.求车速
    代码实现:#include<stdio.h>intis_palindrome_number(intnum){intold_val=num;//传入的数字intnew_val=0;//颠倒后的数字while(num)//当前num的位数大于1(值大于9){//将new_val乘以10,然后加上num的个位数,//使个位数从原......
  • 16QAM调制解调系统相位盲估计matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下:2.算法涉及理论知识概要16QAM是指包含16种符号的QAM调制方式。16QAM是用两路独立的正交4ASK信号叠加而成,4ASK是用多电平信号去键控载波而得到的信号。它是2ASK调制的推广,和2ASK相比,这种调制的优点在于信息传输速率高。正交幅度......
  • 16QAM调制解调系统相位盲估计matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下: 2.算法涉及理论知识概要16QAM是指包含16种符号的QAM调制方式。       16QAM是用两路独立的正交4ASK信号叠加而成,4ASK是用多电平信号去键控载波而得到的信号。它是2ASK调制的推广,和2ASK相比,这种调制的优点在于信息传......
  • P4093[HEOI2016/TJOI2016]序列
    P4093[HEOI2016/TJOI2016]序列目录P4093[HEOI2016/TJOI2016]序列题目描述输入格式输出格式样例#1样例输入#1样例输出#1提示题目大意思路code题目描述佳媛姐姐过生日的时候,她的小伙伴从某宝上买了一个有趣的玩具送给他。玩具上有一个数列,数列中某些项的值可能会变化,但同一个......
  • 165. 小猫爬山
    题目描述每辆缆车承重一样,每个猫重量不一,最少需要多少缆车把猫运下山?f1-深搜+减枝基本分析怎么考虑搜索?对当前的猫来说,有当前开的缆车个数+新缆车几种选择状态空间有哪些维度?当前用到的缆车个数,当前正在处理的猫的id哪些可额能的减枝方式?(1)优化搜索顺序:从大到小;(2)最优性:>=ans......
  • 164. 可达性统计
    题目描述给定一张N个点M条边的有向无环图,分别统计从每个点出发能够到达的点的数量f1-拓扑排序+状态压缩基本分析怎么梳理出统计的顺序?拓扑排序怎么统计?按照拓扑序的逆序记录可达性N在30000规模,怎么维护可达性?利用bitset进行状态压缩代码#include<iostream>#includ......
  • 1688|Taobao|JD京东api接口获取商品详情C++演示案例
    ​ 商品详情页的作用:介绍产品信息、给出购买理由、提升信任感、提出售后保障。1、介绍产品信息:产品信息表做得越完整,越能让用户更细致了解产品,也减少了售前客服咨询的工作量。2、给出购买理由:在用户初步了解了产品信息后,商家就需要展示商品优势,给出核心卖点。接下来小编会展......
  • CSC8016用户场景
    CourseworkCSC8016UseCaseScenarioWewanttoimplementavirtualshoppingsystem,whetherthethreadsareeitherclientsusingthewebapp,orclientsbuyingproductsonthephysicalshopusingthemobileapp.Thelifecycleofanyclientinteractionissum......
  • LeetCode 416 分割等和子集
    LeetCode|416.分割等和子集给你一个只包含正整数的非空数组 nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。示例1:输入:nums=[1,5,11,5]输出:true解释:数组可以分割成[1,5,5]和[11]。示例2:输入:nums=[1,2,3,5]输出:false解......
  • 利用Python爬虫采集1688商品详情数据 +商品列表数据+商品API接口(支持全网)
    一、如何通过手动方式查看1688商品详情页面的数据1.1688商品详情API接口(item_get-获得1688商品详情接口),1688API接口代码对接可以获取到宝贝ID,宝贝标题,价格,掌柜名称,库存,最小购买数,宝贝链接,宝贝图片,品牌名称,商品详情,详情图片等页面上有的数据均可以获取到,手动方式如下:例......