前面文章说到了ReentrantLock,解决线程间安全问题,使用ReentrantLock就可以,但是ReentrantLock是独占锁,某一个时刻只能一个线程获取锁,在写少读多的场景下,显然ReentrantLock并不能满足次场景。今天要说的ReentrantReadWriteLock锁就能满足写少读多的场景。
ReentrantReadWriteLock锁采用读写分离的策略,读锁是一个共享锁,允许多个线程同时获取。写锁是一个独占锁,只允许一个线程获取。当一个线程获取写锁,其他线程不能获取写锁,也不能获取读锁,但是获取写锁的线程可以获取读锁。当一个线程获取读锁,其他线程仍然可以获取读锁,但是无法获取写锁。
ReentrantReadWriteLock内部维护了一个ReadLock和WriterLock,它们依赖Sync实现具体功能,Sync继承AQS,并且提供了公平和非公平的实现。在AQS内部的表示锁状态的变量只有一个state,哪个ReentrantReadWriteLock是怎么同时表示读锁和写锁的状态呢?ReentrantReadWriteLock将state拆分为高16位低16位,高16位表示读锁的状态,低16位表示写锁的状态。
下面分别对ReentrantReadWriteLock类读锁、写锁的获取和释放进行源码介绍。
写锁
ReentrantReadWriteLock内部中的WriterLock是一个独占锁,某时只能有一个线程获取WriterLock。同时WriterLock是一个可重入锁。
获取写锁
如果当前已经有线程获取到读锁和写锁,则当前请求的线程会被阻塞挂起。如果当前线程已经获取了该锁,再次获取只会简单地把可重入次数加1返回。
lock()
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w =exclusiveCount(c);
//(1)
if (c !=0) {
//(2)
if (w ==0 || current != getExclusiveOwnerThread())
return false;
//(3)
if (w +exclusiveCount(acquires) >MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
//(4)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
从源码可以看见ReentrantReadWriteLock的lock方法和ReentrantLock的lock方法内部调用的方法一致,不同的在tryAcquire方法尝试获取资源的实现上。
(1)获取state值并且不为0,说明读锁或写锁已经被某线程获取了。
(2)w=0说明有线程获取到读锁,则返回false。w!=0说明有线程获取到写锁,如果获取到写锁的线程不是当前写锁则返回false。返回false之后,会将当前线程插入到AQS阻塞队列,并阻塞挂起。
(3)执行到这说明当前线程获取到了写锁,则判断重入次数是否超过最大值,没有则增加重入次数,否则抛出error。
(4)代码执行到这说明没有读锁和写锁都没有被线程获取,writerShouldBlock方法判断是否需要阻塞,判断分为非公平和公平,非公平方式则直接返回false,公平方式则判断AQS阻塞队列中是否有非CANCLE状态的节点,有则返回true,否则返回false。writerShouldBlock返回false,则使用CAS算法设置state状态获取锁,设置成功则将当前线程记录到写锁内,否则返回false。返回false之后,会将当前线程插入到AQS阻塞队列,并阻塞挂起。
释放写锁
尝试释放锁,如果当前线程持有该锁,则将state减一,state为0则当前线程会释放该锁。如果当前线程没有持有该锁则会抛出IllegalMonitorStateException异常。
unlock()
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//(1)
if (tryRelease(arg)) {
Node h =head;
if (h !=null && h.waitStatus !=0)
unparkSuccessor(h);
return true;
}
return false;
}
(1)尝试释放写锁,释放成功则从AQS阻塞队列的head节点找到第一个符合条件的node,唤醒node关联的线程。
protected final boolean tryRelease(int releases) {
//(1)
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//(2)
int nextc = getState() - releases;
boolean free =exclusiveCount(nextc) ==0;
if (free)
setExclusiveOwnerThread(null);
//(3)
setState(nextc);
return free;
}
(1)判断当前线程和写锁中记录的线程是否一致,不一致则抛出IllegalMonitorStateException异常。
(2)修改state值,判断state的低16位是否等于0,如果等于0,则将写锁中记录的线程设置为null,即为释放写锁。
(3)设置state值。
读锁
ReentrantReadWriteLock内部中的ReadLock是一个共享锁,运行多个线程获取ReadLock。同时ReadLock是一个可重入锁,每个获取到ReadLock锁的线程都在线程本地记录重入次数,当重入次数为0的时候就说明该线程释放该ReadLock锁。
获取读锁
获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS中的state高16位会加1。如果其他线程持有写锁,则当前线程会被阻塞挂起。
lock()
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {if (tryAcquireShared(arg) <0) //(1)
doAcquireShared(arg); //(2)
}
(1)调用ReentrantReadWriteLock中的sync实现的tryAcquireShared方法。
(2)调用AQS的doAcquireShared方法,具体实现点这里。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//(1)
if (exclusiveCount(c) !=0 &&
getExclusiveOwnerThread() != current)
return -1;
//(2)
int r =sharedCount(c);
//(3)
if (!readerShouldBlock() &&
r< MAX_COUNT &&
compareAndSetState(c, c +SHARED_UNIT)) {
//(4)
if (r ==0) {
firstReader = current;
firstReaderHoldCount =1;
}else if (firstReader == current) { //(5)
firstReaderHoldCount++;
}else { //(6)
HoldCounter rh =cachedHoldCounter;
if (rh ==null || rh.tid !=getThreadId(current))
cachedHoldCounter = rh =readHolds.get();
else if (rh.count ==0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//(7)
return fullTryAcquireShared(current);
}
(1)如果写锁有线程获取并且写锁记录的线程并非当前线程则会返回-1,也就反向说明当获取写锁的线程为当前线程,则当前线程可以再获取读锁。注意:当一个线程先获取写锁,后获取读锁,处理完之后要记得把读锁和写锁都释放,不能只释放写锁。
(2)获取读锁计数。
(3)如果判断不需要阻塞并且设置AQS中的state成功,只能有一个线程设置成功,其他失败的线程会执行(7)进行自旋重试。
(4)r=0说明当前线程是第一个尝试获取读锁的线程。
(5)当前线程是第一个获取读锁的线程。
(6)使用cachedHoldCounter记录最后一个获取到读锁的线程和改线程获取读锁的可重入数,readHolds记录了当前线程获取读锁的可重入数。
(7)类似tryAcquireShared,但是是自旋获取。
释放读锁
unlock()
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {//(1)
if (tryReleaseShared(arg)) {
doReleaseShared(); //(2)
return true;
}
return false;
}
(1)调用ReentrantReadWriteLock中的sync实现的tryReleaseShared方法。
(2)调用AQS的doReleaseShared方法,具体实现点这里。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//(1)
if (firstReader == current) {
if (firstReaderHoldCount ==1)
firstReader =null;
else
firstReaderHoldCount--;
}else {
HoldCounter rh =cachedHoldCounter;
if (rh ==null || rh.tid !=getThreadId(current))
rh =readHolds.get();
int count = rh.count;
if (count <=1) {
readHolds.remove();
if (count <=0)
throw unmatchedUnlockException();
}
--rh.count;
}
//(2)
for (;;) {
int c = getState();
int nextc = c -SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc ==0;
}
}
(1)判断当前线程是不是第一个获取读锁的线程,是的话firstReaderHoldCount减1。
(2)通过自旋的方式将state减去一个读计数单位,state减完之后等于0,说明没有当前已经没有线程获取读锁了,则tryReleaseShared返回true。然后会调用doReleaseShared方法释放一个由于获取写锁而阻塞的线程。
ReentrantReadWriteLock的底层使用AQS,巧妙的将AQS中的state变量分为读写两部分,读共享,写独占,这种在读多写少的场景下比较适用。
今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。