读写锁ReentrantReadWriteLock源码分析

2024-03-31 15:38

本文主要是介绍读写锁ReentrantReadWriteLock源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 读写锁的介绍
    • 写锁详解
      • 写锁的获取
      • 写锁的释放
    • 读锁详解
      • 读锁的获取
      • 读锁的释放
    • 锁降级

读写锁的介绍

在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized(关于synchronized可以看这篇文章)或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间,WriteLock与ReadLock之间以及ReadLock与ReadLock之间进行分析。更多关于读写锁特性介绍大家可以看源码上的介绍(阅读源码时最好的一种学习方式,我也正在学习中,与大家共勉),这里做一个归纳总结:

  1. 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
  2. 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
  3. 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁

要想能够彻底的理解读写锁必须能够理解这样几个问题:1. 读写锁是怎样实现分别记录读写状态的?2. 写锁是怎样获取和释放的?3.读锁是怎样获取和释放的?我们带着这样的三个问题,再去了解下读写锁。

写锁详解

写锁的获取

同步组件的实现聚合了同步器(AQS),并通过重写同步器(AQS)中的方法实现同步组件的同步语义(关于同步组件的实现层级结构可以看这篇文章,AQS的底层实现分析可以看这篇文章)。因此,写锁的实现依然也是采用这种方式。在同一时刻写锁是不能被多个线程所获取,很显然写锁是独占式锁,而实现写锁的同步语义是通过重写AQS中的tryAcquire方法实现的。源码为:

protected final boolean tryAcquire(int acquires) {/** Walkthrough:* 1. If read count nonzero or write count nonzero*    and owner is a different thread, fail.* 2. If count would saturate, fail. (This can only*    happen if count is already nonzero.)* 3. Otherwise, this thread is eligible for lock if*    it is either a reentrant acquire or*    queue policy allows it. If so, update state*    and set owner.*/Thread current = Thread.currentThread();// 1. 获取写锁当前的同步状态int c = getState();// 2. 获取写锁获取的次数int w = exclusiveCount(c);if (c != 0) {// (Note: if c != 0 and w == 0 then shared count != 0)// 3.1 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话// 当前线程获取写锁失败if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquire// 3.2 当前线程获取写锁,支持可重复加锁setState(c + acquires);return true;}// 3.3 写锁未被任何线程获取,当前线程可获取写锁if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;
}

这段代码的逻辑请看注释,这里有一个地方需要重点关注,exclusiveCount©方法,该方法源码为:

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

其中EXCLUSIVE_MASK为: static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; EXCLUSIVE _MASK为1左移16位然后减1,即为0x0000FFFF。而exclusiveCount方法是将同步状态(state为int类型)与0x0000FFFF相与,即取同步状态的低16位。那么低16位代表什么呢?根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论同步状态的低16位用来表示写锁的获取次数。同时还有一个方法值得我们注意:

static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,现在我们可以得出另外一个结论同步状态的高16位用来表示读锁被获取的次数。现在还记得我们开篇说的需要弄懂的第一个问题吗?读写锁是怎样实现分别记录读锁和写锁的状态的,现在这个问题的答案就已经被我们弄清楚了,其示意图如下图所示:

在这里插入图片描述

现在我们回过头来看写锁获取方法tryAcquire,其主要逻辑为:当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态

写锁的释放

写锁释放通过重写AQS的tryRelease方法,源码为:

protected final boolean tryRelease(int releases) {if (!isHeldExclusively())throw new IllegalMonitorStateException();//1. 同步状态减去写状态int nextc = getState() - releases;//2. 当前写状态是否为0,为0则释放写锁boolean free = exclusiveCount(nextc) == 0;if (free)setExclusiveOwnerThread(null);//3. 不为0则更新同步状态setState(nextc);return free;
}

源码的实现逻辑请看注释,不难理解与ReentrantLock基本一致,这里需要注意的是,减少写状态int nextc = getState() - releases;只需要用当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的

读锁详解

读锁的获取

看完了写锁,现在来看看读锁,读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。按照之前对AQS介绍,实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法为:

protected final int tryAcquireShared(int unused) {/** Walkthrough:* 1. If write lock held by another thread, fail.* 2. Otherwise, this thread is eligible for*    lock wrt state, so ask if it should block*    because of queue policy. If not, try*    to grant by CASing state and updating count.*    Note that step does not check for reentrant*    acquires, which is postponed to full version*    to avoid having to check hold count in*    the more typical non-reentrant case.* 3. If step 2 fails either because thread*    apparently not eligible or CAS fails or count*    saturated, chain to version with full retry loop.*/Thread current = Thread.currentThread();int c = getState();//1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前// 线程获取读锁失败返回-1if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&//2. 当前线程获取读锁compareAndSetState(c, c + SHARED_UNIT)) {//3. 下面的代码主要是新增的一些功能,比如getReadHoldCount()方法//返回当前获取读锁的次数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;}//4. 处理在第二步中CAS操作失败的自旋已经实现重入性return fullTryAcquireShared(current);
}

代码的逻辑请看注释,需要注意的是 当写锁被其他线程获取后,读锁获取失败,否则获取成功利用CAS更新同步状态。另外,当前同步状态需要加上SHARED_UNIT((1 << SHARED_SHIFT)即0x00010000)的原因这是我们在上面所说的同步状态的高16位用来表示读锁被获取的次数。如果CAS失败或者已经获取读锁的线程再次获取读锁时,是靠fullTryAcquireShared方法实现的,这段代码就不展开说了,有兴趣可以看看。

读锁的释放

读锁释放的实现主要通过方法tryReleaseShared,源码如下,主要逻辑请看注释:

protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();// 前面还是为了实现getReadHoldCount等新功能if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();int count = rh.count;if (count <= 1) {readHolds.remove();if (count <= 0)throw unmatchedUnlockException();}--rh.count;}for (;;) {int c = getState();// 读锁释放 将同步状态减去读状态即可int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))// Releasing the read lock has no effect on readers,// but it may allow waiting writers to proceed if// both read and write locks are now free.return nextc == 0;}
}

锁降级

读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:

void processCachedData() {rwl.readLock().lock();if (!cacheValid) {// Must release read lock before acquiring write lockrwl.readLock().unlock();rwl.writeLock().lock();try {// Recheck state because another thread might have// acquired write lock and changed state before we did.if (!cacheValid) {data = ...cacheValid = true;}// Downgrade by acquiring read lock before releasing write lockrwl.readLock().lock();} finally {rwl.writeLock().unlock(); // Unlock write, still hold read}}try {use(data);} finally {rwl.readLock().unlock();}}
}

这篇关于读写锁ReentrantReadWriteLock源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/864675

相关文章

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory

MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析

《MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析》本文将详细讲解MyBatis-Plus中的lambdaUpdate用法,并提供丰富的案例来帮助读者更好地理解和应... 目录深入探索MyBATis-Plus中Service接口的lambdaUpdate用法及示例案例背景

MyBatis-Plus中静态工具Db的多种用法及实例分析

《MyBatis-Plus中静态工具Db的多种用法及实例分析》本文将详细讲解MyBatis-Plus中静态工具Db的各种用法,并结合具体案例进行演示和说明,具有很好的参考价值,希望对大家有所帮助,如有... 目录MyBATis-Plus中静态工具Db的多种用法及实例案例背景使用静态工具Db进行数据库操作插入

Go使用pprof进行CPU,内存和阻塞情况分析

《Go使用pprof进行CPU,内存和阻塞情况分析》Go语言提供了强大的pprof工具,用于分析CPU、内存、Goroutine阻塞等性能问题,帮助开发者优化程序,提高运行效率,下面我们就来深入了解下... 目录1. pprof 介绍2. 快速上手:启用 pprof3. CPU Profiling:分析 C

MySQL表锁、页面锁和行锁的作用及其优缺点对比分析

《MySQL表锁、页面锁和行锁的作用及其优缺点对比分析》MySQL中的表锁、页面锁和行锁各有特点,适用于不同的场景,表锁锁定整个表,适用于批量操作和MyISAM存储引擎,页面锁锁定数据页,适用于旧版本... 目录1. 表锁(Table Lock)2. 页面锁(Page Lock)3. 行锁(Row Lock

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

C#使用DeepSeek API实现自然语言处理,文本分类和情感分析

《C#使用DeepSeekAPI实现自然语言处理,文本分类和情感分析》在C#中使用DeepSeekAPI可以实现多种功能,例如自然语言处理、文本分类、情感分析等,本文主要为大家介绍了具体实现步骤,... 目录准备工作文本生成文本分类问答系统代码生成翻译功能文本摘要文本校对图像描述生成总结在C#中使用Deep

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Python实现高效地读写大型文件

《Python实现高效地读写大型文件》Python如何读写的是大型文件,有没有什么方法来提高效率呢,这篇文章就来和大家聊聊如何在Python中高效地读写大型文件,需要的可以了解下... 目录一、逐行读取大型文件二、分块读取大型文件三、使用 mmap 模块进行内存映射文件操作(适用于大文件)四、使用 pand