读写锁ReentrantReadWriteLock源码分析

在Java并发包中,有独占模式的锁,同时只允许一个线程执行,阻塞其它所有线程的读写操作;还有共享模式的锁,它允许同时有多个线程进行读操作,但是会阻塞其它线程的写操作。典型的实现就是读写锁——ReentrantReadWriteLock,下面我们来分析一下它的实现原理。

读写状态的设计

如果阅读过我的上一篇文章《ReentrantLock源码分析》可以知道:AQS中有一个state字段表示锁的获取状态,或者说是重入了几次,那么对于读写锁,它既要表示出读锁的重入次数,又要表示写锁的重入次数,用一个int值怎样表示?我们知道int类型有32位,我们可以把它拆为两半,高16位表示读状态,低16位表示写状态,如图:
图片引用自并发编程网:《Java并发编程的艺术》-Java并发包中的读写锁及其实现分析
那么读写锁又是怎样分别获取读和写的状态?答案是通过位运算,如果想获取写状态,只需要将高16位全部抹0即可,也就是state & 0x0000FFFF,如果要获取读状态,只需将高16位无符号右移16位即可,也就是state >>> 16。如果写状态+1,直接state+1,如果读状态+1,那么就需要state + 0x00010000。
以上便是读写状态的设计原理,我们再来看看源码中是怎么写的:

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

在ReentrantReadWriteLock中有一个静态内部类Sync,这个Sync中又定义了上面几个字段,其中:
SHARED_SHIFT可以理解为移位的位数;
SHARED_UNIT就是在读状态+1时,state相加的那个值,1左移16位便是0x00010000;
MAX_COUNT可以代表读锁或者写锁的最大重入次数,1左移16位再-1等于0x0000FFFF,也就是16bit表示的最大值,无论是读还是写都不能超过这个值;
EXCLUSIVE_MASK就是在获取写状态时进行&运算的掩码,同为0x0000FFFF。

紧接着还有两个方法:
sharedCount计算读状态,直接把参数c无符号右移16位;
exclusiveCount计算写状态,将参数c和EXCLUSIVE_MASK做&运算。
跟刚才所述的原理是相符的。

写锁的加锁过程

在ReentrantReadWriteLock中有一个静态内部类WriteLock即使写锁,它的lock方法调用了sync.acquire(1),其中acquire方法中又调用了tryAcquire,我们直接来看Sync中tryAcquire的实现(这些步骤在我的上一篇文章中已有详细描述,此处不再赘述):

        protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            Thread current = Thread.currentThread();
            //获取state值
            int c = getState();
            //获取写状态
            int w = exclusiveCount(c);
            if (c != 0) {
                //如果state不为0,且写状态为0,说明有线程获取了读锁,此时要阻塞写锁,直接返回false
                //如果state不为0,写状态也不为0,说明有线程持有写锁,但是当前线程不是持有锁的线程,也返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //如果有线程持有写锁且等于当前线程,则重入
                setState(c + acquires);
                return true;
            }
            //如果没有线程持有锁,则先判断当前线程是否需要被阻塞
            if (writerShouldBlock() ||
            	//如果不需要被阻塞,则利用CAS设置state值
                !compareAndSetState(c, c + acquires))
                return false;
            //获取锁成功,将独占线程设置为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }

上述代码,需要关注一下writerShouldBlock()方法,这是一个在Sync中定义的抽象方法,具体实现交由FairSync和NonfairSync,FairSync中实现如下:

        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }

hasQueuedPredecessors()方法的解析请参考上一篇文章ReentrantLock源码分析

NonfairSync中实现:

        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }

可见非公平锁直接返回false,直接竞争锁,从来不需要阻塞。

写锁的解锁过程

WriteLock的unlock方法调用了sync.release(1),release方法如下:

    public final boolean release(int arg) {
    	//尝试释放锁
        if (tryRelease(arg)) {
        	//如果释放锁成功,获取头结点
            Node h = head;
            //如果队列被初始化,头不为null
            //并且头结点的waitStatus不为0,说明有后置结点(后置节点会把前一个结点状态置为SIGNAL,原因请参见上篇文章)
            if (h != null && h.waitStatus != 0)
            	//唤醒后置结点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

unparkSuccessor在上篇文章中已经解析过了,我们重点看看其中的tryRelease方法:

        protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //state减去相应的值
            int nextc = getState() - releases;
            //获取写锁状态并判断是否为0
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
            	//如果没有独占线程,则将exclusiveOwnerThread置为null
                setExclusiveOwnerThread(null);
            //重新设置state
            setState(nextc);
            return free;
        }

其实对于写锁,它的很多处理逻辑与ReentrantLock是类似的,重点还是ReadLock,我们接下来重点分析一下读锁的逻辑。

读锁的加锁过程(重点)

找到ReadLock的lock方法:

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

与独占锁不同的是,这里调用tryAcquireShared尝试获取锁。Sync中tryAcquireShared的实现:

        protected final int tryAcquireShared(int unused) {
        	//获取当前线程
            Thread current = Thread.currentThread();
            //获取state
            int c = getState();
            //获取独占锁的状态
            if (exclusiveCount(c) != 0 &&
            	//如果有线程持有独占锁并且不是当前线程,返回-1,获取锁失败
                getExclusiveOwnerThread() != current)
                return -1;
            //获取共享锁状态
            int r = sharedCount(c);
            //判断当前线程是否需要阻塞
            if (!readerShouldBlock() &&
            	//判断读锁状态是否小于最大可重入次数
                r < MAX_COUNT &&
                //CAS修改读锁state
                compareAndSetState(c, c + SHARED_UNIT)) {
                //如果CAS修改成功,判断当前读锁状态是否为0,如果是说明是第一次获取读锁
                if (r == 0) {
                	//这两个变量相当于对第一个读锁的信息缓存(下面会详细说明)
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                	//如果当前线程是第一个获取读锁的线程,则重入次数+1
                    firstReaderHoldCount++;
                } else {
                	//HoldCounter可以理解为除了第一个获取读锁线程外的读线程的信息缓存(下面详细解释)
                    HoldCounter rh = cachedHoldCounter;
                    //如果rh为null或者rh中缓存的线程id不等于当前线程id
                    if (rh == null || rh.tid != getThreadId(current))
                    	//获取当前线程的HoldCounter
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

我们看看里面调用的readerShouldBlock方法,这也是一个抽象方法,在FairSync中它的实现是调用了hasQueuedPredecessors方法(上篇文章讲过),NonfairSync中的实现是调用了一个apparentlyFirstQueuedIsExclusive方法:

    final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

这个方法顾名思义,判断第一个排队的节点是否是独占模式的。这里有一个问题,为什么获取头结点的下一个节点?如果看过我的上一篇文章应该知道:AQS中第一个排队的节点其实是第二个节点,头结点是已经获取锁的节点,就好比火车站售票窗口前,第一个人是正在买票的,第二个人开始才是在排队的。这也就印证了该方法的命名。
然后再判断当前读锁状态是否小于最大可重入次数,如果是则尝试CAS修改状态,如果修改成功,则执行下面的逻辑。
我们看到下面的逻辑中,有几个新面孔:firstReader、firstReaderHoldCount、HoldCounter、readHolds、cachedHoldCounter,这些是什么?
先思考一个问题:如果是独占锁,同一时间只有一个线程可以获取锁,state只需表示这个线程的重入次数即可;但是对于共享锁,它是允许多个读线程同时持有锁的,这时state虽然可以表示总共的重入次数,但对于每个读线程的重入次数怎么表示?此时一个state就无能为力了,这就需要有两个额外的变量,一个存储某读线程(或线程id),另一个存储该线程获取读锁的次数。
所以firstReader、firstReaderHoldCount就是表示第一个获取读锁的线程和它的重入次数。
那么问题又来了,除了第一个读线程,其它线程的这两个变量怎么定义?难不成定义N组变量,每来一个线程就用一组?显然不合理。这时候你要想到ThreadLocal这个东西,将每个读线程的信息绑定到它自己内部,这就巧妙的解决了这个问题,这便是readHolds:

        /**
         * The number of reentrant read locks held by current thread.
         * Initialized only in constructor and readObject.
         * Removed whenever a thread's read hold count drops to 0.
         */
        private transient ThreadLocalHoldCounter readHolds;

ThreadLocalHoldCounter定义:

        static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

它继承自ThreadLocal,并重写了initialValue方法,这样即使没有set过值,第一次get也可以获取一个初始化的HoldCounter对象。那么HoldCounter又是什么:

        static final class HoldCounter {
            int count = 0;
            // Use id, not reference, to avoid garbage retention
            final long tid = getThreadId(Thread.currentThread());
        }

里面就两个成员变量,count表示线程重入次数,tid表示线程id,这刚好印证了我刚才的论述。
最后还有cachedHoldCounter,它也是HoldCounter类型的对象,这个可以理解为最后一次获取读锁的线程缓存信息。

所以,下面的逻辑就是:如果r==0说明没有线程获取读锁,当前线程就是firstReader,将firstReader和firstReaderHoldCount赋值;如果firstReader有值且等于当前线程,说明firstReader又重入了一次,firstReaderHoldCount++;如果都不是,再获取cachedHoldCounter,也就是最近一次获取读锁的线程信息,如果它为null或者它缓存的线程id不是当前线程的id,则通过readHolds获取一个当前线程的HoldCounter并刷新cachedHoldCounter。所有步骤完成后,会返回1。

我们再回到acquireShared方法:

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

通过刚才的分析我们可以得知,如果tryAcquireShared返回小于0的值说明获取锁失败,返回大于0的值获取锁成功,如果获取锁失败了,则应该把当前线程放到队列中去,这就是doAcquireShared所做的事:

    private void doAcquireShared(int arg) {
    	//以共享模式创建一个Node,并至于队列尾部(上篇文章分析过)
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            //这里是一个死循环
            for (;;) {
            	//获取当前节点的前一个节点p
                final Node p = node.predecessor();
                if (p == head) {
                	//如果p是头节点,再尝试获取一次锁
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                    	//如果这次获取成功,设置新的头节点并向后传播(下面会详细解释)
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //判断是否需要挂起,并且将前一个节点状态置为SIGNAL(上篇文章解释过)
                if (shouldParkAfterFailedAcquire(p, node) &&
                	//挂起线程
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这里需要重点关注下setHeadAndPropagate方法:

    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        //将头结点设置为当前线程的node
        setHead(node);
       	
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //获取当前节点的下一个节点
            Node s = node.next;
            //如果下一个节点为null或者是共享模式的,则需要被唤醒,或者设置当前节点模式为PROPAGATE
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

doReleaseShared:

    private void doReleaseShared() {
    	//此处是一个死循环
        for (;;) {
        	//此处获取的头结点是刚刚被设置的新Node
            Node h = head;
            if (h != null && h != tail) {
            	//获取头结点的waitStatus
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                	//如果头节点的状态为SIGNAL,说明后面还有节点(因为后面的节点会把前置节点状态设为SIGNAL,上篇文章解释过)
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    	//如果CAS成功是不执行continue的,如果失败了再循环一次,执行重复逻辑
                        continue;            // loop to recheck cases
                    //唤醒后面的节点(上篇文章解释过)
                    unparkSuccessor(h);
                }
                //如果ws是0,说明没有后置节点
                else if (ws == 0 &&
               			 //利用CAS将头结点状态设为PROPAGATE,方便将状态传播给以后进来的节点
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //如果h不等于head,说明中间有其他共享节点进入了队列,头节点发生了变化,那就还需要再循环执行上述逻辑
            //如果h等于head,直接跳出循环
            if (h == head)                   // loop if head changed
                break;
        }
    }

这里有一个疑惑,为什么ws==0时,要将头节点状态设为PROPAGATE?shouldParkAfterFailedAcquire方法中有这样一段代码:

            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

看注释的意思,如果waitStatus是0或者PROPAGATE,则将waitStatus设为SIGNAL,所以我的理解就是为了让以后来的节点把自己设为SIGNAL,先把自己置为PROPAGATE。但是如果waitStatus本身为0,后来节点也会把它置为SIGNAL的,所以此处到底用意何在,我也不是很明白,希望有明白的读者指点一二。

这里再说一嘴:为什么头节点的下一个节点是共享节点要唤醒?因为共享锁本来就允许多个读线程同时拥有锁,所以头节点要唤醒后续所有连续的共享节点。当然,这里只调用了一次unparkSuccessor方法,也就是说头节点只能唤醒紧跟在后面的一个节点,如果后面还有共享节点呢?没关系,下一个节点被唤醒后,会从被阻塞的代码处开始执行,也就是doAcquireShared中调用parkAndCheckInterrupt的地方,被唤醒后又进入一次循环,循环中又会走一次调用链setHeadAndPropagate -> doReleaseShared -> unparkSuccessor,这样就一个接一个把后续共享节点唤醒了。

至此,ReadLock的加锁过程分析完毕,还是非常复杂的,个人认为读锁的加锁过程是整个ReentrantReadWriteLock最精华的部分。

读锁的解锁过程

ReadLock的unlock方法:

    public void unlock() {
        sync.releaseShared(1);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

先来看tryReleaseShared实现:

        protected final boolean tryReleaseShared(int unused) {
        	//获取当前线程
            Thread current = Thread.currentThread();
            //如果firstReader是当前线程
            if (firstReader == current) {
                if (firstReaderHoldCount == 1)
             	    //如果firstReaderHoldCount只剩一次,说明本次释放锁后firstReader将不再持有锁,故将firstReader置为null
                    firstReader = null;
                else
                	//否则firstReaderHoldCount减1
                    firstReaderHoldCount--;
            } else {
            	//如果firstReader不是当前线程,获取最后一次获取读锁的线程缓存信息
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                	//如果rh.tid不是当前线程id,则获取当前线程绑定的rh
                    rh = readHolds.get();
                //获取rh缓存的重入重入次数
                int count = rh.count;
                if (count <= 1) {
                	//如果重入次数已经<=1,直接移除缓存信息
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                //重入次数-1
                --rh.count;
            }
            for (;;) {
            	//获取当前state
                int c = getState();
                //读锁状态-1
                int nextc = c - SHARED_UNIT;
                //CAS设置新状态
                if (compareAndSetState(c, nextc))
                    //如果nextc为0,说明当前线程已完全释放读锁,返回true,否则返回false
                    return nextc == 0;
            }
        }

回到releaseShared,如果tryReleaseShared返回true,释放锁成功,则调用doReleaseShared,doReleaseShared刚刚我们分析过,只不过这次它是为了唤醒后面的一个写线程。

至此,ReentrantReadWriteLock分析完毕。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值