一、ReentrantReadWriteLock与ReentrantLock

说到ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系。

ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念。前面的章节中一直在强调这个特点。显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生。但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了。所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能否改造为读写锁。

ReadWriteLock 描述的是:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作(修改数据)。清单0描述了ReadWriteLock的API。

清单0 ReadWriteLock 接口

  1. public interface ReadWriteLock {

  2. Lock readLock();

  3. Lock writeLock();

  4. }

清单0描述的ReadWriteLock结构,这里需要说明的是ReadWriteLock并不是Lock的子接口,只不过ReadWriteLock借助Lock来实现读写两个视角。在ReadWriteLock中每次读取共享数据就需要读取锁,当需要修改共享数据时就需要写入锁。看起来好像是两个锁,但其实不尽然,下文会指出。

二、ReentrantReadWriteLock的特性

ReentrantReadWriteLock有以下几个特性:

  • 公平性

    • 非公平锁(默认) 这个和独占锁的非公平性一样,由于读线程之间没有锁竞争,所以读操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。因此非公平锁的吞吐量要高于公平锁。
    • 公平锁 利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前前提是写线程的等待时间要比所有读线程的等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁。
  • 重入性
    • 读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。
    • 写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
    • 另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。
  • 锁降级
    • 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
  • 锁升级
    • 读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。
  • 锁获取中断
    • 读取锁和写入锁都支持获取锁期间被中断。这个和独占锁一致。
  • 条件变量
    • 写入锁提供了条件变量(Condition)的支持,这个和独占锁一致,但是读取锁却不允许获取条件变量,将得到一个UnsupportedOperationException异常。
  • 重入数
    • 读取锁和写入锁的数量最大分别只能是65535(包括重入数)。

三、ReentrantReadWriteLock的内部实现

3.1 读写锁是独占锁的两个不同视图

ReentrantReadWriteLock里面的锁主体就是一个Sync,也就是上面提到的FairSync或者NonfairSync,所以说实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样,所以前面才有读写锁是独占锁的两个不同视图一说。

ReentrantReadWriteLock里面有两个类:ReadLock/WriteLock,这两个类都是Lock的实现。

清单1 ReadLock 片段

  1. public static class ReadLock implements Lock, java.io.Serializable {

  2. private final Sync sync;

  3. protected ReadLock(ReentrantReadWriteLock lock) {

  4. sync = lock.sync;

  5. }

  6. public void lock() {

  7. sync.acquireShared(1);

  8. }

  9. public void lockInterruptibly() throws InterruptedException {

  10. sync.acquireSharedInterruptibly(1);

  11. }

  12. public boolean tryLock() {

  13. return sync.tryReadLock();

  14. }

  15. public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {

  16. return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));

  17. }

  18. public void unlock() {

  19. sync.releaseShared(1);

  20. }

  21. public Condition newCondition() {

  22. throw new UnsupportedOperationException();

  23. }

  24. }

清单2 WriteLock 片段

  1. public static class WriteLock implements Lock, java.io.Serializable {

  2. private final Sync sync;

  3. protected WriteLock(ReentrantReadWriteLock lock) {

  4. sync = lock.sync;

  5. }

  6. public void lock() {

  7. sync.acquire(1);

  8. }

  9. public void lockInterruptibly() throws InterruptedException {

  10. sync.acquireInterruptibly(1);

  11. }

  12. public boolean tryLock( ) {

  13. return sync.tryWriteLock();

  14. }

  15. public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {

  16. return sync.tryAcquireNanos(1, unit.toNanos(timeout));

  17. }

  18. public void unlock() {

  19. sync.release(1);

  20. }

  21. public Condition newCondition() {

  22. return sync.newCondition();

  23. }

  24. public boolean isHeldByCurrentThread() {

  25. return sync.isHeldExclusively();

  26. }

  27. public int getHoldCount() {

  28. return sync.getWriteHoldCount();

  29. }

  30. }

清单1描述的是读锁的实现,清单2描述的是写锁的实现。显然WriteLock就是一个独占锁,这和ReentrantLock里面的实现几乎相同,都是使用了AQS的acquire/release操作。当然了在内部处理方式上与ReentrantLock还是有一点不同的。对比清单1和清单2可以看到,ReadLock获取的是共享锁,WriteLock获取的是独占锁。

3.2 ReentrantReadWriteLock中的state

在AQS章节中介绍到AQS中有一个state字段(int类型,32位)用来描述有多少线程获持有锁。在独占锁的时代这个值通常是0或者1(如果是重入的就是重入的次数),在共享锁的时代就是持有锁的数量。在上一节中谈到,ReadWriteLock的读、写锁是相关但是又不一致的,所以需要两个数来描述读锁(共享锁)和写锁(独占锁)的数量。显然现在一个state就不够用了。于是在ReentrantReadWrilteLock里面将这个字段一分为二,高位16位表示共享锁的数量,低位16位表示独占锁的数量(或者重入数量)。2^16-1=65536,这就是上节中提到的为什么共享锁和独占锁的数量最大只能是65535的原因了。

3.3 读写锁的获取和释放

有了上面的知识后再来分析读写锁的获取和释放就容易多了。

清单3 写入锁获取片段

  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. if (w == 0 || current != getExclusiveOwnerThread())

  7. return false;

  8. if (w + exclusiveCount(acquires) > MAX_COUNT)

  9. throw new Error("Maximum lock count exceeded");

  10. }

  11. if ((w == 0 && writerShouldBlock(current)) ||

  12. !compareAndSetState(c, c + acquires))

  13. return false;

  14. setExclusiveOwnerThread(current);

  15. return true;

  16. }

清单3 是写入锁获取的逻辑片段,整个工作流程是这样的:

  1. 持有锁线程数非0(c=getState()不为0),如果写线程数(w)为0(那么读线程数就不为0)或者独占锁线程(持有锁的线程)不是当前线程就返回失败,或者写入锁的数量(其实是重入数)大于65535就抛出一个Error异常。否则进行2。
  2. 如果当且写线程数位0(那么读线程也应该为0,因为步骤1已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果增加写线程数失败也返回失败。否则进行3。
  3. 设置独占线程(写线程)为当前线程,返回true。

清单3 中 exclusiveCount(c)就是获取写线程数(包括重入数),也就是state的低16位值。另外这里有一段逻辑是当前写线程是否需要阻塞writerShouldBlock(current)。清单4 和清单5 就是公平锁和非公平锁中是否需要阻塞的片段。很显然对于非公平锁而言总是不阻塞当前线程,而对于公平锁而言如果AQS队列不为空或者当前线程不是在AQS的队列头那么就阻塞线程,直到队列前面的线程处理完锁逻辑。

清单4 公平读写锁写线程是否阻塞

  1. final boolean writerShouldBlock(Thread current) {

  2. return !isFirst(current);

  3. }

清单5 非公平读写锁写线程是否阻塞

  1. final boolean writerShouldBlock(Thread current) {

  2. return false;

  3. }

写入锁的获取逻辑清楚后,释放锁就比较简单了。清单6 描述的写入锁释放逻辑片段,其实就是检测下剩下的写入锁数量,如果是0就将独占锁线程清空(意味着没有线程获取锁),否则就是说当前是重入锁的一次释放,所以不能将独占锁线程清空。然后将剩余线程状态数写回AQS。

清单6 写入锁释放逻辑片段

  1. protected final boolean tryRelease(int releases) {

  2. int nextc = getState() - releases;

  3. if (Thread.currentThread() != getExclusiveOwnerThread())

  4. throw new IllegalMonitorStateException();

  5. if (exclusiveCount(nextc) == 0) {

  6. setExclusiveOwnerThread(null);

  7. setState(nextc);

  8. return true;

  9. } else {

  10. setState(nextc);

  11. return false;

  12. }

  13. }

清单3~6 描述的写入锁的获取释放过程。读取锁的获取和释放过程要稍微复杂些。 清单7描述的是读取锁的获取过程。

清单7 读取锁获取过程片段

  1. protected final int tryAcquireShared(int unused) {

  2. Thread current = Thread.currentThread();

  3. int c = getState();

  4. if (exclusiveCount(c) != 0 &&

  5. getExclusiveOwnerThread() != current)

  6. return -1;

  7. if (sharedCount(c) == MAX_COUNT)

  8. throw new Error("Maximum lock count exceeded");

  9. if (!readerShouldBlock(current) &&

  10. compareAndSetState(c, c + SHARED_UNIT)) {

  11. HoldCounter rh = cachedHoldCounter;

  12. if (rh == null || rh.tid != current.getId())

  13. cachedHoldCounter = rh = readHolds.get();

  14. rh.count++;

  15. return 1;

  16. }

  17. return fullTryAcquireShared(current);

  18. }

  19. final int fullTryAcquireShared(Thread current) {

  20. HoldCounter rh = cachedHoldCounter;

  21. if (rh == null || rh.tid != current.getId())

  22. rh = readHolds.get();

  23. for (;;) {

  24. int c = getState();

  25. int w = exclusiveCount(c);

  26. if ((w != 0 && getExclusiveOwnerThread() != current) ||

  27. ((rh.count | w) == 0 && readerShouldBlock(current)))

  28. return -1;

  29. if (sharedCount(c) == MAX_COUNT)

  30. throw new Error("Maximum lock count exceeded");

  31. if (compareAndSetState(c, c + SHARED_UNIT)) {

  32. cachedHoldCounter = rh; // cache for release

  33. rh.count++;

  34. return 1;

  35. }

  36. }

  37. }

读取锁获取的过程是这样的:

  1. 如果写线程持有锁(也就是独占锁数量不为0),并且独占线程不是当前线程,那么就返回失败。因为允许写入线程获取锁的同时获取读取锁。否则进行2。
  2. 如果读线程请求锁数量达到了65535(包括重入锁),那么就跑出一个错误Error,否则进行3。
  3. 如果读线程不用等待(实际上是是否需要公平锁),并且增加读取锁状态数成功,那么就返回成功,否则进行4。
  4. 步骤3失败的原因是CAS操作修改状态数失败,那么就需要循环不断尝试去修改状态直到成功或者锁被写入线程占有。实际上是过程3的不断尝试直到CAS计数成功或者被写入线程占有锁。

3.4 HoldCounter

在清单7 中有一个对象HoldCounter,这里暂且不提这是什么结构和为什么存在这样一个结构。

接下来根据清单8 我们来看如何释放一个读取锁。同样先不理HoldCounter,关键的在于for循环里面,其实就是一个不断尝试的CAS操作,直到修改状态成功。前面说过state的高16位描述的共享锁(读取锁)的数量,所以每次都需要减去2^16,这样就相当于读取锁数量减1。实际上SHARED_UNIT=1<<16。

清单8 读取锁释放过程

  1. protected final boolean tryReleaseShared(int unused) {

  2. HoldCounter rh = cachedHoldCounter;

  3. Thread current = Thread.currentThread();

  4. if (rh == null || rh.tid != current.getId())

  5. rh = readHolds.get();

  6. if (rh.tryDecrement() <= 0)

  7. throw new IllegalMonitorStateException();

  8. for (;;) {

  9. int c = getState();

  10. int nextc = c - SHARED_UNIT;

  11. if (compareAndSetState(c, nextc))

  12. return nextc == 0;

  13. }

  14. }

好了,现在回头看HoldCounter到底是一个什么东西。首先我们可以看到只有在获取共享锁(读取锁)的时候加1,也只有在释放共享锁的时候减1有作用,并且在释放锁的时候抛出了一个IllegalMonitorStateException异常。而我们知道IllegalMonitorStateException通常描述的是一个线程操作一个不属于自己的监视器对象的引发的异常。也就是说这里的意思是一个线程释放了一个不属于自己或者不存在的共享锁。

前面的章节中一再强调,对于共享锁,其实并不是锁的概念,更像是计数器的概念。一个共享锁就相对于一次计数器操作,一次获取共享锁相当于计数器加1,释放一个共享锁就相当于计数器减1。显然只有线程持有了共享锁(也就是当前线程携带一个计数器,描述自己持有多少个共享锁或者多重共享锁),才能释放一个共享锁。否则一个没有获取共享锁的线程调用一次释放操作就会导致读写锁的state(持有锁的线程数,包括重入数)错误。

明白了HoldCounter的作用后我们就可以猜到它的作用其实就是当前线程持有共享锁(读取锁)的数量,包括重入的数量。那么这个数量就必须和线程绑定在一起。

在Java里面将一个对象和线程绑定在一起,就只有ThreadLocal才能实现了。所以毫无疑问HoldCounter就应该是绑定到线程上的一个计数器。

清单9 线程持有读取锁数量的计数器

  1. static final class HoldCounter {

  2. int count;

  3. final long tid = Thread.currentThread().getId();

  4. int tryDecrement() {

  5. int c = count;

  6. if (c > 0)

  7. count = c - 1;

  8. return c;

  9. }

  10. }

  11. static final class ThreadLocalHoldCounter

  12. extends ThreadLocal<HoldCounter> {

  13. public HoldCounter initialValue() {

  14. return new HoldCounter();

  15. }

  16. }

清单9 描述的是线程持有读取锁数量的计数器。可以看到这里使用ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。

除了readLock()和writeLock()外,Lock对象还允许tryLock(),那么ReadLock和WriteLock的tryLock()不一样。清单10 和清单11 分别描述了读取锁的tryLock()和写入锁的tryLock()。

读取锁tryLock()也就是tryReadLock()成功的条件是:没有写入锁或者写入锁是当前线程,并且读线程共享锁数量没有超过65535个。

写入锁tryLock()也就是tryWriteLock()成功的条件是: 没有写入锁或者写入锁是当前线程,并且尝试一次修改state成功。

清单10 读取锁的tryLock()

  1. final boolean tryReadLock() {

  2. Thread current = Thread.currentThread();

  3. for (;;) {

  4. int c = getState();

  5. if (exclusiveCount(c) != 0 &&

  6. getExclusiveOwnerThread() != current)

  7. return false;

  8. if (sharedCount(c) == MAX_COUNT)

  9. throw new Error("Maximum lock count exceeded");

  10. if (compareAndSetState(c, c + SHARED_UNIT)) {

  11. HoldCounter rh = cachedHoldCounter;

  12. if (rh == null || rh.tid != current.getId())

  13. cachedHoldCounter = rh = readHolds.get();

  14. rh.count++;

  15. return true;

  16. }

  17. }

  18. }

清单11 写入锁的tryLock()

  1. final boolean tryWriteLock() {

  2. Thread current = Thread.currentThread();

  3. int c = getState();

  4. if (c != 0) {

  5. int w = exclusiveCount(c);

  6. if (w == 0 ||current != getExclusiveOwnerThread())

  7. return false;

  8. if (w == MAX_COUNT)

  9. throw new Error("Maximum lock count exceeded");

  10. }

  11. if (!compareAndSetState(c, c + 1))

  12. return false;

  13. setExclusiveOwnerThread(current);

  14. return true;

  15. }

四、小结

使用ReentrantReadWriteLock可以推广到大部分读,少量写的场景,因为读线程之间没有竞争,所以比起sychronzied,性能好很多。
如果需要较为精确的控制缓存,使用ReentrantReadWriteLock倒也不失为一个方案。

参考内容来源:

ReentrantReadWriteLock  http://uule.iteye.com/blog/1549707

深入浅出 Java Concurrency (13): 锁机制 part 8 读写锁 (ReentrantReadWriteLock) (1)

http://www.blogjava.net/xylz/archive/2010/07/14/326080.html
深入浅出 Java Concurrency (14): 锁机制 part 9 读写锁 (ReentrantReadWriteLock) (2)
http://www.blogjava.net/xylz/archive/2010/07/15/326152.html
高性能锁ReentrantReadWriteLock
http://jhaij.iteye.com/blog/269656
JDK说明
http://www.cjsdn.net/Doc/JDK60/java/util/concurrent/locks/ReentrantReadWriteLock.html
关于concurrent包 线程池、资源封锁和队列、ReentrantReadWriteLock介绍
http://www.oschina.net/question/16_636

Java多线程(十)之ReentrantReadWriteLock深入分析相关推荐

  1. Java多线程读写锁ReentrantReadWriteLock原理详解

    ReentrantLock属于排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读和其他写线程都被阻塞.读写锁维护了一对锁,一个读锁和一 ...

  2. Java多线程系列(十一):ReentrantReadWriteLock的实现原理与锁获取详解

    我们继续Java多线程与并发系列之旅,之前我们分享了Synchronized 和 ReentrantLock 都是独占锁,即在同一时刻只有一个线程获取到锁. 然而在有些业务场景中,我们大多在读取数据, ...

  3. java 小程序 多线程_《多线程练习—买票小程序——Java第十四周》

    /* (程序头部注释开始) * 程序的版权和版本声明部分 * Copyright (c) 2011, 烟台大学计算机学院学生 * All rights reserved. * 文件名称:    < ...

  4. Java多线程编程之读写锁【ReentrantReadWriteLock】

    有时候我们需要有这样的需求:         对于同一个文件进行读和写操作,普通的锁是互斥的,这样读的时候会加锁,只能单线程的读,我们希望多线程的进行读操作,并且读的时候不能进行写操作,写的时候不能进 ...

  5. Java多线程总结(3)— Timer 和 TimerTask深入分析

    1. 基本概念   java.util.Timer:是一个实用工具类,该类用来调度一个线程(schedule a thread),使它可以在将来某一时刻执行. Java的Timer类可以调度一个任务运 ...

  6. Java多线程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock

    在JDK5里面,提供了一个Lock接口.该接口通过底层框架的形式为设计更面向对象.可更加细粒度控制线程代码.更灵活控制线程通信提供了基础.实现Lock接口且使用得比较多的是可重入锁(Reentrant ...

  7. JAVA多线程提高十四: 面试题

    前面针对多线程相关知识点进行了学习,那么我们来来看看常见的面试题: 1. 空中网面试题1 package com.kongzhongwang.interview; import java.util.c ...

  8. java多线程做一件事_关于Java的十件事

    java多线程做一件事 那么,您从一开始就一直在使用Java? 还记得那些被称为" Oak"的日子,OO仍然是热门话题,C ++人士认为Java没有机会,Applet还是一件事吗? ...

  9. Java多线程系列(十):源码剖析AQS的实现原理

    在并发编程领域,AQS号称是并发同步组件的基石,很多并发同步组件都是基于AQS实现,所以想掌握好高并发编程,你需要掌握好AQS. 本篇主要通过对AQS的实现原理.数据模型.资源共享方式.获取锁的过程, ...

最新文章

  1. C++友元函数和友元类(三)
  2. Algorithm:C++语言实现之概率算法相关问题(计算机中的概率事件、C语言中的随机事件、产生二维随机数、圆内均匀取点)
  3. windows QT kits配置
  4. java实现简单二叉树
  5. Android商城开发系列(一)——开篇
  6. (93)多人投票器(N人投票器)
  7. jsp文件创建后自动设置其pageEncoding的属性值为UTF-8
  8. php 多个箭头,php连续的两个箭头-〉是什么意思??
  9. 双轨制直销软件功能介绍
  10. # c++万能头文件
  11. 一键自动下载百度美女图片
  12. 基于SSM实现的儿童疫苗信息管理系统设计与实现 毕业设计-附源码311930
  13. pe卸载linux系统软件,Windows和Linux双系统下完美卸载linux
  14. ubuntu16.04中 vim8 backspace键删除功能失效
  15. Lua的安装配置出现的问题以及解决方案(Win10环境下)
  16. 【问题记录】怎么用python读取CIFAR10数据集?
  17. 在等吴恩达深度学习第5课的时候,你可以先看看第4课的笔记
  18. vanilla-tilt.js平滑3D倾斜库的使用
  19. res\values-sw360dp\dimens.xml.orig:518: error:
  20. Safari下载文件名乱码

热门文章

  1. python 问题自动匹配解决方案_匹配字典集。最优雅的解决方案。Python
  2. python批量解压文件_python 批量解压压缩文件的实例代码
  3. C语言 十六进制整数字符串转十进制整数
  4. c语言学生成绩删除功能,c语言学生成绩管理系统程序设计,有添加,查找,删除,输出,修改,排序等功能!!!...
  5. 古风登陆html,再祭古风扬 文/凌霄一羽
  6. java threadstatus_Thread之一:线程生命周期及六种状态
  7. 高频hf调制方式_收藏!AM的三种调制电路
  8. arm linux 添加ntp服务,嵌入式linux添加NTP服务
  9. 安卓手机玩游戏卡顿怎么解决_手机卡顿怎么办?教你3个实用解决方法,两分钟解决卡顿难题!...
  10. C# 解决串口接收数据不完整