ReentrantLock 简介

ReentrantLock 实现了 Lock 接口,是一种可重入的独占锁。

相比于 synchronized 同步锁,ReentrantLock 更加灵活,拥有更加强大的功能,比如可以实现公平锁机制。

首先,先来了解一下什么是公平锁机制。

ReentrantLock 的公平锁机制

我们知道,ReentrantLock 分为公平锁非公平锁,可以通过构造方法来指定具体类型:

//默认非公平锁
public ReentrantLock() {sync = new NonfairSync();
}//公平锁
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

公平锁

在多个线程竞争获取锁时,公平锁倾向于将访问权授予等待时间最长的线程。

也就是说,公平锁相当于有一个线程等待队列,先进入队列的线程会先获得锁,按照 "FIFO(先进先出)" 的原则,对于每一个等待线程都是公平的。

非公平锁

非公平锁是抢占模式,线程不会关注队列中是否存在其他线程,也不会遵守先来后到的原则,直接尝试获取锁。

接下来进入正题,一起分析下 ReentrantLock 的底层是如何实现的。

ReentrantLock 的底层实现

ReentrantLock 实现的前提是 AbstractQueuedSynchronizer(抽象队列同步器),简称 AQS,是 java.util.concurrent 的核心,常用的线程并发类 CountDownLatch、CyclicBarrier、Semaphore、ReentrantLock 等都包括了一个继承自 AQS 抽象类的内部类。

同步标志位 state

AQS 内部维护了一个同步标志位 state,用来实现同步加锁控制:

private volatile int state;

同步标志位 state 的初始值为 0,线程每加一次锁,state 就会加 1,也就是说,已经获得锁的线程再次加锁,state 值会再次加 1。可以看出,state 实际上表示的是已获得锁的线程进行加锁操作的次数。

CLH 队列

除了 state 同步标志位外,AQS 内部还使用一个 FIFO 的队列(也叫 CLH 队列)来表示排队等待锁的线程,当线程争抢锁失败后会封装成 Node 节点加入 CLH 队列中去。

Node 的代码实现:

static final class Node {// 标识当前节点在共享模式static final Node SHARED = new Node();// 标识当前节点在独占模式static final Node EXCLUSIVE = null;static final int CANCELLED =  1;static final int SIGNAL    = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;volatile int waitStatus;//前驱节点volatile Node prev;//后驱节点volatile Node next;//当前线程volatile Thread thread;//存储在condition队列中的后继节点Node nextWaiter;//是否为共享锁final boolean isShared() {return nextWaiter == SHARED;}final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}Node() {}Node(Thread thread, Node mode) {this.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) {this.waitStatus = waitStatus;this.thread = thread;}
}

分析代码可知, 每个 Node 节点都有两个指针,分别指向直接后继节点和直接前驱节点。

Node 节点的变化过程

当出现锁竞争以及释放锁的时候,AQS 同步队列中的 Node 节点会发生变化,如下图所示:

  • 线程封装成 Node 节点追加到队列末尾,设置当前节点的 prev 节点和 next 节点的指向;
  • 通过 CAS 将 tail 重新指向新的尾部节点,即当前插入的 Node 节点;

head 节点表示获取锁成功的节点,当头结点释放锁后,会唤醒后继节点,如果后继节点获得锁成功,就会把自己设置为头结点,节点的变化过程如下:

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

和设置 tail 的重新指向不同,设置 head 节点不需要用 CAS,是因为设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证。只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可。

除了前驱和后继节点,Node 类中还包括了 SHARED 和 EXCLUSIVE 节点,它们起到了什么作用呢?这就不得不介绍一下 AQS 的两种资源共享模式了。

AQS 的资源共享模式

AQS 通过 EXCLUSIVE 和 SHARED 两个变量来定义独占模式共享模式

独占模式

独占模式是最常用的模式,使用范围很广,比如 ReentrantLock 的加锁和释放锁就是使用独占模式实现的。

独占模式中的核心加锁方法是 acquire()

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

这里首先调用 tryAcquire() 方法尝试获取锁,也就是尝试通过 CAS 修改 state 为 1,如果发现锁已经被当前线程占用,就执行重入,也就是给 state+1;

如果锁被其他线程占有,那么当前线程执行 tryAcquire 返回失败,则会执行 addWaiter() 方法在等待队列中添加一个独占式节点,addWaiter() 方法实现如下:

    private Node addWaiter(Node mode) {//创建一个节点,此处mode是独占式的Node node = new Node(mode);for (;;) {Node oldTail = tail;if (oldTail != null) {// 如果tail节点非空,就将新节点的前节点设置为tail节点,并将tail指向新节点node.setPrevRelaxed(oldTail);//CAS将tail更新为新节点if (compareAndSetTail(oldTail, node)) {//把原tail的next设为当前节点oldTail.next = node;return node;}} else {//还没有初始化,就调用initializeSyncQueue()方法初始化initializeSyncQueue();}}}

写入队列后,需要挂起当前线程,代码如下:

/*** 已经入队的线程尝试获取锁*/
final boolean acquireQueued(final Node node, int arg) {boolean failed = true; //标记是否成功获取锁try {boolean interrupted = false; //标记线程是否被中断过for (;;) {final Node p = node.predecessor(); //获取前驱节点//如果前驱是head,即该结点是第二位,有资格去尝试获取锁if (p == head && tryAcquire(arg)) {setHead(node); // 获取成功,将当前节点设置为head节点p.next = null; // 原head节点出队failed = false; //获取成功return interrupted; //返回是否被中断过}// 判断获取失败后是否可以挂起,若可以则挂起if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// 线程若被中断,设置interrupted为trueinterrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

再看下 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 都做了哪些事:

/*** 判断当前线程获取锁失败之后是否需要挂起.*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//前驱节点的状态int ws = pred.waitStatus;if (ws == Node.SIGNAL)// 前驱节点状态为signal,返回truereturn true;// 前驱节点状态为CANCELLEDif (ws > 0) {// 从队尾向前寻找第一个状态不为CANCELLED的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 将前驱节点的状态设置为SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}/*** 挂起当前线程,返回线程中断状态并重置*/
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}

通过以上代码可以看出,线程入队后能够挂起的前提是,它的前驱节点的状态为 SIGNAL,这意味着当前一个节点获取锁并且出队后,需要把后面的节点进行唤醒。

加锁说完了再说解锁,解锁的方法相比来说更加简单,核心方法是 release():

public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}

代码流程:先尝试释放锁,若释放成功,那么查看头结点的状态是否为 SIGNAL,如果是,则唤醒头结点的下个节点关联的线程,如果释放失败就返回 false 表示解锁失败。

其中的 tryRelease() 方法实现如下,详细流程见注释说明:

/*** 释放当前线程占用的锁* @param releases* @return 是否释放成功*/
protected final boolean tryRelease(int releases) {// 计算释放后state值int c = getState() - releases;// 如果不是当前线程占用锁,那么抛出异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {// 锁被重入次数为0,表示释放成功free = true;// 清空独占线程setExclusiveOwnerThread(null);}// 更新state值setState(c);return free;
}

 共享模式

共享模式和独占模式最大的区别在于,共享模式具有传播的特性。

共享模式获取锁的方法为 acquireShared,相比于独占模式,共享模式的加锁多了一个步骤,即自己拿到资源后,还会去唤醒后继队友;

而共享模式释放锁的方法为 releaseShared,它会释放指定量的资源,如果成功释放且允许唤醒等待线程,会唤醒等待队列里的其他线程来获取资源。

本篇博客主要参考文章如下,非常感谢:

AQS底层原理分析 - 千里送e毛 - 博客园

ReentrantLock 实现原理(公平锁和非公平锁) - 知乎

AQS原理解析 - 简书

ReentrantLock原理_路漫漫,水迢迢-CSDN博客_reentrantlock

ReentrantLock的实现原理相关推荐

  1. 详解ReentrantLock之Condition原理

    Condition Jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock.虽然在性能上ReentrantLock和synchronized没有什么区别,但 ...

  2. java中的账户冻结原理_java可重入锁(ReentrantLock)的实现原理

    前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...

  3. reentrantlock原理_分享:synchronized和ReentrantLock的实现原理知识点

    前言 通常呢,会在并发情况下,同时操作某一业务从而造成数据重复提交,业务混乱等问题,通常呢,遇到解决类似问题可采用加锁,限流等问题来解决,那么看看这篇关于java中关于锁中synchronized和R ...

  4. 面试难点:深度解析ReentrantLock的实现原理

    什么是Reentrant Jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock.虽然在性能上ReentrantLock和synchronized没有什么区 ...

  5. ReentrantLock 的实现原理

    我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的.在synchronized中,我们分析了偏向锁.轻量级锁.乐观锁.基于乐观锁以及自旋锁来优化了s ...

  6. Lock、ReentrantLock、ReentrantReadWriteLock原理及应用深入解析

    原文链接: https://blog.csdn.net/u011479540/article/details/52013187

  7. ReentrantLock实现原理深入探究

    前言 这篇文章被归到Java基础分类中,其实真的一点都不基础.网上写ReentrantLock的使用.ReentrantLock和synchronized的区别的文章很多,研究ReentrantLoc ...

  8. 《转》ReentrantLock实现原理深入探究

    五月的仓颉 博客园 管理 随笔 - 202  文章 - 0  评论 - 1646 ReentrantLock实现原理深入探究 前言 这篇文章被归到Java基础分类中,其实真的一点都不基础.网上写Ree ...

  9. JUC AQS ReentrantLock源码分析

    Java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还 ...

最新文章

  1. Centos 7 让docker飞一会儿
  2. 深入理解JavaScript系列(3):全面解析Module模式
  3. 面试官系统精讲Java源码及大厂真题 - 05 ArrayList 源码解析和设计思路
  4. 认识XinYiCMS从这里开始!
  5. 谈debug版本可以正常运行,而在release下运行出错之原因及避免类似情况发生
  6. 【转】英语词汇学习渐进
  7. linux下python网络编程框架-twisted安装手记,linux编程_Linux下Python网络编程框架安装日志...
  8. ads1110程序实测好用
  9. 【技术贴】ppt2003更换图片|更换带有动作特效的图片|替换ppt图片
  10. 《东周列国志》第七十回 杀三兄楚平王即位 劫齐鲁晋昭公寻盟
  11. ai黑白棋_AI的黑白镜
  12. mac转换助理安装Windows11
  13. 关于领域驱动设计(DDD)的理论知识
  14. CSS齿轮转动加载动画
  15. python 搜索 PDF文件 内容
  16. Matlab实现匿名函数计算
  17. 公网部署freeswitch1.8.6后,终端注册成功后,在fs_cli控制台执行originate user/1000 echo,呼通1000的SIP终端后,终端讲话没有收到fs的回声解决方法
  18. python基础试题(1)
  19. 关于WPS Office安全漏洞情况的通报
  20. Google advertiser api开发概述

热门文章

  1. 记一次驴唇不对马嘴的DIY之旅(七)
  2. 历时130天784分通过了HCIE笔试
  3. 十分钟开发物联网:烟雾感应监测(Wifi版)
  4. 文字怎么转成图片?借助这几个软件轻松搞定
  5. vista和win7操作系统关闭用户账户控制
  6. 通过 Google 照片库 API 打造新体验
  7. 核桃编程python年课_2019年核桃编程春季班开抢啦!
  8. 功率谱估计的参数模型方法---AR模型谱估计:自相关法协方差法
  9. 18_小米监控Open-Falcon:Rabbitmq监控
  10. 半并行深度神经网络(SPDNN)混合架构,首次应用于单目相机的深度