Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
沉默王二
Jmx Java
提交
1ddea68d
J
Jmx Java
项目概览
沉默王二
/
Jmx Java
9 个月 前同步成功
通知
160
Star
18
Fork
2
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
J
Jmx Java
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
1ddea68d
编写于
8月 12, 2023
作者:
沉默王二
💬
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
读写锁
上级
02ce418b
变更
4
展开全部
隐藏空白更改
内联
并排
Showing
4 changed file
with
311 addition
and
131 deletion
+311
-131
README.md
README.md
+1
-1
docs/thread/ReentrantReadWriteLock.md
docs/thread/ReentrantReadWriteLock.md
+209
-36
docs/thread/condition.md
docs/thread/condition.md
+98
-94
docs/thread/reentrantLock.md
docs/thread/reentrantLock.md
+3
-0
未找到文件。
README.md
浏览文件 @
1ddea68d
...
...
@@ -259,7 +259,7 @@
-
[
JUC 包下的那些锁
](
docs/thread/lock.md
)
-
[
重入锁ReentrantLock
](
docs/thread/reentrantLock.md
)
-
[
读写锁ReentrantReadWriteLock
](
docs/thread/ReentrantReadWriteLock.md
)
-
[
深入理解Java并发线程
协作类Condition
](
docs/thread/condition.md
)
-
[
协作类Condition
](
docs/thread/condition.md
)
-
[
深入理解Java并发线程线程阻塞唤醒类LockSupport
](
docs/thread/LockSupport.md
)
-
[
简单聊聊Java的并发集合容器
](
docs/thread/map.md
)
-
[
吊打Java并发面试官之ConcurrentHashMap
](
docs/thread/ConcurrentHashMap.md
)
...
...
docs/thread/ReentrantReadWriteLock.md
浏览文件 @
1ddea68d
---
title
:
深入理解Java并发读写锁ReentrantReadWriteLock
shortTitle
:
读写锁ReentrantReadWriteLock
description
:
深入理解Java并发读写锁ReentrantReadWriteLock
description
:
ReentrantReadWriteLock 是 Java 的一种读写锁,它允许多个读线程同时访问,但只允许一个写线程访问,或者阻塞所有的读写线程。这种锁的设计可以提高性能,特别是在数据结构中,读操作的数量远远超过写操作的情况下。
category
:
-
Java核心
tag
:
...
...
@@ -14,31 +14,78 @@ head:
# 14.16 读写锁 ReentrantReadWriteLock
在并发场景中
用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用 java 提供的关键字 synchronized 或者 concurrents 包中实现了 Lock 接口的 ReentrantLock。
它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。
在并发场景中
,为了解决线程安全问题,我们通常会使用关键字
[
synchronized
](
https://javabetter.cn/thread/synchronized-1.html
)
或者 JUC 包中实现了 Lock 接口的
[
ReentrantLock
](
https://javabetter.cn/thread/reentrantLock.html
)
。但
它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。
而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性
(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java 还提供了另外一个实现 Lock 接口的 ReentrantReadWriteLock(读写锁)
。
而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性
,而如果在这种业务场景下,依然使用独占锁的话,很显然会出现性能瓶颈。针对这种读多写少的情况,Java 提供了另外一个实现 Lock 接口的 ReentrantReadWriteLock——读写锁
。
**读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞**
。在分析 WirteLock 和 ReadLock 的互斥性时可以按照 WriteLock 与 WriteLock 之间,WriteLock 与 ReadLock 之间以及 ReadLock 与 ReadLock 之间进行分析
。
我们在
[
前面讲 Lock 接口
](
https://javabetter.cn/thread/lock.html
)
的时候,提到过读写锁,不知道大家是否还有印象
。
更多关于读写锁特性介绍大家可以看源码上的介绍(阅读源码时最好的一种学习方式,我也正在学习中,与大家共勉),这里做一个归纳总结:
**读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞**
。
1.
**公平性选择**
:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
2.
**重入性**
:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
3.
**锁降级**
:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁
在分析 WirteLock 和 ReadLock 的互斥性时,我们可以按照 WriteLock 与 WriteLock,WriteLock 与 ReadLock 以及 ReadLock 与 ReadLock 进行对比分析。
要想能够彻底的理解读写锁必须能够理解这样几个问题:
这里总结一下读写锁的特性:
1)
**公平性选择**
:支持非公平性(默认)和公平的锁获取方式,非公平的吞吐量优于公平;
在计算机科学和性能评估中,吞吐量(Throughput)是一个衡量系统处理能力的指标。它描述了单位时间内系统能够处理的事务或操作数量。吞吐量可以用来评估系统的效率和性能,例如,每秒钟完成多少次请求或操作。
非公平锁不保证等待获取锁的线程的顺序。当锁被释放时,哪个线程能够获取该锁并不遵循任何特定的顺序。这种方式通常效率较高,因为线程不需要按照队列顺序等待,从而可以减少上下文切换和调度开销,提高吞吐量。
公平锁则确保等待获取锁的线程将按照它们请求锁的顺序来获取锁。第一个请求锁的线程将是第一个获得锁的线程,以此类推。虽然公平锁的行为更容易预测,但由于需要维护一个明确的队列顺序,可能会增加额外的开销,从而降低吞吐量。
我们在讲
[
重入锁ReentrantLock
](
https://javabetter.cn/thread/reentrantLock.html
)
提到过这一点。
2)
**重入性**
:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
我们前面在讲
[
Lock
](
https://javabetter.cn/thread/lock.html
)
的时候也细致地讲过这一点。
3)
**锁降级**
:写锁降级是一种允许写锁转换为读锁的过程。通常的顺序是:
-
获取写锁:线程首先获取写锁,确保在修改数据时排它访问。
-
获取读锁:在写锁保持的同时,线程可以再次获取读锁。
-
释放写锁:线程保持读锁的同时释放写锁。
-
释放读锁:最后线程释放读锁。
这样,写锁就降级为读锁,允许其他线程进行并发读取,但仍然排除其他线程的写操作。下面的代码展示了如何使用 ReentrantReadWriteLock 来降级写锁:
```
java
ReentrantReadWriteLock
lock
=
new
ReentrantReadWriteLock
();
ReentrantReadWriteLock
.
WriteLock
writeLock
=
lock
.
writeLock
();
ReentrantReadWriteLock
.
ReadLock
readLock
=
lock
.
readLock
();
writeLock
.
lock
();
// 获取写锁
try
{
// 执行写操作
readLock
.
lock
();
// 获取读锁
}
finally
{
writeLock
.
unlock
();
// 释放写锁
}
try
{
// 执行读操作
}
finally
{
readLock
.
unlock
();
// 释放读锁
}
```
写锁降级为读锁的过程有助于保持数据的一致性,而不影响并发读取的性能。通过这种方式,线程可以继续保持对数据的独占访问权限,直到它准备允许其他线程共享读取访问。这样可以确保在写操作和随后的读操作之间的数据一致性,并且允许其他读取线程并发访问。
要想彻底理解读写锁必须能够理解这几个问题:
-
1. 读写锁是怎样实现分别记录读写状态的?
-
2. 写锁是怎样获取和释放的?
-
3.读锁是怎样获取和释放的?
-
3.
读锁是怎样获取和释放的?
我们带着这样的三个问题,再去了解下读写锁。
## 写锁详解
##
#
写锁详解
### 写锁的获取
###
#
写锁的获取
同步组件的实现聚合了同步器(AQS),并通过重写重写同步器(AQS)中的方法实现同步组件的同步语义。因此,写锁的实现依然也是采用这种方式。在同一时刻写锁是不能被多个线程所获取,很显然写锁是独占式锁,而实现写锁的同步语义是通过重写 AQS 中的 tryAcquire 方法实现的。源码为:
同步组件的实现聚合了同步器(
[
AQS
](
https://javabetter.cn/thread/aqs.html
)
),并通过重写同步器(AQS)中的方法实现同步组件的同步语义。
因此,写锁的实现依然也是采用这种方式。在同一时刻写锁是不能被多个线程获取的,很显然写锁是独占式锁,而实现写锁的同步语义是通过重写 AQS 中的 tryAcquire 方法实现的。源码为:
```
java
protected
final
boolean
tryAcquire
(
int
acquires
)
{
...
...
@@ -80,22 +127,43 @@ protected final boolean tryAcquire(int acquires) {
}
```
这段代码的逻辑请看注释,这里有一个地方需要重点关注,exclusiveCount(c)方法,该方法源码为:
`static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }`
这段代码的逻辑请看注释,这里有一个地方需要重点关注,
`exclusiveCount(c)`
方法,该方法源码为:
```
java
static
int
exclusiveCount
(
int
c
)
{
return
c
&
EXCLUSIVE_MASK
;
}
```
其中
**EXCLUSIVE_MASK**
为:
```
java
static
final
int
EXCLUSIVE_MASK
=
(
1
<<
SHARED_SHIFT
)
-
1
;
```
EXCLUSIVE_MASK 为 1 左移 16 位然后减 1,即为 0x0000FFFF。而 exclusiveCount 方法是将同步状态(state 为 int 类型)与 0x0000FFFF 相与,即取同步状态的低 16 位。
其中
**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 位用来表示写锁的获取次数**
。同时还有一个方法值得我们注意:
那么低 16 位代表什么呢?根据 exclusiveCount 方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论
**同步状态的低 16 位用来表示写锁的获取次数**
。
`static int sharedCount(int c) { return c >>> SHARED_SHIFT; }`
同时还有一个方法值得我们注意:
该方法是获取读锁被获取的次数,是将同步状态(int c)右移 16 次,即取同步状态的高 16 位,现在我们可以得出另外一个结论
**同步状态的高 16 位用来表示读锁被获取的次数**
。现在还记得我们开篇说的需要弄懂的第一个问题吗?读写锁是怎样实现分别记录读锁和写锁的状态的,现在这个问题的答案就已经被我们弄清楚了,其示意图如下图所示:
```
java
static
int
sharedCount
(
int
c
)
{
return
c
>>>
SHARED_SHIFT
;
}
```
该方法是获取读锁被获取的次数,是将同步状态(int c)右移 16 次,即取同步状态的高 16 位,现在我们可以得出另外一个结论
**同步状态的高 16 位用来表示读锁被获取的次数**
。
还记得这个问题“读写锁是怎样实现分别记录读写状态的”吗?其示意图如下图所示:
![
读写锁的读写状态设计
](
https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/ReentrantReadWriteLock-f714bdd6-917a-4d25-ac11-7e85b0ec1b14.png
)
现在我们回过头来看写锁获取方法 tryAcquire,其主要逻辑为:
**当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。**
好,
现在我们回过头来看写锁获取方法 tryAcquire,其主要逻辑为:
**当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。**
### 写锁的释放
###
#
写锁的释放
写锁释放通过重写
AQS
的 tryRelease 方法,源码为:
写锁释放通过重写
[
AQS
](
https://javabetter.cn/thread/aqs.html
)
的 tryRelease 方法,源码为:
```
java
protected
final
boolean
tryRelease
(
int
releases
)
{
...
...
@@ -113,13 +181,13 @@ protected final boolean tryRelease(int releases) {
}
```
源码的实现逻辑请看注释,不难理解
与 ReentrantLock 基本一致,这里需要注意的是,减少写状态
` int nextc = getState() - releases;`
只需要用
**当前同步状态直接减去写状态的
原因正是我们刚才所说的写状态是由同步状态的低 16 位表示的**
。
源码的实现逻辑请看注释,不难理解
,与 ReentrantLock 基本一致,这里需要注意的是,减少写状态
`int nextc = getState() - releases;`
只需要用
**当前同步状态直接减去写状态,
原因正是我们刚才所说的写状态是由同步状态的低 16 位表示的**
。
## 读锁详解
##
#
读锁详解
### 读锁的获取
###
#
读锁的获取
看完了写锁,
现在来看看读锁,读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。按照之前对 AQS
介绍,实现共享式同步组件的同步语义需要通过重写 AQS 的 tryAcquireShared 方法和 tryReleaseShared 方法。读锁的获取实现方法为:
看完了写锁,
再来看看读锁,读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取,也就是一种共享式锁。按照之前对
[
AQS
](
https://javabetter.cn/thread/aqs.html
)
的
介绍,实现共享式同步组件的同步语义需要通过重写 AQS 的 tryAcquireShared 方法和 tryReleaseShared 方法。读锁的获取实现方法为:
```
java
protected
final
int
tryAcquireShared
(
int
unused
)
{
...
...
@@ -172,11 +240,13 @@ protected final int tryAcquireShared(int unused) {
}
```
代码的逻辑请看注释,需要注意的是
**当写锁被其他线程获取后,读锁获取失败**
,否则获取成功利用 CAS 更新同步状态。
代码的逻辑请看注释,需要注意的是
**当写锁被其他线程获取后,读锁获取失败**
,否则获取成功
,会
利用 CAS 更新同步状态。
另外,当前同步状态需要加上 SHARED_UNIT(
`(1 << SHARED_SHIFT)`
即 0x00010000)的原因这是我们在上面所说的同步状态的高 16 位用来表示读锁被获取的次数。如果 CAS 失败或者已经获取读锁的线程再次获取读锁时,是靠 fullTryAcquireShared 方法实现的,这段代码就不展开说了,有兴趣可以看看
。
另外,当前同步状态需要加上 SHARED_UNIT(
`(1 << SHARED_SHIFT)`
,即 0x00010000)的原因,我们在上面也说过了,同步状态的高 16 位用来表示读锁被获取的次数
。
### 读锁的释放
如果 CAS 失败或者已经获取读锁的线程再次获取读锁时,是靠 fullTryAcquireShared 方法实现的,这段代码就不展开说了,有兴趣可以看看。
#### 读锁的释放
读锁释放的实现主要通过方法 tryReleaseShared,源码如下,主要逻辑请看注释:
...
...
@@ -215,9 +285,9 @@ protected final boolean tryReleaseShared(int unused) {
}
```
## 锁降级
##
#
锁降级
读写锁支持锁降级,
**遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁**
,不支持锁升级,关于锁降级
下面的示例代码摘自 ReentrantWriteReadLock 源码中
:
读写锁支持锁降级,
**遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁**
,不支持锁升级,关于锁降级
,下面的示例代码摘自 ReentrantWriteReadLock 源码
:
```
java
void
processCachedData
()
{
...
...
@@ -248,13 +318,116 @@ void processCachedData() {
}
```
---
这里的流程可以解释如下:
-
获取读锁:首先尝试获取读锁来检查某个缓存是否有效。
-
检查缓存:如果缓存无效,则需要释放读锁,因为在获取写锁之前必须释放读锁。
-
获取写锁:获取写锁以便更新缓存。此时,可能还需要重新检查缓存状态,因为在释放读锁和获取写锁之间可能有其他线程修改了状态。
-
更新缓存:如果确认缓存无效,更新缓存并将其标记为有效。
-
写锁降级为读锁:在释放写锁之前,获取读锁,从而实现写锁到读锁的降级。这样,在释放写锁后,其他线程可以并发读取,但不能写入。
-
使用数据:现在可以安全地使用缓存数据了。
-
释放读锁:完成操作后释放读锁。
这个流程结合了读锁和写锁的优点,确保了数据的一致性和可用性,同时允许在可能的情况下进行并发读取。使用读写锁的代码可能看起来比使用简单的互斥锁更复杂,但它提供了更精细的并发控制,可能会提高多线程应用程序的性能。
### ReentrantReadWriteLock的使用
ReentrantReadWriteLock 的使用非常简单,下面的代码展示了如何使用 ReentrantReadWriteLock 来实现一个线程安全的计数器:
```
java
public
class
Counter
{
private
final
ReentrantReadWriteLock
rwl
=
new
ReentrantReadWriteLock
();
private
final
Lock
r
=
rwl
.
readLock
();
private
final
Lock
w
=
rwl
.
writeLock
();
private
int
count
=
0
;
public
int
getCount
()
{
r
.
lock
();
try
{
return
count
;
}
finally
{
r
.
unlock
();
}
}
public
void
inc
()
{
w
.
lock
();
try
{
count
++;
}
finally
{
w
.
unlock
();
}
}
}
```
我们再来模拟一个稍微复杂一点的例子,如何使用读写锁来实现安全地读取和更新共享数据。
```
java
public
class
CachedData
{
private
final
ReentrantReadWriteLock
rwl
=
new
ReentrantReadWriteLock
();
private
Object
data
;
private
boolean
cacheValid
;
public
void
processCachedData
()
{
// Acquire read lock
rwl
.
readLock
().
lock
();
if
(!
cacheValid
)
{
// Must release read lock before acquiring write lock
rwl
.
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
=
fetchDataFromDatabase
();
cacheValid
=
true
;
}
// Downgrade by acquiring read lock before releasing write lock
rwl
.
readLock
().
lock
();
}
finally
{
rwl
.
writeLock
().
unlock
();
// Unlock write, still hold read
}
}
try
{
use
(
data
);
}
finally
{
rwl
.
readLock
().
unlock
();
}
}
private
Object
fetchDataFromDatabase
()
{
// Simulate fetching data from a database
return
new
Object
();
}
private
void
use
(
Object
data
)
{
// Simulate using the data
System
.
out
.
println
(
"使用数据: "
+
data
);
}
public
static
void
main
(
String
[]
args
)
{
CachedData
cachedData
=
new
CachedData
();
cachedData
.
processCachedData
();
}
}
```
当缓存无效时,会先释放读锁,然后获取写锁来更新缓存。一旦缓存被更新,就会进行写锁到读锁的降级,允许其他线程并发读取,但仍然排除写入。
这样的结构允许在确保数据一致性的同时,实现并发读取的优势,从而提高多线程环境下的性能。
### 总结
ReentrantReadWriteLock 是 Java 的一种读写锁,它允许多个读线程同时访问,但只允许一个写线程访问,或者阻塞所有的读写线程。这种锁的设计可以提高性能,特别是在数据结构中,读操作的数量远远超过写操作的情况下。
读写锁的实现主要是通过重写
[
AQS
](
https://javabetter.cn/thread/aqs.html
)
的 tryAcquire 方法和 tryRelease 方法实现的,读锁和写锁的获取和释放都是通过这两个方法实现的。
读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。
> 编辑:沉默王二,内容大部分来源以下三个开源仓库:
>
> - [深入浅出 Java 多线程](http://concurrent.redspider.group/)
> - [并发编程知识总结](https://github.com/CL0610/Java-concurrency)
> - [Java 八股文](https://github.com/CoderLeixiaoshuai/java-eight-part)
>编辑:沉默王二,编辑前的内容主要来自于 CL0610的 GitHub 仓库[https://github.com/CL0610/Java-concurrency](https://github.com/CL0610/Java-concurrency/blob/master/11.深入理解读写锁ReentrantReadWriteLock/深入理解读写锁ReentrantReadWriteLock.md)
---
...
...
docs/thread/condition.md
浏览文件 @
1ddea68d
此差异已折叠。
点击以展开。
docs/thread/reentrantLock.md
浏览文件 @
1ddea68d
...
...
@@ -242,6 +242,9 @@ ReentrantLock 与 synchronized 关键字都是用来实现同步的,那么它
-
ReentrantLock 必须手动释放锁。通常需要在 finally 块中调用 unlock 方法以确保锁被正确释放。synchronized: 自动释放锁。当同步块执行完毕时,JVM 会自动释放锁,不需要手动操作。
-
ReentrantLock: 通常提供更好的性能,特别是在高竞争环境下。ynchronized: 在某些情况下,性能可能稍差一些,但在现代 JVM 实现中,性能差距通常不大。
>编辑:沉默王二,编辑前的内容主要来自于CL0610的 GitHub 仓库[https://github.com/CL0610/Java-concurrency](https://github.com/CL0610/Java-concurrency/blob/master/10.彻底理解ReentrantLock/彻底理解ReentrantLock.md)
---
GitHub 上标星 8700+ 的开源知识库《
[
二哥的 Java 进阶之路
](
https://github.com/itwanger/toBeBetterJavaer
)
》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:
[
太赞了,GitHub 上标星 8700+ 的 Java 教程
](
https://javabetter.cn/overview/
)
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录