Java并发之-读写锁ReentrantReadWriteLock

深碍√TFBOYSˉ_ 2022-03-08 06:26 398阅读 0赞

前言

之前提到的ReentrantLock是排他锁,这种锁同一时刻只允许一个线程访问,而读写锁同一时刻可以多个线程访问,但在写线程访问时,所有读线程和其他写线程都要被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读写锁,使得并发性相比一般的排他锁有很大提升。

参考文献

《Java并发编程的艺术》

正文

读写锁只需要在读操作时获取读锁,写操作获取写锁即可。当写操作被获取时,后续读写锁都会被阻塞,写操作释放以后,所有操作继续执行。

一般情况下,读写锁的性能比排他锁要好,因为大多数场景读是多于写的,所以在读多余写时,读写锁能够提供比排他锁更好的性能和吞吐量。java中读写锁实现是 ReentrantReadWriteLock。

ReentrantReadWriteLock的特性


















特性 说明
公平性 支持非公平性(默认)和公平性获取方式,吞吐量还是非公平性优于公平性。
重入性 支持重入。读线程获取读后,能够再次获取读锁。而写线程获取写锁后能再次获取写锁,同时可以获取读锁。
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁降级为读锁。

读写锁的接口

ReadWriteLock定义了获取读锁和写锁的两个方法分别是,readLock() 方法和 writeLock() 方法,而其实现

是 ReentrantReadWriteLock ,其内部还有一些工作状态的方法,如下。

ReentrantReadWriteLock内部工作状态方法






















方法 说明

int getReadLockCount()

返回当前读锁被获取读次数,同一个线程获取了n次,该方法返回n。
int getReadHoldCount() 返回当前线程获取读锁读次数
boolean isWriteLocked() 判断写锁是否被获取。
int getWriteHoldCount() 返回当前线程获取写锁的次数。

可以观察规律凡是方法名带有 hold 的方法都是针对当前线程而言的,反之针对当前锁而言。

读写锁一般使用

  1. static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  2. static Lock r = rwl.readLock();
  3. static Lock w = rwl.writeLock();
  4. public static final Object get(String key) {
  5. r.lock();
  6. try {
  7. return null;
  8. } finally {
  9. r.unlock();
  10. }
  11. }
  12. public static final Object clean() {
  13. w.lock();
  14. try {
  15. return null;
  16. } finally {
  17. w.unlock();
  18. }
  19. }

读写锁依赖自定义的同步器来实现的其功能,读写状态就是同步器的同步状态。

与ReentrantLock不同在于,读写锁自定义同步器需要在同步状态上维护多个读线程和一个写线程的状态,所以状态的设计成为了关键。

我们要在一个整型变量维护多种状态就要按位切割使用这个整型的变量,读写锁将整形变量切分成两部分,高16位表示读,低16位表示写,如图。

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODAwMzM4OQ_size_16_color_FFFFFF_t_70

如图所示,当前同步状态表示一个线程已经获取了锁,且重入了两次,同时也连续获取了两次读锁。读写锁是通过位运算来快速定位读和写各自状态的。假设当前同步状态的值为S,写状态就为 S & 0x0000FFFF(将高16为全部抹去),读状态为 S >>> 16(无符号位补0右移16位)。当写状态增加 1 状态变为 S+1,当读状态增加1时,状态编码就是 S+(1 << 16)也就是 S+0x00010000。

结论:S不等于0时,当写状态(S&0x0000FFFF) 等于0时,则读状态 (S>>> 16) 大于0 ,即读锁已经获取。

#

写锁的获取与释放

写锁支持重入的排他锁。当一个线程获取了写锁,则增加状态只。如果当前线程在获取写锁时,读写已经被获取,或者该线程不是获取写锁的线程,则当前线程进入阻塞。

  1. protected final boolean tryAcquire(int acquires) {
  2. Thread current = Thread.currentThread();
  3. int c = getState();
  4. int w = exclusiveCount(c);
  5. if (c != 0) {
  6. // 存在读锁或者当前获取线程不是已经获取写锁的线程
  7. if (w == 0 || current != getExclusiveOwnerThread())
  8. return false;
  9. if (w + exclusiveCount(acquires) > MAX_COUNT)
  10. throw new Error("Maximum lock count exceeded");
  11. // Reentrant acquire
  12. setState(c + acquires);
  13. return true;
  14. }
  15. if (writerShouldBlock() ||
  16. !compareAndSetState(c, c + acquires))
  17. return false;
  18. setExclusiveOwnerThread(current);
  19. return true;
  20. }

代码解释:可能有人早已疑问我代码里注释的那句话是什么意思了,为什么要判断以下读锁是否存在,存在读锁的话,写锁就不能获取。原因在于:读写锁要确保写锁的操作对读锁的可见性,如果允许读锁已经被获取的情况下还要获取写锁,那么正在运行的其他读线程就无法感知当前写线程的操作。所以说获取写锁前一定要看是否还有读锁已经被获取。而写锁一旦被获取,其他读写线程都要被阻塞了。说白了,我们要保证在写之前不能有线程还在读,这样数据不准确。

读锁读获取与释放

读相对于写限制就少了写,读锁支持重入和共享,也就是同时可以被多个线程访问,在没有其他写线程访问时,所有读操作总是不会阻塞的,就是都能获取到。但是如果当前线程要获取读锁时,发现写锁已经被获取,那么读锁要进入等待状态。

如果其他线程已经获取了写锁,则当前线程回去读锁失败,进入等待。

如果当前线程获取了写锁或者写锁为被获取,则当前线程增加读状态,读锁获取成功。

锁降级

锁降级指的是写锁降级为读锁,是指把持主写锁,再次获取到读锁,随后释放写锁到过程。

锁降级是有必要到,是保证数据到可见性,如果当前线程不获取读锁而是直接释放写锁,假设此时另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T修改了数据。现在有遵循锁降级步骤,当前线程获取读锁后,就不应该有写锁再被获取,要等读锁释放后,写锁才可以被获取。另外ReentrantReadWriteLock不支持锁升级。

感兴趣的小伙伴,可以加为微信,进入java聊天群

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODAwMzM4OQ_size_16_color_FFFFFF_t_70 1

发表评论

表情:
评论列表 (有 0 条评论,398人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ReentrantReadWriteLock

    1. 写锁比读锁的优先级要高,拥有写锁之后还可以再获取读锁,但是拥有读锁的线程在释放前无法再获取写锁。 2. 允许锁降级,即从写锁降级为读锁,实现的步骤是:先获取写锁,再获