读写锁介绍
现实中有这样一种场景:
对共享资源有读和写的操作,且写操作没有读操作那么频繁(读
多写少)
。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个
线程同时读取共享资源(
读读可以并发
);但是如果一个线程想去写这些共享资源,就不应该
允许其他线程对该资源进行读和写操作了(
读写,写读,写写互斥
)。
在读多于写的情况下,
读写锁能够提供比排它锁更好的并发性和吞吐量。
针对这种场景,
JAVA的并发包提供了读写锁ReentrantReadWriteLock,
它内部,维护了
一对相关的锁
,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁
,描述如下:
线程进入读锁的前提条件:
没有其他线程的写锁
没有写请求或者
有写请求,但调用线程和持有锁的线程是同一个
。
线程进入写锁的前提条件:
没有其他线程的读锁
没有其他线程的写锁
而读写锁有以下三个重要的特性:
公平选择性
:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公
平。
可重入
:读锁和写锁都支持线程重入。以读写线程为例:
读线程获取读锁后,能够
再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
锁降级
:
遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读
锁
。
ReentrantReadWriteLock的使用
读写锁接口ReadWriteLock
一对方法,分别获得读锁和写锁 Lock 对象。

ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类
。在它内部,维护了一对相关的锁,
一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线
程同时持有。也就是说,
写锁是独占的,读锁是共享的。

如何使用读写锁
1
private
ReadWriteLock readWriteLock
=
new
ReentrantReadWriteLock
();
2
private
Lock r
=
readWriteLock
.
readLock
();
3
private
Lock w
=
readWriteLock
.
writeLock
();
4
5
//
读操作上读锁
6
public
Data
get
(
String key
) {
7
r
.
lock
();
8
try
{
9
// TODO
业务逻辑
10
}
finally
{
11
r
.
unlock
();
12
}
13
}
14
15
//
写操作上写锁
16
public
Data
put
(
String key
,
Data value
) {
17
w
.
lock
();
18
try
{
19
// TODO
业务逻辑
20
}
finally
{
21
w
.
unlock
();
22
}
23
}
注意事项
读锁不支持条件变量
重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
重入时支持降级: 持有写锁的情况下可以去获取读锁
应用场景
ReentrantReadWriteLock适合
读多写少的场景
示例Demo
1
public class
Cache
{
2
static
Map
<
String
,
Object
>
map
=
new
HashMap
<
String
,
Object
>
();
3
static
ReentrantReadWriteLock rwl
=
new
ReentrantReadWriteLock
();
4
static
Lock r
=
rwl
.
readLock
();
5
static
Lock w
=
rwl
.
writeLock
();
6
7
//
获取一个
key
对应的
value
8
public static
final Object
get
(
String key
) {
9
r
.
lock
();
10
try
{
11
return
map
.
get
(
key
);
12
}
finally
{
13
r
.
unlock
();
14
}
15
}
16
17
//
设置
key
对应的
value
,并返回旧的
value
18
public static
final Object
put
(
String key
,
Object value
) {
19
w
.
lock
();
20
try
{
21
return
map
.
put
(
key
,
value
);
22
}
finally
{
23
w
.
unlock
();
24
}
25
}
26
27
//
清空所有的内容
28
public static
final
void
clear
() {
29
w
.
lock
();
30
try
{
31
map
.
clear
();
32
}
finally
{
33
w
.
unlock
();
34
}
35
}
36
}
上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的
读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这
使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,
在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被
阻塞,而 只有写锁被释放之后,其他读写操作才能继续。
Cache使用读写锁提升读操作的并发
性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式
锁降级
锁降级指的是写锁降级成为读锁。
如果当前线程拥有写锁,然后将其释放,最后再获取读
锁,这种分段完成的过程不能称之为锁降级。
锁降级是指把持住(当前拥有的)写锁,再获取
到读锁,随后释放(先前拥有的)写锁的过程。
锁降级可以帮助我们拿到当前线程修改后的结
果而不被其他线程所破坏,防止更新丢失。
锁降级的使用示例
因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感
知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的
准备工作。
1
private
final ReentrantReadWriteLock rwl
=
new
ReentrantReadWriteLock
();
2
private
final Lock r
=
rwl
.
readLock
();
3
private
final Lock w
=
rwl
.
writeLock
();
4
private
volatile boolean update
=
false
;
5
6
public void
processData
() {
7
readLock
.
lock
();
8
if
(
!
update
) {
9
//
必须先释放读锁
10
readLock
.
unlock
();
11
//
锁降级从写锁获取到开始
12
writeLock
.
lock
();
13
try
{
14
if
(
!
update
) {
15
// TODO
准备数据的流程(略)
16
update
=
true
;
17
}
18
readLock
.
lock
();
19
}
finally
{
20
writeLock
.
unlock
();
21
}
22
//
锁降级完成,写锁降级为读锁
23
}
24
try
{
25
//TODO
使用数据的流程(略)
26
}
finally
{
27
readLock
.
unlock
();
28
}
29
}
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性
,如果当
前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了
数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步
骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数
据更新。
RentrantReadWriteLock不支持锁升级
(把持读锁、获取写锁,最后释放读锁的过程)。
目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新
了数据,则其更新对其他获取到读锁的线程是不可见的。