前言
之前分析的ReentrantLock以及Synchronized都是排他锁,同一时间只允许一个线程进行资源的访问,但是有时需要允许多线程对资源进行读访问,而不允许多线程对资源写访问时,ReentrantLock和Synchronzied很显然就无法实现,好在JUC中已经实现了,就是本文需要分析的读写锁ReentrantReadWriteLock
一、ReentrantReadWriteLock
ReentrantReadWriteLock是JUC提供的读写锁实现,允许同一时刻多个读线程对资源进行访问,当时一旦有写操作,则同一时间不允许读和写操作。
在ReentrantReadWriteLock同时维护了两个锁,一个读锁ReadLock和一个写锁WriteLock,读写锁分离也使得并发性比排他锁性能更高。
1.1、ReentrantReadWriteLock的使用
ReentrantReadWriteLock实现了ReadWriteLock接口,而ReadWriteLock接口只定义了两个方法,分别是
Lock readLock()返回一个读锁;
Lock writeLock()返回一个写锁;
ReentrantReadWriteLock测试案例如下:
1 public classReentrantReadWriteLockTest {2
3 private static ReentrantReadWriteLock readWriteLock = newReentrantReadWriteLock();4 private static Lock readLock =readWriteLock.readLock();5 private static Lock writeLock =readWriteLock.writeLock();6 private static SimpleDateFormat format = new SimpleDateFormat("mm:ss");7
8 public static void main(String[] args) throwsException{9 /**
10 * 读锁是共享锁,多线程可以同时读但是不能写11 * 写锁是排他锁,不允许其他线程读或写12 **/
13
14 /**两个线程同时读*/
15 Thread readThread1 = new Thread(newReadThread());16 Thread readThread2 = new Thread(newReadThread());17 readThread1.start();18 readThread2.start();19 Thread.sleep(5000L);20 System.out.println("*******************两个线程同时读测试结束*******************");21
22 /**两个线程同时写*/
23 Thread writeThread1 = new Thread(newWriteThread());24 Thread writeThread2 = new Thread(newWriteThread());25 writeThread1.start();26 writeThread2.start();27
28 Thread.sleep(10000L);29 System.out.println("*******************两个线程同时写测试结束*******************");30
31 /**先读后写*/
32 Thread readThread3 = new Thread(newReadThread());33 Thread writeThread3 = new Thread(newWriteThread());34 Thread readThread4 = new Thread(newReadThread());35 readThread3.start();36 writeThread3.start();37 readThread4.start();38 Thread.sleep(15000L);39 System.out.println("*******************两个线程先读后写测试结束*******************");40
41 /**先写后读*/
42 Thread readThread5 = new Thread(newReadThread());43 Thread writeThread5 = new Thread(newWriteThread());44 writeThread5.start();45 readThread5.start();46 Thread.sleep(20000L);47 System.out.println("*******************两个线程先写后读测试结束*******************");48 }49
50 static class ReadThread implementsRunnable{51
52 @Override53 public voidrun() {54 try{55 tryRead();56 } catch(InterruptedException e) {57 e.printStackTrace();58 }59 }60 }61
62 static class WriteThread implementsRunnable{63
64 @Override65 public voidrun() {66 try{67 tryWrite();68 } catch(InterruptedException e) {69 e.printStackTrace();70 }71 }72 }73
74 public static void tryRead() throwsInterruptedException {75 System.out.println("线程:"+Thread.currentThread().getName() + "读锁准备加锁:" + format.format(newDate()));76 readLock.lock();77 System.out.println("线程:"+Thread.currentThread().getName() + "读锁加锁成功" + format.format(newDate()));78 Thread.sleep(2000L);79 readLock.unlock();80 System.out.println("线程:"+Thread.currentThread().getName() + "读锁解锁成功" + format.format(newDate()));81 }82
83 public static void tryWrite() throwsInterruptedException {84 System.out.println("线程:"+Thread.currentThread().getName() + "写锁准备加锁" + format.format(newDate()));85 writeLock.lock();86 System.out.println("线程:"+Thread.currentThread().getName() + "写锁加锁成功" + format.format(newDate()));87 Thread.sleep(2000L);88 writeLock.unlock();89 System.out.println("线程:"+Thread.currentThread().getName() + "写锁解锁成功" + format.format(newDate()));90 }91 }
这里分别测试了多种场景
场景一:两个读线程同时进行读操作,测试结果如下:
1 线程:Thread-1读锁准备加锁:40:48
2 线程:Thread-0读锁准备加锁:40:48
3 线程:Thread-1读锁加锁成功40:48
4 线程:Thread-0读锁加锁成功40:48
5 线程:Thread-1读锁解锁成功40:50
6 线程:Thread-0读锁解锁成功40:50
7 *******************两个线程同时读测试结束*******************
可以看出两个读锁是可以同时获取锁成功
场景二:两个写线程同时进行写操作,测试结果如下:
1 线程:Thread-2写锁准备加锁40:53
2 线程:Thread-3写锁准备加锁40:53
3 线程:Thread-2写锁加锁成功40:53
4 线程:Thread-2写锁解锁成功40:55
5 线程:Thread-3写锁加锁成功40:55
6 线程:Thread-3写锁解锁成功40:57
7 *******************两个线程同时写测试结束*******************
可以看出第二个写锁需要等到第一个写锁释放了锁之后才可以获取锁成功
场景三:先读后写再读操作,测试结果如下:
1 线程:Thread-4读锁准备加锁:41:03
2 线程:Thread-5写锁准备加锁41:03
3 线程:Thread-6读锁准备加锁:41:03
4 线程:Thread-4读锁加锁成功41:03
5 线程:Thread-4读锁解锁成功41:05
6 线程:Thread-5写锁加锁成功41:05
7 线程:Thread-5写锁解锁成功41:07
8 线程:Thread-6读锁加锁成功41:07
9 线程:Thread-6读锁解锁成功41:09
10 *******************两个线程先读后写测试结束*******************
可以发现加了读锁之后,写锁需要等到读锁释放了之后才可以获取锁成功,而写锁的等待的时候,此时第二个读锁尝试加锁,虽然此时只有读线程占有了锁,但是由于已经存在写锁在排队了,所以第二个读线程还是需要等到写线程获取锁并释放锁之后才可以获取到锁。
场景四:先写后读操作,测试结果如下:
1 线程:Thread-8写锁准备加锁41:18
2 线程:Thread-7读锁准备加锁:41:18
3 线程:Thread-8写锁加锁成功41:18
4 线程:Thread-8写锁解锁成功41:20
5 线程:Thread-7读锁加锁成功41:20
6 线程:Thread-7读锁解锁成功41:22
7 *******************两个线程先写后读测试结束*******************
很明显写锁比较霸道,一旦加上写锁,其他的读锁也无法再加锁了。
1.2、ReentrantReadWriteLock的实现原理
ReentrantReadWriteLock读写锁的实现完全是靠其内部的内部类读锁ReadLock和写锁WriteLock分别实现的,ReentrantReadWriteLock对象包含这两个锁的引用,用到什么类型的锁时就调用对应的锁的lock方法进行加锁。
而ReentrantReadWriteLock的内部类ReadLock和WriteLock又是通过ReentrantReadWriteLock的另一个内部类AQS的子类Sync来实现的。
读锁ReadLock实现原理
1 public voidlock() {2 sync.acquireShared(1);3 }4
5 protected final int tryAcquireShared(intunused) {6 /*
7 * Walkthrough:8 * 1. If write lock held by another thread, fail.9 * 2. Otherwise, this thread is eligible for10 * lock wrt state, so ask if it should block11 * because of queue policy. If not, try12 * to grant by CASing state and updating count.13 * Note that step does not check for reentrant14 * acquires, which is postponed to full version15 * to avoid having to check hold count in16 * the more typical non-reentrant case.17 * 3. If step 2 fails either because thread18 * apparently not eligible or CAS fails or count19 * saturated, chain to version with full retry loop.20 */
21 Thread current =Thread.currentThread();22 /**1.获取同步状态*/
23 int c =getState();24 /**2.判断是否被写锁占有; exclusiveCount返回写锁占有次数,不等于0则表示被写锁占有;25 * 并且占有锁的线程不是当前线程,则直接返回-1表示占锁失败*/
26 if (exclusiveCount(c) != 0 &&
27 getExclusiveOwnerThread() !=current)28 return -1;29 /**3.获取写锁占有次数*/
30 int r =sharedCount(c);31 /**
32 * 4.判断条件33 * readerShouldBlock():表示当前线程是否需要被阻塞,分成公平模式和非公平模式34 * 公平模式:当前节点为同步队列首节点的后继节点则不需要阻塞;否则则阻塞35 * 非公平模式:首节点是独占式锁时则阻塞;否则不阻塞36 *37 * r < MAX_COUNT:判断共享锁线程数是否小于最大值;最大值为2的16次方-1 = 6553538 *39 * 当这两个条件都满足时,则进行CAS操作尝试获取同步状态,成功则读锁获取成功40 *41 **/
42 if (!readerShouldBlock() &&
43 r < MAX_COUNT &&
44 compareAndSetState(c, c +SHARED_UNIT)) {45 //当r==0时表示当前线程是第一个获取读锁的
46 if (r == 0) {47 firstReader =current;48 firstReaderHoldCount = 1;49 } else if (firstReader ==current) {50 //如果当前线程已经是第一个获取读锁的,则占锁次数+1次
51 firstReaderHoldCount++;52 } else{53 //缓存中记录当前线程重入锁的次数
54 HoldCounter rh =cachedHoldCounter;55 if (rh == null || rh.tid !=getThreadId(current))56 cachedHoldCounter = rh =readHolds.get();57 else if (rh.count == 0)58 readHolds.set(rh);59 rh.count++;60 }61 return 1;62 }63 /**如果CAS获取读锁失败则执行此方法进行循环CAS操作*/
64 returnfullTryAcquireShared(current);65 }66
67 final intfullTryAcquireShared(Thread current) {68 /*
69 * This code is in part redundant with that in70 * tryAcquireShared but is simpler overall by not71 * complicating tryAcquireShared with interactions between72 * retries and lazily reading hold counts.73 */
74 HoldCounter rh = null;75 for(;;) {76 int c =getState();77 /**
78 * 判断当前是否有写锁,并且不是当前线程占有的,则直接返回失败79 **/
80 if (exclusiveCount(c) != 0) {81 if (getExclusiveOwnerThread() !=current)82 return -1;83 //else we hold the exclusive lock; blocking here84 //would cause deadlock.
85 } else if(readerShouldBlock()) {86 //Make sure we're not acquiring read lock reentrantly
87 /**如果当前线程是firstReader则直接通过*/
88 if (firstReader ==current) {89 //assert firstReaderHoldCount > 0;
90 } else{91 /**进入这里表示当前有其他线程占有读锁*/
92 if (rh == null) {93 rh =cachedHoldCounter;94 if (rh == null || rh.tid !=getThreadId(current)) {95 rh =readHolds.get();96 if (rh.count == 0)97 readHolds.remove();98 }99 }100 if (rh.count == 0)101 return -1;102 }103 }104 if (sharedCount(c) ==MAX_COUNT)105 throw new Error("Maximum lock count exceeded");106 if (compareAndSetState(c, c +SHARED_UNIT)) {107 if (sharedCount(c) == 0) {108 firstReader =current;109 firstReaderHoldCount = 1;110 } else if (firstReader ==current) {111 firstReaderHoldCount++;112 } else{113 if (rh == null)114 rh =cachedHoldCounter;115 if (rh == null || rh.tid !=getThreadId(current))116 rh =readHolds.get();117 else if (rh.count == 0)118 readHolds.set(rh);119 rh.count++;120 cachedHoldCounter = rh; //cache for release
121 }122 return 1;123 }124 }125 }
写锁WriteLock的原理
public voidlock() {
sync.acquire(1);
}protected final boolean tryAcquire(intacquires) {/** Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.*/Thread current=Thread.currentThread();int c =getState();int w =exclusiveCount(c);if (c != 0) {//当占锁线程数大于0写当前线程不是独占锁当线程则直接返回false//(Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current !=getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) >MAX_COUNT)throw new Error("Maximum lock count exceeded");//Reentrant acquire,进入这里表示写锁线程重入操作,直接修改状态,写锁不需要CAS
setState(c +acquires);return true;
}//进入这里表示当前没有线程占有锁,则判断是否需要阻塞,公平模式则排队;非公平模式直接尝试CAS获取锁
if (writerShouldBlock() ||
!compareAndSetState(c, c +acquires))return false;//如果获取锁成功,则设置当前线程为独占式锁的线程
setExclusiveOwnerThread(current);return true;
}
写锁的逻辑不复杂,主要就是通过AQS的独占式锁的获取来实现的。