Java多线程(十)之ReentrantReadWriteLock深刻分析

1、ReentrantReadWriteLock与ReentrantLock


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

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

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

 

清单0 ReadWriteLock 接口 并发

[java]  view plain copy
  1. public interface ReadWriteLock {  
  2.     Lock readLock();  
  3.     Lock writeLock();  
  4. }  

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


2、ReentrantReadWriteLock的特性


ReentrantReadWriteLock有如下几个特性: 性能

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


3、ReentrantReadWriteLock的内部实现


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


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

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

清单1 ReadLock 片断 .net

[java]  view plain copy
  1. public static class ReadLock implements Lock, java.io.Serializable  {  
  2.     private final Sync sync;  
  3.   
  4.     protected ReadLock(ReentrantReadWriteLock lock) {  
  5.         sync = lock.sync;  
  6.     }  
  7.   
  8.     public void lock() {  
  9.         sync.acquireShared(1);  
  10.     }  
  11.   
  12.     public void lockInterruptibly() throws InterruptedException {  
  13.         sync.acquireSharedInterruptibly(1);  
  14.     }  
  15.   
  16.     public  boolean tryLock() {  
  17.         return sync.tryReadLock();  
  18.     }  
  19.   
  20.     public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {  
  21.         return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));  
  22.     }  
  23.   
  24.     public  void unlock() {  
  25.         sync.releaseShared(1);  
  26.     }  
  27.   
  28.     public Condition newCondition() {  
  29.         throw new UnsupportedOperationException();  
  30.     }  
  31.   
  32. }  


清单2 WriteLock 片断 线程

[java]  view plain copy
  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.   
  10.     public void lockInterruptibly() throws InterruptedException {  
  11.         sync.acquireInterruptibly(1);  
  12.     }  
  13.   
  14.     public boolean tryLock( ) {  
  15.         return sync.tryWriteLock();  
  16.     }  
  17.   
  18.     public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {  
  19.         return sync.tryAcquireNanos(1, unit.toNanos(timeout));  
  20.     }  
  21.   
  22.     public void unlock() {  
  23.         sync.release(1);  
  24.     }  
  25.   
  26.     public Condition newCondition() {  
  27.         return sync.newCondition();  
  28.     }  
  29.   
  30.     public boolean isHeldByCurrentThread() {  
  31.         return sync.isHeldExclusively();  
  32.     }  
  33.   
  34.     public int getHoldCount() {  
  35.         return sync.getWriteHoldCount();  
  36.     }  
  37. }  


清单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 写入锁获取片断

[java]  view plain copy
  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 公平读写锁写线程是否阻塞

[java]  view plain copy
  1. final boolean writerShouldBlock(Thread current) {  
  2.     return !isFirst(current);  
  3. }  


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

[java]  view plain copy
  1. final boolean writerShouldBlock(Thread current) {  
  2.     return false;  
  3. }  


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

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

[java]  view plain copy
  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 读取锁获取过程片断

[java]  view plain copy
  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.   
  20. final int fullTryAcquireShared(Thread current) {  
  21.     HoldCounter rh = cachedHoldCounter;  
  22.     if (rh == null || rh.tid != current.getId())  
  23.         rh = readHolds.get();  
  24.     for (;;) {  
  25.         int c = getState();  
  26.         int w = exclusiveCount(c);  
  27.         if ((w != 0 && getExclusiveOwnerThread() != current) ||  
  28.             ((rh.count | w) == 0 && readerShouldBlock(current)))  
  29.             return -1;  
  30.         if (sharedCount(c) == MAX_COUNT)  
  31.             throw new Error("Maximum lock count exceeded");  
  32.         if (compareAndSetState(c, c + SHARED_UNIT)) {  
  33.             cachedHoldCounter = rh; // cache for release  
  34.             rh.count++;  
  35.             return 1;  
  36.         }  
  37.     }  
  38. }  


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

    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 读取锁释放过程

[java]  view plain copy
  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 线程持有读取锁数量的计数器

[java]  view plain copy
  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.   
  12. static final class ThreadLocalHoldCounter  
  13.     extends ThreadLocal<HoldCounter> {  
  14.     public HoldCounter initialValue() {  
  15.         return new HoldCounter();  
  16.     }  
  17. }  


清单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()

[java]  view plain copy
  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()

[java]  view plain copy
  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. }  


4、小结


使用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

相关文章
相关标签/搜索