ReentrantReadWriteLock读写锁

 

读写锁介绍
现实中有这样一种场景: 对共享资源有读和写的操作,且写操作没有读操作那么频繁(读
多写少) 。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个
线程同时读取共享资源( 读读可以并发 );但是如果一个线程想去写这些共享资源,就不应该
允许其他线程对该资源进行读和写操作了( 读写,写读,写写互斥 )。 在读多于写的情况下,
读写锁能够提供比排它锁更好的并发性和吞吐量。
针对这种场景, 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不支持锁升级 (把持读锁、获取写锁,最后释放读锁的过程)。
目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新
了数据,则其更新对其他获取到读锁的线程是不可见的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值