并发编程 ReadWriteLock

本文将介绍并发编程中的读写锁 ReadWriteLock。

一、适用场景

读多写少场景。

二、什么是读写锁?

读写锁提供了写锁和读锁,遵循以下三条基本原则:

  • 同一时刻,允许多个线程读
  • 同一时刻,只允许一个线程写
  • 若有线程正在写,则禁止其它线程读

读写锁与互斥锁的一个重要区别是读写锁允许多个线程同时读,这是读写锁在读多写少场景下性能优于互斥锁的关键。

三、缓存的简单实现

  • 声明一个 Cache<K, V> 类,其中 K 代表 key 的类型,V 代表 value 的类型
  • 缓存数据存储于 HashMap 中,由于 HashMap 本身不是线程安全的,额外假如 ReadWriteLock 机制保证其安全
  • ReadWriteLock 是一个接口,有实现类 ReentrantReadWriteLock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Cache<K,V> {

final Map<K, V> m = new HashMap<>();

final ReadWriteLock rwl = new ReentrantReadWriteLock();

// 读锁
final Lock r = rwl.readLock();

// 写锁
final Lock w = rwl.writeLock();


V get(K key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}

V put(K key, V value) {
w.lock();
try {
return m.put(key, v);
} finally {
w.unlock();
}
}

}

四、按需加载缓存

1. 示例

在实际项目中,我们可能会使用这样一种按需加载缓存:向缓存中获取值时,缓存应该首先判断是否有此值,如果有直接返回,如果没有则加载后返回。

下面是利用 ReadWriteLock 实现的简单按需加载缓存:

  • 首先获取读锁,读取缓存中对应的数据,释放读锁
  • 当值存在时,直接返回
  • 当值不存在时,获取写锁,再次验证值是否存在,加载值,放入缓存,释放写锁,返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Cache<K,V> {

final Map<K, V> m = new HashMap<>();

final ReadWriteLock rwl = new ReentrantReadWriteLock();

// 读锁
final Lock r = rwl.readLock();

// 写锁
final Lock w = rwl.writeLock();

V get(K key) {
V v = null;
// 读缓存
r.lock();
try {
v = m.get(key);
} finally {
r.unlock();
}
// 若缓存中存在,返回
if (v != null) {
return v;
}
// 若缓存中不存在,加载
w.lock();
try {
// 再次验证
v = m.get(key);
if (v == null) {
v = 查询数据库;
m.put(key, v);
}
} finally {
w.unlock();
}
return v;
}

}

2. 为什么获取写锁后再次验证值是否存在?

可能存在这样一种情况,多个线程同时访问 get() 方法,均发现值不存在,其中一个线程率先获取读锁,填充值,后续线程再获取到读锁时值实质上已经存在,无需重复填充,

3. 为什么先获取写锁再加载数据?

通过这种方式,可以保证同一时刻只有一个线程在加载数据,避免 “被加载方” 的压力。

五、锁的升级与降级

1. 锁的升级

1
2
3
4
5
6
7
8
9
10
11
12
r.lock();
try {
// do something
w.lock();
try {
// do something
} finally {
w.unlock();
}
} finally {
r.unlock();
}

锁的升级:先获取读锁,然后在读锁未释放时获取写锁。

ReadWriteLock 并不支持锁的升级,对于 ReadWriteLock 来说,获取写锁的前提是所有读锁均已释放(即使读锁是线程自身持有)。

在上述示例中,读锁未释放时获取写锁,将导致写锁永久等待。

2. 锁的降级

1
2
3
4
5
6
7
8
9
10
11
12
w.lock();
try {
// do something
r.lock();
try {
// do something
} finally {
r.unlock();
}
} finally {
w.unlock();
}

ReadWriteLock 允许锁的降级,即先获取写锁,然后在写锁未释放时获取读锁。

参考

  • Java 并发编程实战