ReentrantReadWriteLock源码分析,java8

一,关系简介

二, 实现

  2.1 Sync的成员变量
  2.2 构造器
  2.3 lock()
  2.4 ReadLock.lock()流程图

关系简介

这里写图片描述

  在这个关系很乱的UML图中,我们可以发现ReentrantReadWriteLock实现了ReadWriteLock接口,内部类有Sync(锁的实现),NonfairSync,FairSync公平锁与非公平锁,WriteLock和ReadLock读锁与写锁。还引用了AQS来管理阻塞队列。

  所以我们可以大致知道加锁方式有公平的读写锁,还有不公平的读写锁。同时有读写,那就可能会发生资源竞争。在资源竞争产生的情况只有读和写,写和写之间会产生资源 竞争,读和读没有什么竞争压力。
  那就还要保存锁的状态,ReentrantReadWriteLock保存锁的状态是采用高低位进行记录的。高16位来记录读锁(共享锁)的state,低16记录写锁(独占锁)的state。

Sync的成员变量

        //读写锁用state记录两种锁的状态
        // 高16位来记录读锁(共享锁)的state,低16记录写锁(独占锁)的state
        //读锁占用的位数
        static final int SHARED_SHIFT   = 16;
        //每次让读锁状态加1, 则让state加1<<16
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        //最大可重入数,也就是int_MAX
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        //写锁标记,作getState&EXCLUSIVE_MASK 大于1就是重入了
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        //获取读锁的重入数
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        //获取写锁的重入数
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

举个例子:比如,现在当前,申请读锁的线程数为13个,写锁1个,那state怎么表示?
上文说过,用一个32位的int类型的高16位表示读锁线程数,13的二进制为 1101,那state的二进制表示为
00000000 00001101 00000000 00000001,十进制数为851969, 接下在具体获取锁时,需要根据这个851968这个值得出上文中的 13 与 1。要算成13,只需要将state 无符号向右移位16位置,得出00000000 00001101,就出13,根据851969要算成低16位置,只需要用该00000000 00001101 00000000 00000001 & 111111111111111(15位),就可以得出00000001,就是利用了1&1得1,1&0得0这个技巧。

构造器

    /**
     * 默认为非公平锁
     */
    public MyReentrantReadWriteLock() {
        this(false);
    }

    /**
     * @param fair true为公平锁,false 为非公平锁
     */
    public MyReentrantReadWriteLock(boolean fair) {
        sync = fair ? new MyReentrantReadWriteLock.FairSync() : new MyReentrantReadWriteLock.NonfairSync();
        readerLock = new MyReentrantReadWriteLock.ReadLock(this);//读锁
        writerLock = new MyReentrantReadWriteLock.WriteLock(this);//写锁
    }

Lock中核心——lock()

  private final MyReentrantReadWriteLock.ReadLock readerLock;

  private final MyReentrantReadWriteLock.WriteLock writerLock;

  public MyReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
  public MyReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

读写锁分别有自己的lock

我们先看看写锁的lock()

        public void lock() {
            sync.acquire(1);
        }

调用了AQS获取锁的方式,AQS的acquire我在ReentrantLock详细分析了
https://blog.csdn.net/WeiJiFeng_/article/details/81390935

我们再看看读锁的lock()

        public void lock() {
            sync.acquireShared(1);
        }

在进一层

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

首先尝试获取共享锁

        protected final int tryAcquireShared(int unused) {
            //获取当前线程引用
            Thread current = Thread.currentThread();
            //获取当前线程锁的状态
            int c = getState();

            //如果该线程持有写锁,或者获得锁的不是当前线程,都是获取锁失败
            if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                return -1;
            //获取读锁的重入计数器
            int r = sharedCount(c);

            //进入该代码块条件是
            //1,没有写锁被占用时,尝试通过一次CAS去获取锁时,成功获取共享锁
            //2,当前线程占有写锁,并且没有有其他写锁在当前线程的下一个节点等待获取写锁
            if (!readerShouldBlock() &&//读是否应该被阻塞,
                    r < MAX_COUNT &&//读不应该阻塞,共享锁状态是否合法
                    compareAndSetState(c, c + SHARED_UNIT)) {//同时合法那就尝试一次CAS操作获取共享锁

                //获取成功,且该线程是第一次获取到共享锁
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    MyReentrantReadWriteLock.Sync.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);
        }

对于readerShouldBlock这个公平锁和非公平锁有区别

在公平锁中:

    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
    public final boolean hasQueuedPredecessors() {

        //获取阻塞队列头尾
        MyAbstractQueueSync.Node t = tail; // Read fields in reverse initialization order
        MyAbstractQueueSync.Node h = head;
        MyAbstractQueueSync.Node s;
        //如果阻塞队列中没有被阻塞的线程,就不用阻塞
        //如果阻塞队列的队头是当前线程,且阻塞队列的下一个线程不是当前线程也不用阻塞
        return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
    }

在非公平锁中:

    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }
    final boolean apparentlyFirstQueuedIsExclusive() {
        MyAbstractQueueSync.Node h, s;
        return (h = head) != null &&//如果head为null,不阻塞
                (s = h.next)  != null && // head不为null,head的下一个节点为null,也不阻塞
                !s.isShared()         &&//如果锁不是共享锁,不阻塞
                s.thread != null; //head的下一个节点线程为null也不阻塞
    }

我们回到进入判断条件的代码

 if (r == 0) {
     firstReader = current;
     firstReaderHoldCount = 1;
 } else if (firstReader == current) {
     firstReaderHoldCount++;
 } else {
     MyReentrantReadWriteLock.Sync.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;
 }

以及我们在介绍Sync成员变量未提到的几个成员变量

    private transient MyReentrantReadWriteLock.Sync.ThreadLocalHoldCounter readHolds;
    private transient MyReentrantReadWriteLock.Sync.HoldCounter cachedHoldCounter;
    private transient Thread firstReader = null;
    private transient int firstReaderHoldCount;     

  上述这4个变量,其实就是完成一件事情,将获取读锁的线程放入线程本地变量(ThreadLocal),方便从整个上 下文,根据当前线程获取持有锁的次数信息。其实 firstReader,firstReaderHoldCount ,cachedHoldCounter 这三个变量就是为readHolds变量服务的,是一个优化手段,尽量减少直接使用readHolds.get方法的次数,firstReader与firstReadHoldCount保存第一个获取读锁的线程,也就是readHolds中并不会保存第一个获取读锁的线程;cachedHoldCounter 缓存的是最后一个获取线程的HolderCount信息,该变量主要是在如果当前线程多次获取读锁时,减少从readHolds中获取HoldCounter的次数。

我们再次回到没有进入判断条件的代码

        return fullTryAcquireShared(current);

        final int fullTryAcquireShared(Thread current) {

            MyReentrantReadWriteLock.Sync.HoldCounter rh = null;
            for (;;) {
                //还是获取锁的状态
                int c = getState();
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        //如果当前线程不是写锁的持有者,直接退出
                        return -1;
                } else if (readerShouldBlock()) {//重入锁不需要阻塞
                    //当前线程就是第一个获取读锁的线程,那么此时当然是重入锁。
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    //线程阻塞之前,清空readHolds
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //CAS再一次尝试获取锁
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

大回头到acquireShared

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

尝试获取共享锁失败之后,则要进入 doAcquireShared了

    private void doAcquireShared(int arg) {
        //没有获取到锁的线程加入到阻塞队列中
        final MyAbstractQueueSync.Node node = addWaiter(MyAbstractQueueSync.Node.SHARED);
        //设置入队是否成功获取锁的标志
        boolean failed = true;
        try {
            //是否需要挂起
            boolean interrupted = false;
            for (;;) {
                //获取刚刚加入这个阻塞队列的线程的前置节点引用
                final MyAbstractQueueSync.Node p = node.predecessor();
                if (p == head) {
                    //如果这个线程的前置节点呆在阻塞线程的头,那么乐观的尝试再一次获取锁
                    //万一上一个持有锁的线程,在刚刚释放了锁呢
                    int r = tryAcquireShared(arg);
                    //r == 1 获取到锁
                    //r == -1 要老实写入队列
                    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);
        }
    }

挂起也是借助AQS操作,最开始看的ReentrantLock的源码,在那里有详细介绍,doAcquireShared和acquireQueued实现有相近之处:在 acquireQueued之后有详细介绍线程是否需要挂起。
https://blog.csdn.net/WeiJiFeng_/article/details/81390935

设置头并且释放共享锁

    /**
     * @param node 要设置为头的节点
     * @param propagate 固定参数1
     */
    private void setHeadAndPropagate(MyAbstractQueueSync.Node node, int propagate) {
        //获取队列头的引用
        MyAbstractQueueSync.Node h = head; // Record old head for check below
        //set 头为传入的节点
        setHead(node);
        if (propagate > 0 || //如果读锁获取成功
                h == null || //或者头部节点为空
                h.waitStatus < 0 || //或者SIGNAL小于0
                (h = head) == null || h.waitStatus < 0) {
            //刚获取读锁的线程的引用
            MyAbstractQueueSync.Node s = node.next;
            //如果刚 获取读锁的线程下一个节点为空
            //或者 下一个是共享锁的申请
            //去队列中传播唤醒线程
            //是读锁获取的节点,一个获取成功,可以继续下一个读锁的获取、
            //直到队列的节点要获取的是写锁的时候停止传播
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    private void doReleaseShared() {

        for (;;) {
            MyAbstractQueueSync.Node h = head;
            if (h != null && h != tail) {//把阻塞队列从头到尾做个遍历
                //获取当前遍历节点的等待状态
                int ws = h.waitStatus;

                //如果ws == -1,挂起态
                if (ws == MyAbstractQueueSync.Node.SIGNAL) {
                    //cas操作将挂起态转为等待状态
                    if (!compareAndSetWaitStatus(h, MyAbstractQueueSync.Node.SIGNAL, 0))
                        //失败,阻塞着,循环的重新尝试cas操作
                        continue;            // loop to recheck cases
                    //成功就唤醒该节点
                    unparkSuccessor(h);
                }
                //如果状态为0,则设置为Node.PROPAGATE传播状态
                //该值然后会在什么时候变化呢?
                // 在判断该节点的下一个节点是否需要阻塞时,会判断,如果状态不是Node.SIGNAL或取消状态,
                // 为了保险起见,会将前置节点状态设置为Node.SIGNAL,然后再次判断,是否需要阻塞。
                else if (ws == 0 &&
                        !compareAndSetWaitStatus(h, 0, MyAbstractQueueSync.Node.PROPAGATE))
                    //cas设置传播状态失败,阻塞
                    continue;                // loop on failed CAS
            }
            //如果处理过一次 unparkSuccessor 方法后,头节点没有发生变化,就退出该方法
            //那head在什么时候会改变呢?
            // 当然在是抢占锁成功的时候,head节点代表获取锁的节点。
            // 一旦获取锁成功,则又会进入setHeadAndPropagate方法,
            // 当然又会触发doReleaseShared方法,传播特性应该就是表现在这里吧。
            // 再想一下,同一时间,可以有多个多线程占有锁,那在锁释放时,写锁的释放比较简单,
            // 就是从头部节点下的第一个非取消节点,唤醒线程即可,
            // 为了在释放读锁的上下文环境中获取代表读锁的线程,将信息存入在 readHolds ThreadLocal变量中。
            if (h == head)                   // loop if head changed
                break;
        }
    }

lock的源码到这里也分析完毕了,此时我们通过图示流程在来回顾一波ReadLock的lock过程:

  尝试获取读锁过程
这里写图片描述
  从阻塞队列中获取读锁的流程如下:

参考文章:
https://blog.csdn.net/prestigeding/article/details/53286756

http://tool.oschina.net/apidocs/apidoc?api=jdk-zh

猜你喜欢

转载自blog.csdn.net/WeiJiFeng_/article/details/81666444