编程语言
首页 > 编程语言> > Java 读写锁实现原理

Java 读写锁实现原理

作者:互联网

Java 读写锁实现原理

#扫描上方二维码报名成都源创会#

作者:牛李

链接:

https://my.oschina.net/editorial-story/blog/1928306

本文为作者投稿文章,转载请注明上述信息

最近做的一个小项目中有这样的需求:整个项目有一份config.json保存着项目的一些配置,是存储在本地文件的一个资源,并且应用中存在读写(读>>写)更新问题。既然读写并发操作,那么就涉及到操作互斥,这里自然想到了读写锁,本文对读写锁方面的知识做个梳理。

为什么需要读写锁?

与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥,读写互斥,写写互斥,而一般的独占锁是:读读互斥,读写互斥,写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。

注意是读远远大于写,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。

一个简单的读写锁实现

根据上面理论可以利用两个int变量来简单实现一个读写锁,实现虽然烂,但是原理都是差不多的,值得阅读下。

Java 读写锁实现原理

Java 读写锁实现原理

ReadWriteLock的实现原理

在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:

公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。

可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁

可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。

ReentrantReadWriteLock的结构

ReentrantReadWriteLock的核心是由一个基于AQS的同步器Sync构成,然后由其扩展出ReadLock(共享锁),WriteLock(排它锁)所组成。

Java 读写锁实现原理

并且从ReentrantReadWriteLock的构造函数中可以发现ReadLock与WriteLock使用的是同一个Sync,具体怎么实现同一个队列既可以为共享锁,又可以表示排他锁下文会具体分析。

清单一:ReentrantReadWriteLock构造函数

Java 读写锁实现原理

Sync的实现

sync是读写锁实现的核心,sync是基于AQS实现的,在AQS中核心是state字段和双端队列,那么一个一个问题来分析。

Sync如何同时表示读锁与写锁?

清单2:读写锁状态获取

Java 读写锁实现原理

从代码中获取读写状态可以看出其是把state(int32位)字段分成高16位与低16位,其中高16位表示读锁个数,低16位表示写锁个数,如下图所示(图来自Java并发编程艺术)。

Java 读写锁实现原理

该图表示当前一个线程获取到了写锁,并且重入了两次,因此低16位是3,并且该线程又获取了读锁,并且重入了一次,所以高16位是2,当写锁被获取时如果读锁不为0那么读锁一定是获取写锁的这个线程。

读锁的获取

读锁的获取主要实现是AQS中的acquireShared方法,其调用过程如下代码。

清单3:读锁获取入口

Java 读写锁实现原理

其中doAcquireShared(arg)方法是获取失败之后AQS中入队操作,等待被唤醒后重新获取,那么关键点就是tryAcquireShared(arg)方法,方法有点长,因此先总结出获取读锁所经历的步骤,获取的第一部分步骤如下:


清单4:读锁获取的第一部分

Java 读写锁实现原理

当操作2,操作3失败时会执行fullTryAcquireShared(current),为什么会这样写呢?个人认为是一种补偿操作,操作2与操作3失败并不代表当前线程没有读锁的资格,并且这里的读锁是共享锁,有资格就应该被获取成功,因此给予补偿获取读锁的操作。在fullTryAcquireShared(current)中是一个循环获取读锁的过程,大致步骤如下:

清单5:读锁获取的第二部分

Java 读写锁实现原理

Java 读写锁实现原理

读锁的释放

清单6:读锁释放入口

Java 读写锁实现原理

读锁的释放主要是tryReleaseShared(arg)函数,因此拆解其步骤如下:

清单7:读锁的释放流程

Java 读写锁实现原理

写锁的获取


清单8:写锁的获取入口

Java 读写锁实现原理

写锁的获取也主要是tryAcquire(arg)方法,这里也拆解步骤:

操作1:如果读锁数量不为0或者写锁数量不为0,并且不是重入操作,则获取失败。

操作2:如果当前锁的数量为0,也就是不存在操作1的情况,那么该线程是有资格获取到写锁,因此修改状态,设置独占线程为当前线程

清单9:写锁的获取

Java 读写锁实现原理

写锁的释放

清单10:写锁的释放入口

Java 读写锁实现原理

写锁的释放主要是tryRelease(arg)方法,其逻辑就比较简单了,注释很详细。

清单11:写锁的释放

Java 读写锁实现原理


一些其他问题

锁降级操作哪里体现?

锁降级操作指的是一个线程获取写锁之后再获取读锁,然后读锁释放掉写锁的过程。在tryAcquireShared(arg)获取读锁的代码中有如下代码。

清单12:写锁降级策略

Java 读写锁实现原理

那么锁降级有什么用?答案是为了可见性的保证。在ReentrantReadWriteLock的javadoc中有如下代码,其是锁降级的一个应用示例。

Java 读写锁实现原理
Java 读写锁实现原理

公平与非公平的区别

清单13:公平下的Sync

Java 读写锁实现原理

公平下的Sync实现策略是所有获取的读锁或者写锁的线程都需要入队排队,按照顺序依次去尝试获取锁。

清单14:非公平下的Sync

Java 读写锁实现原理

非公平下由于抢占式获取锁,写锁是可能产生饥饿,因此解决办法就是提高写锁的优先级,换句话说获取写锁之前先占坑。

作者:牛李,一个正在努力学习的码农,主要关注后端领域、代码设计,以及一些有趣的技术。

GitHub: https://github.com/mrdear

本文系作者投稿文章。欢迎投稿。

投稿方式

标签:Java,写锁,读写,获取,读锁,线程,操作,原理
来源: https://blog.51cto.com/u_15127629/2833538