在面试或者日常开发当中,经常会遇到公平锁和非公平锁的概念。

两者最大的区别如下

1️⃣ 公平锁:N个线程去申请锁时,会按照先后顺序进入一个队列当中去排队,依次按照先后顺序获取锁。就像下图描述的上厕所的场景一样,先来的先占用厕所,后来的只能老老实实排队。

2️⃣ 非公平锁:N个线程去申请锁,会直接去竞争锁,若能获取锁就直接占有,获取不到锁,再进入队列排队顺序等待获取锁。同样以排队上厕所打比分,这时候,后来的线程会先尝试插队看看能否抢占到厕所,若能插队抢占成功,就能使用厕所,若失败就得老老实实去队伍后面排队。

针对这两个概念,我们通过ReentrantLock底层源码来分析下 :公平锁和非公平锁在ReentrantLock类当中锁怎样实现的。

ReentrantLock内部实现的公平锁类是FairSync,非公平锁类是NonfairSync。

当ReentrantLock以无参构造器创建对象时,默认生成的是非公平锁对象NonfairSync,只有带参且参数为true的情况下FairSync,才会生成公平锁,若传参为false时,生成的依然是非公平锁,两者构造器源码结果如下

​ 图1

在实际开发当中,关于ReentrantLock的使用案例,一般是这个格式

 class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock();
// block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}

这时的lock指向的其实是NonfairSync对象,即非公平锁。

当使用lock.lock()对临界区进行占锁操作时,最终会调用到NonfairSync对象的lock()方法。根据图1可知,NonfairSync和FairSync两者的lock方法实现逻辑是不一样的,而体现其锁是否符合公平与否的地方,就是在两者的lock方法里。

可以看到,在非公平锁NonfairSync的上锁lock方法当中,若if(compareAndSetState(0,1))判断不满足,就会执行acquire(1)方法,该方法跟公平锁FairSync的lock方法里调用的acquire(1)其实是同一个,但方法里的tryAcquire具体实现又存在略微不同,这里后面会讨论。

这里就呼应前文提到的非公平锁的概念——当N个线程去申请非公平锁,它们会直接去竞争锁,若能获取锁就直接占有,获取不到锁,再进入队列排队顺序等待获取锁。这里的“获取不到锁,再进入队列排队顺序等待获取锁”可以理解成——当线程过来直接竞争锁失败后,就会变成公平锁的形式,进入到一个队列当中,按照先后顺序排队去获取锁。

而if(compareAndSetState(0,1))语句块的逻辑,恰好就体现了“当N个线程去申请非公平锁,它们会直接去竞争锁,若能获取锁就直接占有”这句话的意思。

首先,先来分析NonfairSync的lock()方法原理,源码如下

final void lock() {
//先竞争锁,若能竞争成功,则占有锁资源
if (compareAndSetState(0, 1))
//将独占线程成员变量设置为当前线程,表示占有锁资源的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

compareAndSetState(0, 1)就是一个当前线程与其他线程抢占锁的过程,这里面涉及到AQS的知识点,因此,阅读本文时,需具备一定的AQS基础。

JUC的锁实现是基于AQS实现的,可以简单理解成,AQS里定义了一个private volatile int state变量,若state值为0,说明无线程占有,其他线程可以进行抢占锁资源;若state值为1,说明已有线程占有锁资源,其他线程需要等待该占有锁的线程释放锁资源后,方能进行抢占锁的动作。

线程在抢占锁时,是通过CAS对state变量进行置换操作的,期望值expect是0,更新值update为1,若期望值expect能与内存地址里的state值一致,就可以通过原子操作将内存地址里state值置换成更新值update,返回true,反之,就置换失败返回false。

protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

可见,这里的if (compareAndSetState(0, 1))就体现了非公平锁的机制,当前线程会先去竞争锁,若能竞争成功,就占有锁资源。

若竞争锁失败话,就会执行acquire(1)方法,其原理就相当走跟公平锁类似的逻辑。

acquire(1);

进入acquire方法,该方法是位于AbstractQueuedSynchronizer里,就是前文提到的AQS,即抽象同步队列器,它相当提供一套用户态层面的锁框架,基于它可以实现用户态层面的锁机制。

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

注意一点,NonfairSync和FairSync调用的acquire(int arg)方法中的tryAcquire方法,其实现是不同的。NonfairSync调用的acquire方法,其底层tryAcquire调用的是NonfairSync重写的tryAcquire方法;FairSync调用的acquire方法,其底层tryAcquire调用的是FairSync重写的tryAcquire方法。

NonfairSync类的acquire方法的流程图如下

先分析非公平锁的!tryAcquire(arg)底层源码实现,该方法的整体逻辑是,通过getState()获取state状态值,判断是否已为0。若state等于0了,说明此时锁资源处于无锁状态,那么,当前线程就可以直接再执行一遍CAS原子抢锁操作,若CAS成功,说明已成功抢占锁。若state不为0,再判断当前线程是否与占有资源的锁为同一个线程,若同一个线程,那么就进行重入锁操作,即ReentrantLock支持同一个线程对资源的重复加锁,每次加锁,就对state值加1,解锁时,就对state解锁,直至减到0最后释放锁。

最后,若在该方法里,通过CAS抢占锁成功或者重入锁成功,那么就会返回true,若失败,就会返回false。

final boolean nonfairTryAcquire(int acquires) {
//获取当前线程引用
final Thread current = Thread.currentThread();
//获取AQS的state状态值
int c = getState();
//若state等于0了,说明锁处于无被占用状态,可被当前线程抢占
if (c == 0) {
//再次尝试通过CAS抢锁
if (compareAndSetState(0, acquires)) {
//将独占线程成员变量设置为当前线程,表示占有锁资源的线程
setExclusiveOwnerThread(current);
return true;
}
}
//判断当前线程是否与占有锁资源的线程为同一个线程
else if (current == getExclusiveOwnerThread()) {
//每次重入锁,state就会加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

在 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代码当中,根据 &&短路机制,若!tryAcquire(arg)为false,就不会再执行后面代码。反之,若!tryAcquire(arg)为true,说明抢占锁失败了或者不属于重入锁,那么就会继续后续acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代码的执行。acquireQueued里面的逻辑,就可以理解成“获取不到锁,再进入队列排队顺序等待获取锁”。这块内容涉及比较复杂的双向链表逻辑,我后面会另外写一篇文章深入分析,本文主要是讲解公平锁和非公平锁的区别科普。

FairSync公平锁lock方法里acquire(1)的逻辑与非公平锁NonfairSync的acquire(1)很类似,其底层实现同样是这样

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

我们来看下FairSync类里重实现的tryAcquire与NonfairSync最终执行的tryAcquire区别

可以看到,公平锁FairSync的tryAcquire方法比NonfairSync的nonfairTryAcquire方法多了一行!hasQueuedPredecessors()代码。

在FairSync公平锁里,若hasQueuedPredecessors()返回false,那么!hasQueuedPredecessors()就会为true,在执行以下判断时,就会通过compareAndSetState(0, acquires)即CAS原子抢占锁。

if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}

那么,什么情况下,hasQueuedPredecessors() 能得到false值呢?

public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

存在两种情况:

1️⃣ 第一种情况,h != t为false,说明head和tail节点都为null或者h和t都指向一个假节点head,这两种情况都说明了,此时的同步队列还没有初始化,简单点理解,就是在当前线程之前,还没有出现线程去抢占锁,因此,此时,锁是空闲的, 同时当前线程算上最早到来的线程之一(高并发场景下同一时刻可能存在N个线程同时到来),就可以通过CAS竞争锁。

2️⃣ 第二种情况,h != t为true但(s = h.next) == null || s.thread != Thread.currentThread()为false,当头节点head和尾节点都不为空且指向不是同一节点,就说明同步队列已经初始化,此时至少存在两个以上节点,那么head.next节点必定不为空,即(s = h.next) == null会为false,若s.thread != Thread.currentThread()为false,说明假节点head的next节点刚好与当前线程为同一节点,也就意味着,当前线程排在队列最前面,排在前面的可以在锁空闲时获取锁资源,就可以执行compareAndSetState(0, acquires)去抢占锁资源。

若同步队列已经初始化,且当前线程又不是在假节点head的next节点,就只能老老实实去后面排队等待获取锁了。

图解ReentrantLock底层公平锁和非公平锁实现原理的更多相关文章

  1. 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

    问题 (1)重入锁是什么? (2)ReentrantLock如何实现重入锁? (3)ReentrantLock为什么默认是非公平模式? (4)ReentrantLock除了可重入还有哪些特性? 简介 ...

  2. 深入分析ReentrantLock公平锁和非公平锁的区别

    在ReentrantLock中包含了公平锁和非公平锁两种锁,通过查看源码可以看到这两种锁都是继承自Sync,而Sync又继承自AbstractQueuedSynchronizer,而AbstractQ ...

  3. java多线程20 : ReentrantLock中的方法 ,公平锁和非公平锁

    公平锁与非公平锁 ReentrantLock有一个很大的特点,就是可以指定锁是公平锁还是非公平锁,公平锁表示线程获取锁的顺序是按照线程排队的顺序来分配的,而非公平锁就是一种获取锁的抢占机制,是随机获得 ...

  4. Java之ReentrantLock公平锁和非公平锁

    在Java的ReentrantLock构造函数中提供了两种锁:创建公平锁和非公平锁(默认).代码如下: public ReentrantLock() { sync = new NonfairSync( ...

  5. 深入分析ReentrantLock公平锁和非公平锁的区别 (转)

    在ReentrantLock中包含了公平锁和非公平锁两种锁,通过查看源码可以看到这两种锁都是继承自Sync,而Sync又继承自AbstractQueuedSynchronizer,而AbstractQ ...

  6. 理解ReentrantLock的公平锁和非公平锁

    学习AQS的时候,了解到AQS依赖于内部的FIFO同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个Node对象并将其加入到同步队列,同时会阻塞当 ...

  7. ReentrantLock源码探究1:非公平锁的获取和释放

    1.AQS简单介绍 ​ Sync是ReentrantLock的一个内部类,它继承了AbstractQueuedSynchronizer,即AQS,在CountDownLatch.FutureTask. ...

  8. ReentrantLock中的公平锁与非公平锁

    简介 ReentrantLock是一种可重入锁,可以等同于synchronized的使用,但是比synchronized更加的强大.灵活. 一个可重入的排他锁,它具有与使用 synchronized ...

  9. 聊聊ReentrantLock基于AQS的公平锁和非公平锁的实现区别

    ReentrantLock锁的实现是基于AQS实现的,所以先简单说下AQS: AQS是AbstractQueuedSynchronizer缩写,顾名思义:抽象的队列同步器,它是JUC里面许多同步工具类 ...

  10. 深入了解ReentrantLock中的公平锁和非公平锁的加锁机制

    ReentrantLock和synchronized一样都是实现线程同步,但是像比synchronized它更加灵活.强大.增加了轮询.超时.中断等高级功能,可以更加精细化的控制线程同步,它是基于AQ ...

随机推荐

  1. PLSQL 与 PLPGSQL

    KingbaseES 为了更好地适应用户的oracle 应用,实现了对 plsql 的支持,用户可以根据需要使用 plsql 或 plpgsql. 以下简要介绍下二者的差异 一.格式差异 1.plpg ...

  2. 深度剖析js闭包

    一.什么是闭包? 方法里面返回一个方法 二.闭包存在的意义 延长变量的生命周期 作用域链 沟通内外部方法的桥梁    闭包会常驻内存  ==>慎用闭包  闭包里的变量不会被回收 创建私有环建 例 ...

  3. 天天写SQL,这些神奇的特性你知道吗?

    摘要:不要歪了,我这里说特性它不是 bug,而是故意设计的机制或语法,你有可能天天写语句或许还没发现原来还能这样用,没关系我们一起学下涨姿势. 本文分享自华为云社区<[云驻共创]天天写 SQL, ...

  4. 【gRPC】C++下使用CMakeLists快速构建项目

    在gRPC中,编写.proto文件(protocol buffer文件)来定义RPC服务的接口是第一步 先通过proto的代码生成器编译生成pb.h.pb.cc.grpc.pb.h.grpc.pb.c ...

  5. Java语言(基础一)

    Java语言 Java的特性和优势 简单性(简单易学) 面向对象(一种思想 万物皆对象) 可移植性(一次编写到处运行 JVM) 高性能(及时编译) 分布式(网络分布式url) 动态性(反射机制) 多线 ...

  6. STM32F10x SPL V3.6.2 集成 FreeRTOS v202112

    STM32F10x SPL 集成 FreeRTOS 在整理 GCC Arm 工具链的Bluepill代码示例, 常用外设都差不多了, 接下来是 FreeRTOS, 网上查到的基本上都是基于旧版本的集成 ...

  7. VScode开发STM32/GD32单片机-环境搭建

    1.软件下载 1.1.安装VSCode 1.2.下载安装VisualGDB 1.3.下载安装mingwin64 1.4.下载安装OpenOCD 1.5.下载GNU Arm Embedded Toolc ...

  8. 洛谷P1950 长方形(单调栈)

    一道单调栈的好题啊...... 思路是很奇妙的,对于每个点(i,j),我们可以算它对答案的贡献(即包含它的矩形数量),包含该点的矩形,点的高度为h[j],点右边的高度一定大于等于h[j],左边的高度一 ...

  9. How to get the return value of the setTimeout inner function in js All In One

    How to get the return value of the setTimeout inner function in js All In One 在 js 中如何获取 setTimeout ...

  10. 【做题笔记】CSP-S 往年试题

    题单 本文章正在持续更新-- [2021] 廊桥分配 题目 题面描述 所有飞机分为两类--国内区和国际区,两区廊桥数量互不干扰.每架飞机遵循"先到先得"的原则,优先选择编号最小的廊 ...