• ReentrantReadWriteLock源码分析笔记


    ReentrantReadWriteLock包含两把锁,一是读锁ReadLock, 此乃共享锁, 一是写锁WriteLock, 此乃排它锁. 这两把锁都是基于AQS来实现的.

    下面通过源码来看看ReentrantReadWriteLock是如何做到读读共享,读写互斥的.

    1. 测试代码 

    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. 获取读锁资源

    读锁资源的获取通过下面这段代码实现

    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) 读锁线程唤醒可一次性唤醒多个, 写锁线程一次只能唤醒 一个

  • 相关阅读:
    gitlab授权登录
    mysql的sql_mode设置
    fork了别人项目怎么保持更新呢?
    memcache命令参数详解
    memcache 操作详解
    字符串拼接性能对比
    yum设置镜像
    ifconfig找不到命令怎么通过工具连
    this is incompatible with sql_mode=only_full_group_by
    jmeter切换语言
  • 原文地址:https://www.cnblogs.com/z-qinfeng/p/12081683.html
Copyright © 2020-2023  润新知