02加锁源码分析-ReentrantReadWriteLock原理-AQS-并发编程(Java)

news2024/11/27 18:41:31

文章目录

      • 3.1 加锁
        • 3.1.1 读锁加锁
          • 3.1.1.1 tryAcquireShared()
          • 3.1.1.2 readerShouldBlock()
          • 3.1.1.3 fullTryAcquireShared()
          • 3.1.1.4 doAcquireShared()
        • 3.1.2 写锁加锁
          • 3.1.2.1 tryAcquire()
          • 3.1.2.2 acquireQueued()
      • 3.2 加锁示意图
        • 3.2.1 先写锁在读锁
        • 3.2.2 先读锁在写锁
      • 后记

3.1 加锁

3.1.1 读锁加锁

当前读写锁未加锁,开始加读锁,看下读锁加锁过程。源代码如下:

public void lock() {
    sync.acquireShared(1);
}
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
    	doAcquireShared(arg);
}

执行流程如下:

  • 读锁加锁,调用sync.acquireShared()加读锁
  • acquireShared()方法,执行tryAcquireShared()尝试加读锁,返回负数表示失败;返回整数成功,方法结束。
    • 这里不会有返回0的情况
    • 加锁失败,执行doAcquireShared()执行读锁阻塞流程,这个下面讲解。
  • 下面我们详细分析下读锁加锁在tryAcquireShared()方法中的主要逻辑。
3.1.1.1 tryAcquireShared()

源代码如下:

protected final int tryAcquireShared(int unused) {
	Thread current = Thread.currentThread();
	int c = getState();
	if (exclusiveCount(c) != 0 &&
		getExclusiveOwnerThread() != current)
		return -1;
	int r = sharedCount(c);
	if (!readerShouldBlock() &&
		r < MAX_COUNT &&
		compareAndSetState(c, c + SHARED_UNIT)) {
		if (r == 0) {
			firstReader = current;
			firstReaderHoldCount = 1;
		} else if (firstReader == current) {
			firstReaderHoldCount++;
		} else {
			HoldCounter rh = cachedHoldCounter;
			if (rh == null || rh.tid != getThreadId(current))
				cachedHoldCounter = rh = readHolds.get();
			else if (rh.count == 0)
				readHolds.set(rh);
			rh.count++;
		}
		return 1;
	}
	return fullTryAcquireShared(current);
}

执行流程如下:unused值为1

  • 获取当前线程,获取锁状态(计数)

  • 第一步判断如果当前已经加写锁且锁持有线程非当前线程,返回-1

    • 当前加写锁且锁持有线程是当前线程的情况就是当前线程以获取读锁,现在想获取读锁即锁降级情况,后面关于读写锁特性分析方法,详细讲解。
  • 获取读锁计数,具体计算方式,前面有提到

  • 第二步判断

    1. 读锁是否应该被阻塞

    2. 满足1,读锁计数是否小于最大容量

    3. 满足1,2,通过cas方式对状态计数+1

  • 如果第二步执行成功,对线程读锁进行计数

    • 如果是读锁计数为0,即第一次加读锁
      • firstReader单独记录当前线程,firstReaderHoldCount对当前线程计数1
    • 如果当前线程是第一个加读锁的线程,表示该线程读锁重入,计数+1
    • 否则获取缓存的计数器,判断如果为null
      • 通过readHolds获取当前线程的计算器
      • 不为null,判断计数如果=0,通过readHolds设置计算器
      • 计算器计数+1
    • 返回1,获取锁成功
  • 如果第二步执行失败,执行fullTryAcquireShared()方法

相关说明:

  • 对第一个加读锁的线程有单独的线程变量记录,计数器。其他后续加读锁的线程,通过readHolds设置或获取计数器。
  • readHolds继承ThreadLocal,多线程环境下不会有安全问题。ThreadLocal相关知识,可以查看之前的讲解或者查阅相关文档。
  • 第二步的3个判断条件,只有上一个满足的情况下才会继续向下判断。
  • 这里看出加读锁是可以共享的,通过线程计数器记录同一线程读锁锁重入,锁计数和线程计数器后面解锁时会用到。
3.1.1.2 readerShouldBlock()

此处是读写锁非公平锁中的方法,看下源代码:

final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
	Node h, s;
	return (h = head) != null &&
		(s = h.next)  != null &&
		!s.isShared()         &&
		s.thread != null;
}

判断如果锁竞争队列第二个结点是获取写锁的线程结点,返回true,其他情况返回false。目的防止一直执行获取读锁的线程执行,而获取写锁的线程一直阻塞,下面具体分析。

3.1.1.3 fullTryAcquireShared()

读锁尝试加锁第二步没执行成功,会执行该方法,源代码如下:

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

对应之前的3个判断条件失败的情况,做了相应的处理,执行流程如下:

  • 设置计算器变量rh
  • for循环没有结束条件
    • 获取锁状态
    • 判断是否是加的写锁
      • 是,判断锁持有线程不是当前线程
        • 返回-1,失败
      • 否的话死锁了
    • 否则判断是不是readerShouldBlock(),
      • 是,说明此时锁竞争队列第二个结点为获取写锁的线程结点
        • 判断如果是第一个加写锁的线程再次获取读锁,这里没必要
        • 否则判断rh为空
          • 获取缓存的计数器
          • 判断rh为空或者计数器记录线程非当前线程
            • 获取当前线程的计数器
            • 如果计数为0直接移除,释放资源
        • 判断rh计数为0,返回-1
    • 判断是不是由于读锁计数达到容量上限,是的话直接抛错误
    • 非上述情况,就是之前cas锁计数+1失败,再次尝试cas锁计数+1
      • 成功执行同3.1.1.1中一样的操作,不在重复。

说明:

  • 进入readerShouldBlock()为true的语句块的话,如果是新的获取读锁的线程,会返回-1,执行阻塞流程,目的就是我们在3.1.1.2 中说的,不在重复;如果不是新的获取读锁的线程,那就说明是正在执行已经获取了读锁的线程,这种情况下我认为没有必要,会判断执行计数器清理工作。至于什么环境下会出现后一种情况,暂时不知,遇到的话在记录。
  • 我们这个方法执行是在3.1.1.1方法中调用,如果当前加写锁情况下,又获取读锁,锁持有线程不是当前线程,直接返回-1没问题。如果锁持有线程是当前线程,表示同一线程在获取了写锁的情况下,又获取读锁。
    • 第一种情况,3.1.1.1中第二步的执行失败,进入3.1.1.3执行,会死锁
      • 3个判断条件,第二个判断容量的一般成功,第三个cas,既然持有锁也成功。失败的情况只能是第一个判断条件失败。
    • 所以虽然读写锁支持锁降级,一般不建议使用
3.1.1.4 doAcquireShared()

尝试加锁失败后,执行获取读锁线程的阻塞流程,源代码如下:

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

有没有很熟悉的赶脚😄,对的之前我们讲解过ReentrantLock的加锁流程方法(传送门==加锁和解锁-ReentrantLock详解-AQS-并发编程(Java)==)主要流程一样,下面我们吧不一样的地方讲解下:

  • addWaiter(Node.SHARED)这里结点类型为共享结点,默认null即独占结点
    • 共享结点:即获取读锁的结点,在唤醒的时候,会把连续的共享结点唤醒,想象一下撸串的感觉🤤
    • 独占结点:获取写锁的结点,唤醒的时候只唤醒当前结点,且会把所持有线程设置为结点线程。
  • tryAcquireShared()这里尝试获取读锁,具体逻辑3.1.1.1中有讲解。
  • setHeadAndPropagate(),这里会执行唤醒共享结点的操作,具体我们放在解锁中讲解。

3.1.2 写锁加锁

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

依然很眼熟,下面讲解了和ReentrantLock不一样的tryAcquire()和acquireQueued()方法,其他参考ReentrantLock加锁流程。

3.1.2.1 tryAcquire()

源代码如下:

protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

写锁尝试加锁流程如下:

  • 获取锁状态,获取写锁计数
  • 第一步判断锁计数不等0,说明已经加锁
    • 如果写锁计数为0或者锁持有线程非当前线程,返回false,失败
      • 写锁计数为0,说明加的是读锁,读写互斥
    • 判断如果写锁计数+1超过读锁容量,抛错误
    • 锁计数+1,返回true,加写锁成功
      • 这里没有通过cas设置原因,程序执行到这儿,一定是当前线程持有锁
  • 锁计数为0 表示没有加锁
  • 执行第二步的判断,2个条件
    • 第一个条件判断获取写锁线程是否应该被阻塞
      • 公平锁实现:判断如果锁竞争队列有前驱结点,返回true;否则返回false。非公平实现:直接放回false
    • 第二个判断条件,cas直接设置锁状态
    • 执行成功,返回false,尝试加锁失败
  • 继续执行,说明cas尝试设置状态成功
    • 设置锁持有线程为当前线程,返回true,成功加写锁

说明:

  • 关于读写锁公平和非公平实现,只在writerShouldBlock()和readerShouldBlock()这2个方法有区别,其他都相同,具体逻辑,在后面关于读写锁的特性在讲解。
3.1.2.2 acquireQueued()
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
			// tryAcquire不同于ReentrantLock
            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);
    }
}

不说了,这里不同的是tryAcquire()这里是尝试获取读锁,其他参考ReentrantLock。

因为之前学习AQS之一的ReentrantLock,这里相对来说容易理解,就没有在画流程图,简单过了下加锁流程。

3.2 加锁示意图

3.2.1 先写锁在读锁

以先加写锁为例,thread-0获取写锁,示意图如下3.2-1所示:在这里插入图片描述

然后thread-1,thread-2尝试获取读锁,thread-3尝试获取写锁,示意图如下3.2-3所示:在这里插入图片描述

  • 其中结点的nextWaiter值为Shared为共享结点,即获取读锁的线程结点;值为null为独占结点,即为获取写锁的线程结点。

3.2.2 先读锁在写锁

thread-0,thread-1尝试获取读锁,thread-2尝试获取写锁,thread-3,thread-4尝试获取读锁,示意图如下3.2.2-1所示:在这里插入图片描述

后记

如有问题,欢迎交流讨论。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/concurrent

参考:

[1]黑马程序员.黑马程序员深入学习Java并发编程,JUC并发编程全套教程[CP/OL].2020-01-18/2022-12-12.p253~p255.

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

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

相关文章

全网惟一面向软件测试人员的Python基础教程-Python数据类型中有那些故事呢?

全网惟一面向软件测试人员的Python基础教程 起点&#xff1a;《python软件测试实战宝典》介绍 第一章 为什么软件测试人员要学习Python 第二章 学Python之前要搞懂的道理 第三章 你知道Python代码是怎样运行的吗&#xff1f; 第四章 Python数据类型中有那些故事呢&#xff1f;…

PHP+Laravel框架RabbitMQ简单使用

RabbitMQ安装教程请转到&#xff1a;RabbitMQ安装教程&#xff08;超详细&#xff09; 1、创建生产者 在app/Http/Controllers里创建一个php控制器文件&#xff0c; namespace App\Http\Controllers;use App\Http\Controllers\Controller; //引入amqp扩展 use PhpAmqpLib\Co…

铁路轨道交通智慧管理系统

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&…

sklearn基础篇(九)-- 主成分分析(PCA)

1 引言 降维是对数据高维度特征的一种预处理方法。降维是将高维度的数据保留下最重要的一些特征&#xff0c;去除噪声和不重要的特征&#xff0c;从而实现提升数据处理速度的目的。在实际的生产和应用中&#xff0c;降维在一定的信息损失范围内&#xff0c;可以为我们节省大量的…

机器学习项目中Xgboost深入理解(二)梯度及GB的理解

Xgboost 与 GBDT同源&#xff0c;那么所谓的梯度是什么意思呢&#xff1f;G、B又代表什么。 1. 梯度Gradient 梯度是一个向量&#xff08;矢量&#xff09;&#xff0c;表示梯度是方向导数在某一点的最大值。理解为某函数&#xff08;可以指损失函数&#xff09;在某点处的方…

Fluent后处理的色谱设置

PART 1 概述 对物理场的空间分布进行可视化展示&#xff0c;在科研、工程等领域是必不可少的环节。 将物理量不同数值赋予不同色彩&#xff0c;是一种很常见的物理场可视化方法。通过色彩的变化&#xff0c;即可识别物理量的分布规律。 物理量数值和颜色之间的对应关系称为色…

Linux常用指令

目录 1.1关于目录操作的指令 1.2文件操作的指令 1.3创建/删除目录操作 1.4目录/文件的移动/复制/改名 1.5 vim对文件进行编辑 1.1关于目录操作的指令 ls 列出当前目录下都有啥 ls 具体路径或 /注意中间含有一个空格&#xff0c;列出跟目录下或指定路径的内容 ls -l / 以列表的…

mysql日志篇

mysql日志篇1、Undo-log 撤销日志 Undo即撤销的意思&#xff0c;大家通常也习惯称它为回滚日志。如果大家有仔细研究过 MySQL 的日志&#xff0c;应该会发现 Undo-log 并不存在单独的日志文件&#xff0c;也就是磁盘中并不会存在 xx-undo.log 这类的文件&#xff0c;那 Undo-lo…

12个python超强学习网站

一、python学习网站 1 CSDN 地址&#xff1a; CSDN学院 特点&#xff1a;从免费视频到入门项目&#xff0c;从入门到进阶&#xff0c;学习视频应有尽有&#xff0c;还有Python学习社区&#xff0c;良好的学习和沟通氛围&#xff01; 2 Python123 地址&#xff1a;python123…

ReentrantLock源码分析

ReentrantLock源码分析 源码思想加锁&#xff1a; (1)使用构造方法设置该锁为公平锁FairSync或非公平锁NonfairSync;(本处选定非公平锁) (2)调用lock方法进行获取锁,第一次(getState() 0)在NonfairSync中使用CAS(compareAndSwapInt)直接获取(此处默认未获取成功),第一次获取失…

docker--快速启动功能性Hadoop集群进行功能验证

工作时&#xff0c;有时需要快速启动功能性Hadoop集群来测试一些hadoop相关的项目及功能。 本文讲解如何基于docker&#xff0c;快速搭建一个功能性Hadoop集群&#xff0c;来测试验证一些功能。 使用的docker镜像 使用的docker镜像的源码地址是&#xff1a;hadoop-hive-3.1.x…

挑战杯刷题

字符串相关 字符串相关&#xff1a; 插入元素&#xff1a; str.insert(pos,num,string);//pos是待查入的位置&#xff0c;num是插入几次&#xff0c;string是待插入的字符串 str.insert(pos,string);//与上面insert一样&#xff0c;不过只在pos位置插入string一次查找指定字符串…

[附源码]Node.js计算机毕业设计高校宿舍管理系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

Mac M1配置SSH Key并添加

配置SSH Key 设置github账户信息 添加github的名字和绑定的邮箱 git config --global user.name “your_name” git config --global user.email “your_emailexample.com” 查看一下 git config --global --list 如果添加成功就会显示刚才的名称和邮箱 创建 创建SSH Key s…

关于切换场景加载耗时的优化问题

1&#xff09;关于切换场景加载耗时的优化问题 ​2&#xff09;SpriteAtlas打包AssetBundle后运行时内存大小和什么有关系 3&#xff09;手机上使用MRT的限制和兼容性 4&#xff09;如何控制粒子系统组件数量的上限 这是第317篇UWA技术知识分享的推送。今天我们继续为大家精选了…

大数据-玩转数据-深入浅出K8S

一、K8S和Docker的比较 Docker 是一个开源的、轻量级的容器引擎&#xff0c;和 VMware 虚拟机相比&#xff0c;Docker 使用容器承载应用程序&#xff0c;而不使用操作系统&#xff0c;所以它的开销很少&#xff0c;性能很高。但是&#xff0c;Docker 对应用程序的隔离不如虚拟…

SO_REUSEPORT socket选项介绍以及在nginx上的配置

SO_REUSEPORT (reuseport) 是网络的一个选项设置&#xff1a; 它能开启内核功能&#xff1a;网络链接分配 内核负载均衡&#xff0c;该功能允许多个进程/线程 bind/listen 相同的 IP/PORT&#xff0c;提升了新链接的分配性能。reuseport 也是内核解决 惊群问题 的优秀方案&…

CoSENT:比sentence bert更好的向量表征

本文是对https://spaces.ac.cn/archives/8847文章的总结 有监督的文本表征主流方案是sentence bert&#xff0c;虽然sentence BERT效果还行&#xff0c;但是存在训练和预测不一致的问题&#xff0c;而一些文章也指出而如果直接优化预测目标cos值&#xff0c;效果往往特别差&…

方寸间联结世界,中国直播电商如何诠释“人类命运共同体”?

十几个人凝神屏息地盯着屏幕&#xff0c;仿佛静止了一般&#xff0c;眼睛一眨不眨&#xff0c;生怕错过了什么。直到听到屏幕另一端终于有了动静&#xff0c;悬在他们心里的石头才落下——“谢谢大家的支持&#xff0c;全部没有了。” 话音未落&#xff0c;这边已爆发了欢呼声…

只工作,不上班,「远程办公」是不是理想的工作方式?

在我们90后眼中&#xff0c;上班和工作是两回事。准确来说。上班是为别人做事&#xff0c;而工作是为自己。 圆桌派有一期的话题是“不想工作怎么破&#xff1f;”当时&#xff0c;几位嘉宾说&#xff1a;“不是不想工作&#xff0c;是不想上班”&#xff0c;“周围很多年轻人…