重点,一文掌握ReentrantLock加解锁原理!|原创

news2025/8/3 13:33:41

本文详细讲解了 ReentrantLock 加锁和释放锁的原理,以及和 Synchronized 的对比。本文较长,建议收藏!

点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达

简要总结 ReentrantLock

实现原理:volatile 变量 + CAS设置值 + AQS + 两个队列

实现阻塞:同步队列 + CAS抢占标记为 valatile 的 state

实现等待唤醒:await :持有锁,park ->加入等待队列 ;signal:唤醒下一个等待队列节点,转移进入同步队列,然后CAS抢占或者按照阻塞队列等待抢占。接着 await 后续内容程序得以继续执行。

ReentrantLock 结构分析

ReentrantLock 继承了Lock接口, lock方法实际上是调用了Sync的子类NonfairSync(非公平锁)的lock方法。ReentrantLock的真正实现在他的两个内部类NonfairSync 和 FairSync中,默认实现是非公平锁。并且内部类都继承于内部类Sync,而Sync根本的实现则是大名鼎鼎的 AbstractQueuedSynchronizer 同步器(AQS)。

ace281765102e06b86ef3b69d4834175.png

具体详见如下代码:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;
  
  public ReentrantLock() {
        sync = new NonfairSync();
    }
     abstract static class Sync extends AbstractQueuedSynchronizer {
       ……省略代码
     }
  
  //非公平锁
  static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
  // 公平锁
  static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
……省略
        }
    }
  // lock 方法本质就是调用sync类
  public void lock() {
        sync.lock();
    }
}

lock 加锁过程

按照调用 lock 方法是否抢占锁成功,可以以调用 park 方法为界限,将加锁的过程分为两部分:一部分是当前线程被阻塞前,另一部分是线程被唤醒继续执行后。(这里以非公平锁为例)

阻塞前

1.直接通过CAS尝试获取锁,设置state为1。如果获取成功则将锁标识设为独占,就是是将当前线程设置给 exclusiveOwnerThread。

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

2.如果获取失败,再次尝试获取,调用acquire。

3.tryAcquire ->:判断锁是否被占有,如果空闲则再次尝试CAS获取锁;如果已被占有则对比占有锁的线程是否为本线程,是的话将state+1,这就是可重入锁的关键逻辑。

//AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
//ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
      // cas再次尝试获取
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
      // 可重入逻辑
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

4.如果获取失败则将节点插入队列尾部,如果队列为空,则会初始化队列,并且设置头尾节点为空节点,再将Node设为尾节点。

// 获取锁失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

// 加入同步队列
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
      // 通过CAS设置尾节点为当前节点,前驱节点为之前的尾节点。
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
  // 如果当前链表为空,则在此处进行初始化
    enq(node);
    return node;
}
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
          // 追加到队列尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

5.将新建的Node传入acquireQueued,获取前驱节点,如果节点就是head 头节点,那么尝试CAS竞争锁(head随时释放)。如果抢占成功将头节点设为自己。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
          // 如果是头节点,再次尝试
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

6.如果没有抢占成功,则进入shouldParkAfterFailedAcquire逻辑,将前驱节点设置为Signal,表示后继节点(也就是当前节点)需要前驱节点去唤醒。设置完之后再次进入自旋锁,尝试获得锁。

关于Node的状态这里说明一下:

节点刚创建的时候,status=0,假设这时候本节点就是head节点,那么他会进入else逻辑,将自身状态设置为Signal,然后再次进入自旋,尝试获取锁。如果还是没有获取到锁,那么再次进入shouldParkAfterFailedAcquire方法后会进入第一个if逻辑,方法返回True。

/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops.  Requires that pred == node.prev.
* 如果获取锁失败,检查并且更新节点。如果需要被park阻塞,返回true。
* 在所有的循环逻辑中,这是主要的信号控制逻辑。
*
* pred:表示前驱节点
* node:表示当前线程节点
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
       // 第二次尝试获取锁会进入这段逻辑
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
         // 表明线程已经准备好被阻塞并等待之后被唤醒
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
         // 若pred.waitStatus状态位大于0,说明这个前驱点已经取消了获取锁的操作,
         // doWhile循环会递归删除掉这些放弃获取锁的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 节点刚创建的时候,status=0,逻辑会走到这里将自身状态设置为signal
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
         //若状态位不为Node.SIGNAL,且没有取消操作,则会尝试将前驱节点状态位修改为Node.SIGNAL
        // 表示将会唤醒后继节点
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

7.第二次自旋获取失败后,由于前驱节点已经是Signal,这时进入parkAndCheckInterrupt,将当前线程阻塞,等待被唤醒。后续其他线程如果也尝试抢占锁,会同样被阻塞。

private final boolean parkAndCheckInterrupt() {
  // 阻塞线程
    LockSupport.park(this);
  // 线程继续执行
    return Thread.interrupted();
}


park方法被唤醒后

在其他线程释放锁资源后,唤醒下一个节点,park的后半部分逻辑继续执行。

  1. 继续执行之前Park之后的逻辑,在此处线程被唤醒。这里会返回中断标记,这也是为什么ReentrantLock可以相应中断的原因。

947bba3358a922f1bc9d0b26dce803e8.png
  1. 然后再次进入自旋锁,使用CAS获取到锁标记,将头节点设为当前节点,然后返回中断标记跳出循环。

  2. 至此,获取锁流程结束。

unlock 释放锁过程

1.尝试释放锁,用state减去1,判断是否等于0。如果等于0表示已经完全释放锁,将线程标记设为null。否则释放失败,表示当前线程仍在继续持有,继续持有说明有重入情况。

// ReentrantLock
public void unlock() {
    sync.release(1);
}
// AQS
public final boolean release(int arg) {
  // 释放锁
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
          // 唤醒后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
      // 释放锁
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

2.拿到头节点,然后解锁后继节点。如果当前节点状态小于0(signal=-1),则修改节点status为0。然后向后递归找到status小于等于0的节点(正常为0),调用unpark解除阻塞。返回解锁成功。

// 唤醒后继节点
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)
        compareAndSetWaitStatus(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) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
      //解锁
        LockSupport.unpark(s.thread);
}
cbb020a87ff4b9897e58151568550573.png

3.在这之后便继续开始执行之前被阻塞的线程中的逻辑。

到这里 ReentrantLock 的加解锁过程原理便讲解结束,关于条件队列的内容,有兴趣后续文章会做讲解。

对比 Synchronized

既然已经了解了 ReentrantLock ,那么在此对大家所熟知的 Synchronized 进行一个对比。

与Synchronized相同点:

1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。

但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

2.ReentrantLock和synchronized都是可重入锁。

synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

3.都可以实现线程之间的等待通知机制。使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

与Synchronized 不同点:

  1. ReentrantLock是Java层面的实现,synchronized是JVM层面的实现。

  2. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),而ReentrantLock需要手动释放锁需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。

  3. synchronized是非公平锁,ReentrantLock可以实现公平和非公平锁。

  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。配合重试机制更好的解决死锁。

  5. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。

  6. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。

  7. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。

最后,欢迎大家提问和交流。

如果对你有帮助,欢迎点赞、评论或分享,感谢阅读!

update在MySQL中是怎样执行的,一张图牢记|原创

2022-11-19

8347b3ec709c87f58c96f4e5d2f65d66.jpeg

从二叉查找树到B*树,一文搞懂搜索树的演进!|原创

2022-11-14

410ed804bea17b122f9712a5ab84b1e0.jpeg

问到ThreadLocal,看这一篇就够了|原创

2022-10-13

6da4801fb6b9d1b47120e564dc9536f6.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/34822.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Arduino ESP32使用U3115S芯片控制H桥驱动有刷直流电机

Arduino ESP32实现互补PWM输出 问题提出半桥驱动电路必须是PWM互补输入才能工作Arduino ESP32的互补PWM控制问题提出 直流有刷电机控制使用U3115S芯片。芯片是电压高达300V的半H桥驱动电路&#xff0c;管脚说明&#xff1a; NumberSymbolDescription1VCC低侧固定逻辑电源输入…

牛客网——verilog练习题思路汇总

目录 基础语法 VL1 四选一多路器 VL2 异步复位的串联T触发器 VL3 奇偶校验 VL4 移位运算与乘法 VL5 位拆分与运算 VL6 多功能数据处理器 VL7 求两个数的差值 VL8 使用generate…for语句简化代码 VL9 使用子模块实现三输入数的大小比较 VL10 使用函数实现数据大小端转…

智慧城市建设的原则及规划目标

一、建设原则 智慧城市建设过程中必须把握以下原则&#xff1a; &#xff08;一&#xff09; 遵循市信息化建设总体规划与发展框架。遵循 “十二五” 信息化规划确定的“紧紧围绕国家西部大开发战略目标&#xff0c;强化信息技术和信息化在我市加快推进新型工业化进程中的战略…

数据库周期表整体设计方案

这是学习笔记的第 2442篇文章数据库周期表是我们自定义的名称&#xff0c;从数据视角来说&#xff0c;通常会有三类数据表&#xff1a;字典表&#xff0c;状态表&#xff0c;日志表&#xff0c;其中周期表从定位上更侧重于日志表。为什么叫周期表&#xff0c;主要是因为日志数据…

看着2022年世界杯,我无比怀念98世界杯

作者&#xff1a;朱金灿 来源&#xff1a;clever101的专栏 为什么大多数人学不会人工智能编程&#xff1f;>>> 断断续续看了2022年世界杯部分比赛&#xff0c;我无比怀念1998年世界杯&#xff01;该文权当本人的一点回忆文字&#xff0c;没有经过严谨认证&#xff0…

2023年第三届智能制造与自动化前沿国际会议(CFIMA 2023)

2023年第三届智能制造与自动化前沿国际会议(CFIMA 2023) 重要信息 会议网址&#xff1a;www.cfima.org 会议时间&#xff1a;2023年6月9-11日 召开地点&#xff1a;中国大理 截稿时间&#xff1a;2023年4月20日 录用通知&#xff1a;投稿后2周内 收录检索&#xff1a;EI,…

IDEA创建父子项目

一、搭建父项目 创建第一个maven项目作为父项目 创建完之后建议删掉src文件夹 pom.xml <packaging>POM</packaging><!--依赖版本的锁定--><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</pr…

Linux-Nginx前端项目部署

目录 一、Nginx简介 负载均衡 动静分离 二、Nginx安装 Tomcat负载均衡 准备2个tomcat 修改此三处 修改后保存 启动两个Tomcat 修改Tomcat主界面​编辑 Nginx配置 查看nginx.conf配置 添加/更改配置​编辑 重启Nginx服务&#xff0c;让配置生效 出现权限问题 测试N…

计算机组成原理4小时速成:I/O系统,IO指令,编址方式,串行并行,程序查询传送,中断程序传送,DMA传送,接口组成

计算机组成原理4小时速成&#xff1a;I/O系统&#xff0c;IO指令&#xff0c;编址方式&#xff0c;串行并行&#xff0c;程序查询传送&#xff0c;中断程序传送&#xff0c;DMA传送&#xff0c;接口组成 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#x…

verilog generate

Verilog 生成块generate块允许乘以模块实例或执行任何模块的条件实例化。它提供了基于 Verilog 参数构建设计的能力。当需要多次重复相同的操作或模块实例&#xff0c;或者必须根据给定的 Verilog 参数有条件地包含某些代码时&#xff0c;这些语句特别方便。generate块不能包含…

centos7 clamav 杀毒软件安装

壹&#xff1a;编译安装 1&#xff0c;官网获取源码包 clamav官网地址 2&#xff0c;创建clamav 用户和存放病毒库目录 a, 创建clamav 用户和用户组 groupadd clamav && useradd -g clamav clamav && id clamavb,日志存放目录 mkdir -p /henan/clamav/lo…

Spring中的ApplicationContext所体现出来的工厂模式

Spring初体验 通常我们写代码时&#xff0c;先定义一个Class&#xff0c;然后再别的地方实例化&#xff0c;再进行调用&#xff0c;比如下边的Hello World的例子&#xff0c;类Hello有一个say()方法&#xff0c;用于打印出“Hello World”的字符串&#xff0c;另一个类App&…

python和R绘制箱线图+抖动点

python和R绘制箱线图抖动点 散点箱线图 IPCC AR6 https://www.ipcc.ch/ 通过散点展示数据的直观分布 x轴刻度标签如果是“连续”数据&#xff0c;可添加渐变背景。通过渐变来体现升温幅度&#xff0c;美观形象。 python版本&#xff1a; 先手搓数据&#xff1a; import pan…

【构建ML驱动的应用程序】第 11 章 :监控和更新模型

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

c++11 unique_lock 使用

我们知道c11 提供了很多的类模板供我们使用&#xff0c;其中就有 lock_guard&#xff0c;这个用法也很简单&#xff0c;像这样&#xff1a; std::mutex m_mutex; std::lock_guard<std::mutex> guard(m_mutex); mutex 的 lock 和 unlock 完全是自动的&#xff0c;无需我…

新鲜出炉|基于深度学习的运维日志领域新进展

作者&#xff1a;云智慧算法工程师 Hugo Guo 运维日志领域研究方向主要包含异常日志检测、日志模式解析、日志内容分类、日志告警等。本篇文章介绍了热门异常检测模型 DeepLog、A2Log 等模型&#xff0c;以及云智慧自研模型 Translog 等。与此同时&#xff0c;在文章最后介绍了…

springboot自定义starter实践

创建一个spring项目 仿照spring的规范&#xff0c;artifact命名为xxx-spring-boot-starter 按需添加必要的依赖 这里只作为测试&#xff0c;就按最低的需求来只勾选如下三个 lombok、spring-boot-configuration-processor、spring-boot-autoconfigure 默认生成的项目结构如下…

使用HuggingFace实现 DiffEdit论文的掩码引导语义图像编辑

在本文中&#xff0c;我们将实现Meta AI和Sorbonne Universite的研究人员最近发表的一篇名为DIFFEDIT的论文。对于那些熟悉稳定扩散过程或者想了解DiffEdit是如何工作的人来说&#xff0c;这篇文章将对你有所帮助。 什么是DiffEdit? 简单地说&#xff0c;可以将DiffEdit方法…

【论文解读】Self-Explaining Structures Improve NLP Models

&#x1f365;关键词&#xff1a;性能提升、文本分类、信息推理 &#x1f365;发表期刊&#xff1a;arXiv 2020 &#x1f365;原始论文&#xff1a;https://arxiv.org/pdf/2012.01786.pdf &#x1f365;代码链接https://github.com/ShannonAI/Self_Explaining_Structures_Impro…

Java内部类

Java当中内部类主要有4种&#xff0c;分别是静态内部类、实例内部类/非静态内部类、局部内部类&#xff08;几乎不用&#xff09;、匿名内部类。静态内部类&#xff1a;被static修饰的内部成员类 ①在静态内部类只能访问外部类中的静态成员 ②创建静态内部类对象时&#xff0c;…