深入ReentrantLock锁

news2024/11/28 3:34:49

1. 前言

今天我们来探讨下另一个核心锁ReentrantLock. 从具体的实现到JVM层面是如何实现的。 我们都会一一进行讨论的,好了,废话不多说了,我们就开始吧

2. ReentrantLock 以及synchronized

  • 核心区别:
    • ReentrantLock 是一个抽象的基类
    • synchronized 是一个关键字。
    • 从JVM层面来看的话,都是互斥锁的一种实现
  • 效率区别:
    • 如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。
    • 而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的
  • 底层实现区别:
    • ReentrantLock 基于AQS实现的
    • synchronized 是基于ObjectMonitor
  • 功能区别:
    • ReentrantLock的功能比synchronized更全面
    • ReentrantLock支持公平锁和非公平锁
    • ReentrantLock可以指定等待锁资源的时间

3. 简单实例展示

public class T21_Thread_Lock11 {

    public static int count = 0;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(T21_Thread_Lock11::add);
        Thread t2 = new Thread(T21_Thread_Lock11::add);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count);
    }

    public static void add() {
        try {
            lock.lock();
            for (int i = 0; i < 100000; i++) {
                count ++;
            }
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock 是需要手动开启以及关闭锁的。所以为了防止程序中途出现异常,将锁关闭的部分放到finally中,说明一定会执行的。

4. 核心分析

4.1 AQS概述

  1. AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现
  1. 首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量
  1. 其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象

4.2 关键字分析

4.2.1 抽象基类

在这里插入图片描述

通过上述的截图中我们可以看到抽象类AbstractQueuedSynchronizer 是由Doug Lea 在JDK1.5之后实现的

4.2.2 AbstractQueuedSynchronizer 关键字

public abstract class AbstractQueuedSynchronizer {
	// 表示抢锁状态。 默认是0, 每次抢锁就会+1 锁释放就会-1
	private volatile int state;
	// 双向链表的头节点
	private transient volatile Node head;
	// 双向链表的尾节点
	private transient volatile Node tail;
}

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;

	// 节点状态。一般都是表示后继节点的状态。例如:如果是-1 表示挂起
	volatile int waitStatus;
	// 表示前继节点
	volatile Node prev;
	// 表示后继节点
	volatile Node next;
	// 表示抢锁的线程
	volatile Thread thread;
}
  • state 表示线程抢锁的一个状态
  • head 双向链表的头节点
  • tail 双向链表的尾节点
  • SHARED 共享锁的标志
  • EXCLUSIVE 互斥锁的标志
  • waitStatus 其实下一个锁的状态。比如说是否需要唤醒等
  • thread 表示当前执行/ 待执行 的线程
  • prev 表示上一个Node节点
  • next 表示下一个Node节点

4.2.3 加锁流程的概述

非公平锁的加锁方式
image.png

4.2.4 lock 实现部分

非公平锁,1. 首先先进行抢锁,如果抢锁成功了将状态0 修改为 状态1,然后设置持有锁的线程。2. acquire 内部就是再次尝试抢锁,然后添加到队列中

  • 非公平锁
final void lock() {
	// 尝试修改state状态,进行抢锁
    if (compareAndSetState(0, 1))
    	// 如果抢锁成功的话 直接设置线程为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	// 内部再次尝试抢锁,反之就是添加到队列中
        acquire(1);
}
  • 公平锁
final void lock() {
  // 内部再次尝试抢锁,反之就是添加到队列中
  acquire(1);
}

4.2.5 acquire 实现部分

方法tryAcquire 再次尝试抢锁,如果抢锁抢不到的话,直接将节点标记为互斥锁后封装为node,添加到双向链表中

public final void acquire(int arg) {
    // 再次抢锁。如果没有抢到的话 返回false
    if (!tryAcquire(arg) &&
        // 将线程封装为node  添加到双向链表 尾部
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

4.2.6 tryAcquire 实现部分

抢锁大致分为两种形式:1. 如果状态state为0的话,表示没有线程抢锁,尝试抢锁,抢锁成功后直接返回。 2. 状态state 不是0 && 被抢到的锁的线程 就是 当前的线程,此时表示是重入锁。 如果两者都不是的话,直接返回false。

final boolean nonfairTryAcquire(int acquires) {
    // 表示获取当前线程
    final Thread current = Thread.currentThread();
    // 表示线程抢锁的state状态
    int c = getState();
    // 如果是0的话 表示还未抢到锁
    if (c == 0) {
        // 再次尝试抢锁。如果抢锁成功直接返回true
        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;
    }

    // 上锁两种情况都不是的话 直接返回false
    return false;
}

4.2.7 addWaiter 实现部分

将线程封装为Node元素,添加到链表的尾部

private Node addWaiter(Node mode) {
    // 将当前节点 封装为node
    Node node = new Node(Thread.currentThread(), mode);
    // 表示尾节点
    Node pred = tail;
    // 如果尾节点不为空。  如果尾节点为null的话  说明链表中没有节点
    if (pred != null) {
        // 维持双向链表的关系
        node.prev = pred;
        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
            // 利用CAS加锁 来创建伪节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 维持节点关系
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

4.2.8 acquireQueued 实现部分

// 当前没有拿到锁资源后,并且到AQS排队之后触发的方法
final boolean acquireQueued(final Node node, int arg) {
    // 判断锁是否获取成功
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 表示当前节点的前继节点
            final Node p = node.predecessor();
            // 如果前继节点是head 再次尝试获取锁 如果前继节点是head的话,那么node就是第二个元素
            if (p == head && tryAcquire(arg)) {
                // 设置head
                setHead(node);
                // 将原来的head 设置为空。 方便GC回收
                p.next = null; // help GC

                failed = false;
                return interrupted;
            }
            // 没拿到锁资源...
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


// 修改head节点的方法
private void setHead(Node node) {
    // 将当前节点设置为head 节点
    head = node;
    // 当前节点是伪节点 无需要设置thread 线程
    node.thread = null;
    // 无前节点
    node.prev = null;
}



private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 表示前继节点的 后节点状态
    int ws = pred.waitStatus;
    // 如果后节点状态是-1的话  说明是后节点是挂起状态 直接返回true
    if (ws == Node.SIGNAL)
        return true;
        // 如果ws是1的话 满足ws > 0的条件。 但是此时的节点是无效节点
    if (ws > 0) {
        // 通过循环之前往前找 直到是有效节点,将节点挂载到后面
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 设置状态 表示需要将当前节点挂起
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

4.2.9 tryLock 无参实现部分

// 此方法也是尝试加锁。 
// 字段acquires 此时的值为1
final boolean nonfairTryAcquire(int acquires) {
    // 获取执行的线程
    final Thread current = Thread.currentThread();
    // 表示抢锁 状态。
    int c = getState();
    // 此时表示 还没有线程抢到锁
    if (c == 0) {
        // 利用CAS来尝试修改state 状态
        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.2.10 tryLock 有参实现部分

大致的原理就是使用死循环来 递归判断时间来抢锁。如果时间到了 && 还没抢到锁 就直接跳出循环。

// 此方法也是尝试加锁
// 修改的state状态的值 以及时间
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 如果此线程被 标记为中断了 直接包异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // tryAcquire 尝试加锁。
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

// 尝试指定时间内抢锁
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 如果延迟的时间为0的话 直接返回
    if (nanosTimeout <= 0L)
        return false;
    // 表示结束时间
    final long deadline = System.nanoTime() + nanosTimeout;
    // 将节点添加到链表中
    final Node node = addWaiter(Node.EXCLUSIVE);
    // 默认抢锁是失败的
    boolean failed = true;
    try {
        // 使用一个死循环进行抢锁
        for (;;) {
            // 表示前继节点
            final Node p = node.predecessor();
            // 如果前继节点是头节点。 再次尝试抢锁
            if (p == head && tryAcquire(arg)) {
                // 抢锁成功后 将当前节点设置为头结点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 计算剩余时间
            nanosTimeout = deadline - System.nanoTime();
            // 如果没有剩余时间了 直接返回
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


// 节点状态判断的方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 如果此时状态是-1的话 表示node节点是一个挂起的状态
    if (ws == Node.SIGNAL)
        return true;
        // 如果是ws是>0的话 表示是无效状态。
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果此时是等于0的状态的话, 将状态0修改为-1. 表示后继节点挂起
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

4.2.11 AQS 常见的问题

4.2.11.1 AQS中为什么要有一个虚拟的head节点

因为AQS提供了ReentrantLock的基本实现,而在ReentrantLock释放锁资源时,需要去考虑是否需要执行unparkSuccessor方法,去唤醒后继节点。

因为Node中存在waitStatus的状态,默认情况下状态为0,如果当前节点的后继节点线程挂起了,那么就将当前节点的状态设置为-1。这个-1状态的出现是为了避免重复唤醒或者释放资源的问题。

因为AQS中排队的Node中的线程如果挂起了,是无法自动唤醒的。需要释放锁或者释放资源后,再被释放的线程去唤醒挂起的线程。 因为唤醒节点需要从整个AQS双向链表中找到离head最近的有效节点去唤醒。而这个找离head最近的Node可能需要遍历整个双向链表。如果AQS中,没有挂起的线程,代表不需要去遍历AQS双向链表去找离head最近的有效节点。

为了避免出现不必要的循环链表操作,提供了一个-1的状态。如果只有一个Node进入到AQS中排队,所以发现如果是第一个Node进来,他必须先初始化一个虚拟的head节点作为头,来监控后继节点中是否有挂起的线程。

4.2.11.2 AQS中为什么选择使用双向链表,而不是单向链表

首先AQS中一般是存放没有获取到资源的Node,而在竞争锁资源时,ReentrantLock提供了一个方法,lockInterruptibly方法,也就是线程在竞争锁资源的排队途中,允许中断。中断后会执行cancelAcquire方法,从而将当前节点状态置位1,并且从AQS队列中移除掉。如果采用单向链表,当前节点只能按到后继或者前继节点,这样是无法将前继节点指向后继节点的,需要遍历整个AQS从头或者从尾去找。单向链表在移除AQS中排队的Node时,成本很高。

当前在唤醒后继节点时,如果是单向链表也会出问题,因为节点插入方式的问题,导致只能单向的去找有效节点去唤醒,从而造成很多次无效的遍历操作,如果是双向链表就可以解决这个问题。

5. 总结:

  1. 其实ReentrantLock的实现逻辑还是很简单,有几个比较重要的点,这里可以描述下:
  2. 字段state,被volatile修饰。保证了可见性以及有序性。为抢锁是否成功提供了凭据
  3. 用双向链表来存放排队挂起的线程。无非是使用特殊的手段来维护链表
  4. 每个链表节点中存在属性waitStatus来表示下个节点是否挂起,默认是0,如果挂起表示-1. 同时存在伪head节点来 表示第一个真实节点是否被挂起

废话不多说了,分享就到这里,如果什么新的想法评论区记得及时留言哦。

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

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

相关文章

MVC框架知识详解

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;Java案例分…

机器学习课程学习随笔

文章目录本文来源机器学习简介机器学习流程机器学习可以完成如下功能&#xff1a;机器学习应用场景金融领域零售领域机器学习分类机器学习实现基于python等代码自己实现本文来源 本博客 是通过学习亚马逊的官方机器学习课程的随笔。 课程参考链接https://edu.csdn.net/course/…

爬虫与反爬虫 - 道高一尺魔高一丈 - 2013最新 - JS逆向 - Python Scrapy实现 - 爬取某天气网站历史数据

目录 背景介绍 网站分析 第1步&#xff1a;找到网页源代码 第2步&#xff1a;分析网页源代码 Python 实现 成果展示 后续 Todo 背景介绍 今天这篇文章&#xff0c;3个目的&#xff0c;1个是自己记录&#xff0c;1个是给大家分享&#xff0c;还有1个是向这个被爬网站的前…

synchronized锁膨胀(附代码分析)

synchronized锁膨胀 1. 基本概念 Java对象头 Java对象的对象头信息中的 Mark Word 主要是用来记录对象的锁信息的。 现在看一下 Mark Word 的对象头信息。如下&#xff1a; 其实可以根据 mark word 的后3位就可以判断出当前的锁是属于哪一种锁。注意&#xff1a;表格中的…

shell脚本练习2023年下岗版

shell脚本练习 1.判断指定进程的运行情况 #!/bin/bash NAMEhttpd #这里输入进程的名称 NUM$(ps -ef |grep $NAME |grep -vc grep) if [ $NUM -eq 1 ]; thenecho "$NAME running." elseecho "$NAME is not running!" fi2.判断用户是否存在 #!/bin/bash r…

【RabbitMQ】安装、启动、配置、测试一条龙

一、基本环境安装配置 1.英文RabbitMQ是基于erlang开发的所以需要erlang环境,点击以下链接下载安装 Downloads - Erlang/OTP 2.官网下载RabbitMQ安装包并安装 Installing on Windows — RabbitMQ 3.配置erlang本地环境变量(和JAVAHOME类似) 4.cmd查看erlang版本 5.点击以下…

自己看的操作系统

计算机网络冯诺依曼体系进程线程内核和虚拟内存os管理线程冯诺依曼体系 计算机五大组成&#xff1a;输入设备、输出设备、控制器、运算器、存储器 进程线程 这些应用都是进程 进程相当于一个菜谱&#xff0c;读取到内存中去使用。 电脑一时间能运行很多进程。 进程中为什么要…

excel函数技巧:MAX在数字查找中的应用妙招

大家都知道VLOOKUP可以按给定的内容去匹配到我们所需的数据&#xff0c;正因为如此&#xff0c;它在函数界有了很大的名气。但是今天要分享的这三个示例&#xff0c;如果使用VLOOKUP去匹配数据的话&#xff0c;就有些麻烦了。就在VLOOKUP头疼不已的时候&#xff0c;MAX函数二话…

2022 年度总结

1、CSDN 年度总结 2022年的粉丝涨幅比较明显竟然超过了之前几年的总和&#xff0c;这是比较意外的。应该是因为今年研究了一些云原生、元宇宙的原因&#xff0c;方向比努力真的重要的多。 1500的阅读确实没想到~~~说明低头一族还是没白当 涨粉稍微明细&#xff0c;不过还需…

English Learning - L1-11 时态 + 情态动词 2023.1.9 周一

English Learning - L1-11 时态 情态动词 2023.1.9 周一8 时态8.4 完成进行时&#xff08;一&#xff09;现在完成进行时核心思维&#xff1a;动作开始于现在之前&#xff0c;并有限地持续下去&#xff0c;动作到目前为止尚未完成1. 动作从过去某时开始一直持续到现在并可能继…

【Python】如何使用python将一个py文件变成一个软件?

系列文章目录 这个系列文章将写一些python中好玩的小技巧。 第一章 使用Python 做一个软件 目录 系列文章目录 前言 一、第一步&#xff1a;写好文件 二、第二步&#xff1a;生成程序 1.安装库 2.使用安装的库进行转化 总结 前言 本文重点说如何将py文件转化为exe文件…

回溯法--符号三角形(杂记)

回溯法说来简单&#xff0c;写起来难&#xff0c;真的是要愁死。回溯法有两种模板--子集树、排列树5.4符号三角形--dfs计算多少个满足条件的符号三角形&#xff0c;同号下面为“”&#xff0c;异号下面为“-”。根据异或的规则我们令“”0&#xff0c;“-”1&#xff0c;(异或的…

postgresql 启用ssl安全连接方式

SSL的验证流程 利用openssl环境自制证书 CA 证书签发 创建私钥ca.key,使用des3算法,有效期2048天 openssl genrsa -des3 -out ca.key 2048生成根CA证书请求&#xff08;.csr&#xff09; openssl req -new -key ca.key -out ca.csr -subj "/CCN/STGuangDong/LGuangZhou…

Cloudflare免费版不支持cname解析解决办法

最近调整CDN&#xff0c;使用云盾CDN的话基本上节点都在国内&#xff0c;国外访问就比较难了&#xff0c;虽然我们的站国外用户基本没有&#xff0c;但作为一个有大抱负的站长&#xff0c;眼界必须得宽&#xff0c;必须得支持国外访问才行&#xff01;说起国外免费CDN&#xff…

iOS开发之Code:-402653103,Code:5

问题一&#xff1a;Code&#xff1a;-402653103 Demo中添加了第三方库&#xff0c;然后运行Demo时&#xff0c;总是运行不起来&#xff0c;现象如下&#xff1a; 遇到这种问题常见的几种方式&#xff1a; 方式一&#xff1a;command shift K&#xff0c;清理Xcode缓存&…

常用的字符串与内存操作函数(1)

Tips 1. 2. 3. 在进行数值计算的时候&#xff0c;补码能算对&#xff0c;因此计算机里面放的都是补码&#xff0c;运算的对象都是补码 但是与真实数值吻合的是原码&#xff0c;因此打印&#xff0c;求值等都要转化为原码 4. for (exp1 ; exp2 ; exp3)&#xff0c;是先…

从0到1完成一个Vue后台管理项目(十九、地图区域样式设置、区域文字和立体设置)

往期 从0到1完成一个Vue后台管理项目&#xff08;一、创建项目&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;二、使用element-ui&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;三、使用SCSS/LESS&#xff0c;安装图标库&#xff09; 从0到1完成一个Vu…

上半年要写的博客文章25

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

分享86个NET源码,总有一款适合您

NET源码 分享86个NET源码&#xff0c;总有一款适合您 链接&#xff1a;https://pan.baidu.com/s/1JOY-9pJIM7sUhafxupMaZw?pwdfs2y 提取码&#xff1a;fs2y 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c;大家下载…

Blender里的三种绑定:(一)主从绑定

文章目录Blender里的三种绑定.主从绑定.进行物体绑定.进行顶点绑定.解除绑定.保持变换.无反向.进行晶格绑定.Blender里的三种绑定. 1 Blender中一共有三种绑定模式&#xff0c;分别是 主从绑定&#xff0c;约束&#xff0c;骨骼 主从绑定. 1 主从绑定即父子关系&#xff0c;…