多线程并发编程9-ReentrantReadWriteLock源码剖析

    前面文章说到了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变量分为读写两部分,读共享,写独占,这种在读多写少的场景下比较适用。

     今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。

发布了11 篇原创文章 · 获赞 3 · 访问量 591

猜你喜欢

转载自blog.csdn.net/zfs_sir/article/details/104956285