ReentrantReadWriteLock source code analysis notes

ReentrantReadWriteLock includes two locks, one read lock ReadLock, which was shared locks, one write lock WriteLock, which was exclusive lock The two lock AQS are based to achieve.

 

By the following source code to see how ReentrantReadWriteLock do is read to share, read and write mutually exclusive.

 

1. Test Code 

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ShareLockTest {
    public static void main(String[] args) {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        CyclicBarrier cyclicBarrier = new CyclicBarrier(50);
        for (int i = 1; i <= 50; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (finalI % 2 == 0) {
                    System.out.println(Thread.currentThread().getName() + "开始抢写锁");
                    writeLock.lock();
                } else {
                    System.out.println(Thread.currentThread().getName() + "开始抢读锁");
                    readLock.lock();
                }
                try {
                    System.out.println(Thread.currentThread().getName() + "抢读锁成功");
                    Thread.currentThread().sleep(1000);
                    System.out.println(Thread.currentThread().getName() + "释放读锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (finalI % 2 == 0) {
                        writeLock.unlock();
                    } else {
                        readLock.unlock();
                    }
                }
            }, "线程" + i).start();

        }
        System.out.println("main over");
    }
}

 

2. Get a read lock resources

 

Read lock resources available through the following piece of code to achieve

protected final int tryAcquireShared(int unused) {
   // 1. 如果读锁被其它线程持有,失败
    // 当前抢锁的线程
    Thread current = Thread.currentThread();
    // AQS四大属性中的state值 
    int c = getState();
    // 如果持有写锁的线程数量不等于0 且 当前线程不是AQS中的保存的写锁线程 (忽略重入情况)
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)  // 简单讲就是当前线程不是持有写锁的线程就返回-1
        return -1;   // 获取读锁失败
    // 拥有读锁的线程数量
    int r = sharedCount(c);
    
    if (!readerShouldBlock()  //  不需要排队
        && r < MAX_COUNT      //  拥有读锁的线程数量 小于65535
        && compareAndSetState(c, c + SHARED_UNIT)) {  // 通过cas将AQS中state值由c修改成c+65536
        if (r == 0) {// 如果还没有线程持有读锁
            firstReader = current;  // 将当前线程赋值给firstReader这个变量,其实就是标识一下
            firstReaderHoldCount = 1;   // 读锁持有量记为1,以便于这个线程再次获取读锁时进行累加
        } else if (firstReader == current) {  //如果当前线程等于firstReader,将firstReaderHoldCount加1
            firstReaderHoldCount++;
        } else {   // 如果是其它的线程来获取读锁
            // 与上面原理一样,也是一个计数器,来计录每个线程获取读锁的次数(底层使用了一个ThreadLocal)
            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; // 获取读锁成功
    }
    // 没看懂, 似乎是为了抓捕漏网之鱼
    return fullTryAcquireShared(current);
}

 

以上代码不难, 就是通过 tryAcquireShared获取读锁资源 ,如果获取读锁失败, 就会执行 doAcquireShared 方法. 这个方法有两个功能, 首次是将当前线程封装成一个Node节点(注意该Node是SHARED模式),然后通过addWaiter方法将其添加到CLH链表的尾部. 再次就是将其park.

 

3. 获取写锁资源

下面这段代码就是尝试获取写锁的过程

protected final boolean tryAcquire(int acquires) {
    // 当前线程
    Thread current = Thread.currentThread();
    // AQS四大属性的state, 只有在即无读锁也无写锁的情况,才等于0
    int c = getState();
    // 写锁的数量
    int w = exclusiveCount(c);
    
    if (c != 0) {  // 表示已经有线程持有锁(可能是读锁,也可能是写锁)
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread()) // 无线程持有写锁,或者是持有写锁的线程不是当前线程,返回false
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 重入, 持有写锁的线程再次获取锁,对state值进行更新
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))  // NonfairSync 默认就是false
        return false;
    // 当前线程获取到锁
    setExclusiveOwnerThread(current);
    return true;
}

 

就是通过上面这段代码来进行写锁获取,可以看到当前线程能否获取到写锁资源, 最终还是通过AQS中exclusiveOwnerThread与当前线程进行比较. 

(1) c = 0 , w= 0 时 , 说明还没有线程持有锁资源(读锁和写锁), 这时当前线程获取写锁肯定成功(应该只有第一次获取写锁才会走到下面的逻辑)

  compareAndSetState(c, c + acquires)  //将AQS中state设置为1 
   setExclusiveOwnerThread(current);    //将AQS中exclusiveOwnerThread设置为当前线程

(2) c != 0 , w =0时, 先判断写锁数量是否等于0,.如果等于0再判断 exclusiveOwnerThread 是否是当前线程,如果不是,返回false,获取写锁资源失败. 如果是, 表示当前线程再次获取写锁资源了(重入锁的情况), 这时会对AQS对象的state属性值加1, 同时返回true, 获取写锁成功.

 

总之,  tryAcquire()方法就是尝试获取写锁资源, 如果获取成功,一切好说. 如果获取失败, 就会通过 addWaiter方法将当前线程封装成一个Node节点(注意该节点是EXCLUSIVE模式),放到CLH链表中,然后再通过acquireQueued方法将当前线程进行park.(具体过程可参考AQS源码分析笔记 )

 

4. 读锁释放, 写锁唤醒

 

咱们通过debug来模拟这样一种情况 . 1号线程和11号线程持有读锁, 10号线程,20号线程获取写锁没成功, 被挂起了.

现在1号线程释放读锁资源, 看看会发生什么情况....

 

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        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;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
       
            return nextc == 0;
    }
}


tryReleaseShared方法很简单,唯一需要注意的就是标红部分,只有当最终return的结果是true时,才会进入到 doReleaseShared()方法中, 看下源码
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);  // 唤醒线程
            }
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

 

通过debug, 不难发现, 读锁资源被1号线程和11号线程持有,我先释放1号线程的读锁资源,结果读锁资源并没有释放成功, 我再去11号线程的读锁资源, 结果释放成功. 然后进入到doReleaseShared()方法中,这个方法主要就是去唤醒CLH链表中线程.

 

5. 写锁释放,唤醒写锁

   public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

这段代码在讲AQS中也提到过, 如果锁资源释放成功,会通过unparkSuccessor方法唤醒CLH链表中下一个节点的线程, 这时不再多说了.

 

6. 写锁释放,唤醒读锁

这种情况有点特别, 先是通过释放写锁的线程去唤醒CLH链表中head节点next节点指向的读锁线程, 然后再通过这个读锁线程递归唤醒所有读锁线程

private void unparkSuccessor(Node node) {
  
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);  // 唤醒CLH链表中第一个读锁线程
}

 

 

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

 

注意标红部分,读锁线程被唤醒之后,for循环就活了,然后就会调用  setHeadAndPropagate(node, r)--->  doReleaseShared() ---->  unparkSuccessor(h), 

如果最后这个unparkSuccessor(h)方法中,h节点的下一个节点是读锁线程,那么又会触发一次  setHeadAndPropagate(node, r)--->  doReleaseShared() ---->  unparkSuccessor(h)调用链,直到将所有读锁线程都唤醒.

 

7. 总结

(1) ReentrantReadWriteLock是在AQS的基础上实现读,写锁分离的过程.

(2) 将state这个属性值 拆分为高低位,来实现读,写锁控制 (有点懵, 位运算,与运算可读性差...)

(3) 读,写锁线程的Node节点仍然是放在CLH链表中的..

(4) 读锁线程唤醒可一次性唤醒多个, 写锁线程一次只能唤醒 一个

Guess you like

Origin www.cnblogs.com/z-qinfeng/p/12081683.html