ReentrantLock之公平锁和非公平锁详解

article/2025/9/27 6:42:47

ReentrantLock是一个互斥锁,它具有synchronized相同的能力;但相比之下,ReentrantLock扩展性更强,比如实现了公平锁。

下面详细拆解下ReentrantLock的公平锁和非公平锁的实现。

JDK版本:1.8.0_40

公平锁

先看ReentrantLock两个构造方法:

// 默认非公平
public ReentrantLock() {sync = new NonfairSync();
}// 根据传参来实现公平或非公平锁
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

因此,要想实现公平锁,只需要传参为true即可。

先来看一个测试类,后面都将围绕该测试类来分享:

public class TestReentrantLock {private static ReentrantLock lock = new ReentrantLock(true);public static void main(String[] args) {Runnable runnable = () -> {process();};Thread t0 = new Thread(runnable);Thread t1 = new Thread(runnable);Thread t2 = new Thread(runnable);t0.start();t1.start();t2.start();}public static void process() {String name = Thread.currentThread().getName();System.out.println(name +" 尝试获取锁");lock.lock();try {System.out.println(name +" 已获取锁");sleep(1000L);} finally {System.out.println(name +" 释放锁");lock.unlock();}}private static void sleep(Long millis) {try {Thread.sleep(millis);} catch (InterruptedException e) {// ignore}}
}

输出:

Thread-0 尝试获取锁
Thread-2 尝试获取锁
Thread-1 尝试获取锁
Thread-0 已获取锁
Thread-0 释放锁
Thread-2 已获取锁
Thread-2 释放锁
Thread-1 已获取锁
Thread-1 释放锁

根据这个测试类,通过画图的方式,一起来看看每一步都执行了什么。

lock()过程

先来看看lock()过程

1)首先创建一个公平锁,ReentrantLock lock = new ReentrantLock(true);
image-20220504142401828

2)t0尝试获取锁,lock.lock(),该方法会调用父类的acquire方法

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

tryAcquire(arg)方法:

protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();// 1.判断state是否为0,因为t0是第一个进来的,此时state为0if (c == 0) {// 2.hasQueuedPredecessors() 队列是否有内容,此时没有内容// 3.compareAndSetState(0, acquires) 尝试将state改成1,我们假设t0线程修改成功if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {// 4.将成员变量exclusiveOwnerThread修改为当前线程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;
}

由于该方法返回了true,!tryAcquire(arg)为false,acquire(int arg)方法直接返回了。

t0拿到了锁,开始处理业务。此时该对象锁如图:(绿色表明与上一个图的变化,下面也是一样)
image-20220504142422698

3)我们假设t0还未释放锁,接着由t1尝试获取锁,lock.lock(),再来看acquire方法

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

还是首先看tryAcquire(arg)

protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();// 1.判断state是否为0,因为t0已经将该值改成了1if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// exclusiveOwnerThread 也被改成了t0else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

因未释放锁,因此该方法会返回false。!tryAcquire(arg)为true,接着会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。

先看看addWaiter(Node.EXCLUSIVE)做了什么:

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;// 此时tail为null,不会执行这个方法if (pred != null) {// 如果t1已经入队,t2则直接加入到队尾,tail指向t2node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 入队enq(node);return node;
}private Node enq(final Node node) {for (;;) {// 记录尾部nodeNode t = tail;if (t == null) { // Must initialize// 第1次循环,tail为null,给head和tail赋值一个空的Nodeif (compareAndSetHead(new Node()))tail = head;} else {// 非第1次循环,将传入的node的prev指向刚新建的nodenode.prev = t;// 将传入的node赋值给tail,这里会存在竞争,如果有多个同时修改,那么未修改成功的将进入下一次循环if (compareAndSetTail(t, node)) {// 将之前记录的尾部的next指向传入的node,也就是指向尾部t.next = node;return t;}}}
}

入队,第1次循环:
image-20220504142443648

第2次循环,将t1入队:
image-20220504142502564

如果在第2次循环的时候,存在竞争,则未竞争成功的会继续第3次循环,最终都将入队:
image-20220504142522816

入队成功之后,我们看下acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// 获取当前node的prev节点final Node p = node.predecessor();// 如上图所示,如果当前节点是t1的话,会再次尝试获取锁。if (p == head && tryAcquire(arg)) {// 能获取到锁,表明t0已经释放了锁,此时将head指向当前nodesetHead(node);// prev节点next指向置为空,方便回收p.next = null; // help GCfailed = false;return interrupted;}// 如果t0未释放,或prev不是head// shouldParkAfterFailedAcquire(p, node) 该方法是给每个节点的prev节点的waitStatus设置为Node.SIGNAL,即-1// parkAndCheckInterrupt() 暂停线程if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

该方法每个节点的prev节点的waitStatus设置为Node.SIGNAL,即-1,并且使线程暂停,如图:
image-20220504142647364

到此,lock()到此结束,除t0正在执行外,其他都在等待。

unlock()过程

再来看看unlock过程

1)t0业务逻辑执行完毕,开始释放锁

public final boolean release(int arg) {// 尝试释放锁if (tryRelease(arg)) {Node h = head;// 判断head是否不为0,在lock的时候,waitStatus都已改成-1if (h != null && h.waitStatus != 0)// 唤醒线程unparkSuccessor(h);return true;}return false;
}protected final boolean tryRelease(int releases) {// 直接把state减1int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 不为0,则该线程之前存在重入锁情况if (c == 0) {// 为0,则当前线程占用的锁全部释放free = true;// 将锁的占有线程置为nullsetExclusiveOwnerThread(null);}// 设置新的state值,因为此时不会有竞争,可直接设置setState(c);return free;
}private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)// 将head waitStatus改成0compareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node.  But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*/Node s = node.next;if (s == null || s.waitStatus > 0) {// head的next的node节点waitStatus大于0,即已取消,// 则从tail开始循环获取到队列头部第一个waitStatus<=0的值s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)// 唤醒线程,如上图,唤醒的是t1线程LockSupport.unpark(s.thread);
}

2)t1被唤醒之后,我们再来看acquireQueued方法

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// t1的prev是headfinal Node p = node.predecessor();if (p == head && tryAcquire(arg)) {// 因t0已释放锁,t1获取到锁,exclusiveOwnerThread=t1, state=1// 将head指向为当前node,即t1所在node,并把该node的prev、thread都置为nullsetHead(node);// prev节点next指向置为空,方便回收p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// 如果是中断唤醒的,interrupted设置为trueinterrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

t1拿到了锁,exclusiveOwnerThread值为t1, 之前创建的空Node对象没有任何指向,将会被回收掉。

如图:
image-20220504143900481

3)如果t1是被其他线程中断的,parkAndCheckInterrupt()会返回true,interrupted则会设置为true。

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);// 该方法会返回是否被中断,以及清理中断状态return Thread.interrupted();
}
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 被中断后,会执行该方法;// 因为线程的中断状态已被清理,此处会再次执行中断selfInterrupt();
}

因Thread.interrupted()方法会清理中断状态,执行selfInterrupt()方法再次中断下,这样业务逻辑执行时拿到的线程状态就是中断的。

总结下加锁和解锁过程:

1.新加入的线程会尝试获取锁(tryAcquire);

2.获取不到,则入队(addWaiter);

3.入队之后,队头会再次尝试获取锁,获取不到,则和其他线程一样暂停(parkAndCheckInterrupt);

4.有线程释放锁,唤醒队头线程(unparkSuccessor);

5.队头线程尝试获取锁,退出自旋(acquireQueued),开始执行业务逻辑。

非公平锁

非公平锁和公平锁的区别:主要在于获取锁的时候要不要先判断是否已有线程在等待。非公平锁,有线程进来,直接尝试获取一次锁。

我们看下非公平锁获取锁的过程:

final void lock() {// 先尝试获取锁if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

tryAcquire方法会调nonfairTryAcquire

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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

每次都是先尝试compareAndSetState(0, acquires)获取锁。

再来看下公平锁获取过程:

protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 先判断队列是否有线程在等待if (!hasQueuedPredecessors() &&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;
}

公平锁,会判断队列是否有线程在等待。

以上是ReentrantLock详解,有问题欢迎沟通。


http://chatgpt.dhexx.cn/article/ZKhGgYTG.shtml

相关文章

ReentrantLock中公平锁和非公平锁的区别

目录 背景知识 ReentrantLock的组成 概述 公平锁示意图 非公平锁示意图 源码解读 非公平锁 公平锁 代码对比 问题 知识扩展 tryLock方法 参考资料 背景知识 ReentrantLock的组成 首先看下ReentrantLock的组成结构。 公平锁和非公平锁主要是通过内部类FairSync和…

公平锁和非公平锁

Reentrant Re entrant&#xff0c;Re是重复、又、再的意思&#xff0c;entrant是enter的名词或者形容词形式&#xff0c;翻译为进入者或者可进入的&#xff0c;所以Reentrant翻译为可重复进入的、可再次进入的&#xff0c;因此ReentrantLock翻译为重入锁或者再入锁。 公平锁…

阿里面试官:说一下公平锁和非公平锁的区别?

点赞再看&#xff0c;养成习惯&#xff0c;微信搜索【三太子敖丙】关注这个互联网苟且偷生的工具人。 本文 GitHub https://github.com/JavaFamily 已收录&#xff0c;有一线大厂面试完整考点、资料以及我的系列文章。 前言 上次我们提到了乐观锁和悲观锁&#xff0c;那我们知道…

Ubuntu 手动安装 JDK8

文章目录 1. 下载2. 解压安装3. 配置环境变量 1. 下载 先去官网下载合适的版本&#xff0c;官网&#xff1a;https://www.oracle.com/java/technologies/downloads/archive/ 通过下载页面获取到下载链接后&#xff0c;可以直接在Ubuntu上使用wget下载&#xff0c;也可以先下载…

centos8安装jdk教程

文章目录 一、安装二、配置环境变量三.验证 一、安装 1、查看JDK软件包列表 yum search java | grep -i --color jdk2、选择版本安装 yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel或者如下命令安装jdk8所有文件 yum install -y java-1.8.0-openjdk*二、配置…

Java - JDK8安装及配置环境变量教程

Java - JDK8安装及配置环境变量教程 一、安装JDK教程 甲骨文官网下载JDK版本&#xff1a;windows64下载地址 下载完成后开始安装JDK&#xff1a;双击打开 点击下一步&#xff1a; 若不需要自定义路径&#xff0c;则安装到默认路径即可&#xff08;安装的路径需记住&#xff0…

JDK8安装和环境配置

JDK8的安装和环境配置 一、JDK8下载二、安装三、环境配置 一、JDK8下载 官网下载&#xff1a; https://www.oracle.com/java/technologies/downloads/#java8-windows 二、安装 打开安装&#xff0c;一直下一步即可&#xff0c;可以在安装过程中更改安装地址&#xff0c;我放…

Java JDK 8的安装与配置

文章目录 前言1. 安装JDK 8Step1&#xff1a;选择JDK的版本Step2&#xff1a;选择系统平台Step3&#xff1a;下载安装包Step4&#xff1a;开始安装 2. 配置JDK 8Step1&#xff1a;配置“环境变量path” 前言 本教程是在Windows 64位平台上安装JDK 8版本。 1. 安装JDK 8 官网…

JDK8安装与环境配置

前言&#xff1a;在网上看了下JDK的安装与环境配置&#xff0c;发现很多视频以及博客的讲的都很复杂&#xff0c;对初学者很不友好。很多小白看到这些配置步骤都一脸懵&#xff0c;即使一步一步看着操作还是配置失败。这主要是因为很多博主配置了不需要的配置环境&#xff0c;让…

Linux安装JDK8详细图文教程

第一步、获取JDK文件 JDK下载包&#xff1a;直接进入 如果跳转登录页面&#xff0c;注册一个账号登录即可 登录过后文件就下载完成 第二步、上传JDK到服务器 1、创建JDK目录 mkdir -p /developer/env/jdk进入目录 cd /developer/env/jdk2、安装lrzsz&#xff08;用于远程传…

JDK8安装教程(Windows、Ubuntu)

文章目录 下载JDK8在Windows上安装设置环境变量 在Ubuntu上安装 下载JDK8 Linux版&#xff0c;CSDN下载地址 推荐官网下载&#xff08;需要注册&#xff09;&#xff0c;能下到Windows和Ubuntu的最新版 在Windows上安装 双击打开下载好的安装包&#xff0c;点击下一步 点击左…

linux系统安装jdk8详细教程

文章目录 前言一、下载jdk8的安装包二、压缩包上传解压1.将下载好的压缩包使用ftp工具上传到服务器2.将压缩包解压到指定目录 三、配置jdk的环境变量四、测试是否安装成功 前言 虚拟机版本&#xff1a;centos7 jdk版本&#xff1a;1.8 一、下载jdk8的安装包 方法1&#xff1a…

jdk8安装教程及环境变量配置

目录 一、JDK下载 二、安装JDK 三、环境变量配置 四、测试环境变量 一、JDK下载 1、JDK下载地址&#xff1a;Java Downloads | Oraclehttps://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 2、选这个位置 3、向下滑动鼠标&#xff0c…

WIN10javaJDK8安装教程

一、下载jdk8文件 下载地址: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 下载步骤&#xff1a; 1、进入网站后往下拉&#xff0c;找到下载界面&#xff0c;选择自己合适的下载。 二、安装 双击运行刚下载好的exe文件…

JDK8安装教程-极其详细

下载安装JDK 前往oracle官网下载JDK 链接&#xff1a;https://www.oracle.com/java/technologies/downloads/#java8-windows下载完成后 打开下载的程序进行安装JDK 这里可以选择你要存放的位置 选择jre安装的路径 继续下一步&#xff0c;等待安装完成 配置环境变量 此电脑…

jdk8安装教程及环境变量部署

一、jdk下载 下载方式&#xff1a; 官网下载 地址&#xff1a;Java Downloads | Oracle网盘下载&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1jjcOsdyaLfAy2N5zp64sew 提取码&#xff1a;tzsj 官网下载&#xff1a; 这是最新版本的可以根据自己的需要选择 选择W…

win10系统安装jdk8全过程

一 下载安装文件 jdk的安装与配置是Java学习的第一步&#xff0c;下面记录一下具体过程。首先根据自己系统下载对应版本。下载地址http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 二 安装 双击exe文件&#xff0c;按照默认设置一步一步…

JDK8下载安装及配置环境教程,一看就会

目录 一.下载安装包 二.安装步骤 三.配置环境 一.下载安装包 百度网盘链接:百度网盘 请输入提取码 提取码:QI20 也可以选择自己到ORACLE官网下载:www.oracle.com. 1.点击链接进入官网 可以看到上方有个"Products"&#xff0c;点击它 2.然后可以看到下滑的菜单…

JDK8详细图文安装教程

1、双击软件 2、点击下一步 3、选择jdk的安装目录 4、安装jre环境&#xff0c;选择jre安装目录 5、配置环境变量&#xff1a;选中计算机→右键→属性→高级系统设置→高级→环境变量 6、系统变量→新建JAVA_HOME变量 7、系统变量→寻找Path变量→编辑 8、新增jdk、jre的bin位置…

手把手教你安装JDK8~

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 JDK8安装教程一、JKD8下载及安装流程1、浏览器搜索jdk8&#xff0c;进入官网网站进行下载2、选择相应操作系统进行下载&#xff0c;&#xff08;作者是windows系统&…