ReentrantReadWriteLock实现源码剖析

    ReentrantReadWriteLock提供了一个读写锁的实现,并且有着ReentrantLock相似的语义。

简介

非公平策略

    此模式下,读写锁获取的顺序是不确定的,服从于可重入的限制。不公平锁意味着持续的竞争可能会无限延迟一个或者更多的读线程或者写线程,但比公平锁有更高的吞吐量。

公平策略
    此模式下,线程竞争锁获取会使用一个大致精确的FIFO的策略。当前锁被释放之后,或者等待最长的一条写线程会获取写锁,或者如果有一组读线程等待比所有写线程都要长的时间,那么这组线程会获取读锁。
    如果写锁已被获取,或者有写线程在等待的时候,单条线程尝试获取一个公平的读锁就会被阻塞。该线程将无法获取读锁直到当前等待最长的写线程获取锁并且写锁被释放。当然,如果写线程放弃等待,让一个或者更多的读线程作为队列中最长等待线程并且此时没有写锁没被获取,那么这些读线程就会获取读锁。

    除非读锁和写锁都没有被获取(意味着没有等待线程),单条线程尝试获取一个公平的写锁就会被阻塞。(注意非阻塞方法ReadLock.tryLock和WriteLock.tryLock不会遵循这个公平策略并且在可能情况下,将忽略等待的线程,获取锁后马上返回)。

可重入性

    锁允许读线程和写线程重新获取读或者写锁。写线程可以获取读写,但反过来则不允许。在其它应用中,可重入性在写锁在调用或回调到使用读锁执行读操作的方法里变得很有用。如果读线程尝试获取写锁,则该线程永远也不会成功。

锁降级

    可重入性也允许从写锁下降到读锁,这是通过获取写锁,然后获取读锁,接着释放写锁达到。不过,从读写到写锁的升级是不可能的。

锁获取的中断

    读锁和写锁都支持在锁获取过程中的中断操作。

Condition支持
    写锁提供了Condition实现。读锁并不支持Condition实现。这是由于写锁是独占锁,读锁属于共享锁。

具体实现

(1)WriteLock获取锁实现
    我们先来分析一下WriteLock的实现。
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }


    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    使用者调用方法writeLock获取写锁对象,该函数返回writeLock成员变量,该变量在构造函数里初始化,类WriteLock是一个公有静态内部类,我们看看实现。
    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;


        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }


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


        public boolean tryLock( ) {
            return sync.tryWriteLock();
        }


        public void unlock() {
            sync.release(1);
        }


        public Condition newCondition() {
            return sync.newCondition();
        }
     }
    我们集中来看看这几个主要方法(为了方便解析,一些次要的方法没有列出来)。获取写锁的时候,调用lock方法,该方法调用内部类Sync的acquire方法,而这个内部类Sync也是AbstractQueuedSynchronizer的子类,acquire方法会调用tryAcquire尝试获取锁,获取失败进入内部FIFO等待队列,直到获取成功。
    因此我们来看看这个Sync类的tryAcquire实现。
    protected final boolean tryAcquire(int acquires) {
        Thread current = Thread.currentThread();
        int c = getState();
        int w = exclusiveCount(c);
        if (c != 0) {
            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() ||
                !compareAndSetState(c, c + acquires))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }
    函数首先调用getState获取锁状态,这里要说明一下,这个锁状态并不是和ReentrantLock一样简单的0和大于等于1表示锁的获取情况,而是把这个锁状态int值分成高半位和低半位的两个unsigned shorts值,低半位表示独占锁(写锁)的获取数(包括可重入),高半位表示共享锁(读锁)的获取数。
    如果要分别获取写锁和读锁的获取数,可以通过以下方法
    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;


    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    SHARED_UNIT表示添加一个读锁获取数时(高半位添加1),锁状态数要真正添加的数字。MAX_COUNT表示读锁和写锁的最大获取数,SHARED_SHIFT和EXCLUSIVE_MASK可以方便通过位移和掩位获取读锁和写锁数,sharedCount和exclusiveCount就是分别利用这两个数字完成了位移获取读锁获取数以及掩位获取写锁获取数。

    我们来重新看回tryAcquire的实现。调用exclusiveCount获取了独占数获取数之后,如果锁状态数c不为0,此时有可能会写锁重入,但也有可能是读锁已被获取。因此还要继续判断,如果此时独占锁数w为0,则此时共享锁数必定不为0(共享锁数高半位和独占锁数低半位组成锁状态数),也就是有读线程获取了读锁,由于写锁是独占的,因此写锁获取失败,要返回false。如果独占锁w不为0,但此时当前线程并不是获取独占锁的线程,则获取锁也失败,同样返回false。另外还需要通过把当前独占锁数与请求独占锁数相加,如果大于MAX_COUNT则要抛出异常。如果都没有问题,则可以调用setState添加当前锁状态数,表明这是一次重入的写锁,返回true。
    如果c为0的时候,此时就轮到是否公平策略的判断了,在这里调用writerShouldBlock方法让公平锁和非公平锁决定是否可以马上获取锁,如果writerShouldBlock返回true则当前获取失败,写锁要阻塞,返回false的时候就调用CAS来重新设置锁状态数,CAS成功的话就设置当前线程为获取了独占锁线程,返回true,CAS失败就返回false表示获取锁失败。
    以下分别来看看NonfairSync类(非公平策略)和FairSync类(公平策略)的writerShouldBlock函数的实现。
    //NonfairSync
    final boolean writerShouldBlock() {
        return false;
    }


    //FairSync
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    两者的实现都很简单,NonfairSync类的writerShouldBlock直接返回false表示写锁可以插队获取锁,FairSync类的实现则是调用基类AQS的hasQueuedPredecessors方法来的判断当前等待队列里是否有前继结点在等待锁,如果有则返回true,表示需要当前写线程需要被阻塞。
    我们看回WriteLock的tryLock方法,tryLock方法与上面的lock相比,最大不同便是允许外来线程插队获取锁。tryLock调用Sync类的tryWriteLock方法,看看该方法的实现。
  final boolean tryWriteLock() {
        Thread current = Thread.currentThread();
        int c = getState();
        if (c != 0) {
            int w = exclusiveCount(c);
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;
            if (w == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
        }
        if (!compareAndSetState(c, c + 1))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }
    tryWriteLock的实现与tryAcquire大体相同,最大区别就是没有了writerShouldBlock这个公平策略控制的判断。
    这样,writeLock的获取锁实现分析就结束,总体上来说,与之前的ReentrantLock的tryAcquire相比,增加了读锁被获取的情况判断,具体差别不大。接下来看看writeLock释放锁实现。

(2)WriteLock释放锁实现
    WriteLock释放锁方法unlock的实现也很简单,调用了Sync的基类AQS的release方法,按照之前的分析,release会调用tryRelease来判断是否可以释放独占锁。具体实现如下。
    protected final boolean tryRelease(int releases) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        int nextc = getState() - releases;
        boolean free = exclusiveCount(nextc) == 0;
        if (free)
            setExclusiveOwnerThread(null);
        setState(nextc);
        return free;
    }
    首先是isHeldExclusively判断,函数判断当前线程是否获取独占锁的线程相等,如果返回false,则要抛出异常IllegalMonitorStateException。然后计算出释放后的锁状态数,如果此状态数的独占锁数为0,则会把当前独占锁线程设为null,然后重新设置锁状态数。
    tryRelease的实现并没有太多要注意的地方,和ReentrantLock也没有太大区别。接下来看看ReadLock的获取锁实现。

(3)ReadLock获取锁实现
    我们来看看ReadLock类的实现。
 public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;


        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }


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


        public  boolean tryLock() {
            return sync.tryReadLock();
        }


        public  void unlock() {
            sync.releaseShared(1);
        }


        public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
    }
    同样,我们这里省略一些次要方法,集中看看获取锁和释放锁的实现。与WriteLock对比,我们注意到newCondition方法抛出异常UnsupportedOperationException,这是读锁是共享的,Condition要求锁必须在独占模式下才能使用。
    获取锁方法lock调用类Sync的acquireShared方法,这是AQS获取共享模式锁的方法,与acquire相比,该方法最大特点就是会使后继等待共享锁的结点获得共享锁。由于获取共享锁会调用tryAcquireShared来判断能否获取锁,因此我们看看该方法实现。
    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);
        if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != current.getId())
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        return fullTryAcquireShared(current);
    }
    首先,我们回忆一下基类AQS要求函数返回负值代表获取读锁失败;返回0表示本次获取读锁成功,但不允许后继读线程获取,也就是不会唤醒等待队列里后继共享结点;返回正值表示本次获取读锁成功,同时允许后继读线程获取读结点,因此会唤醒等待队列里后继共享结点。
    重新看看tryAcquireShared函数,做了以下事情:
    1、调用exclusiveCount获取独占锁数,如果不为0,则此时有写线程获取了独占锁,此时如果当前线程也不是获取独占锁的线程,则读锁获取失败,返回-1表示获取读锁失败;
    2、此状态下,线程便有获取读锁。于是调用readerShouldBlock方法,由公平策略判断是否应该入队列,如果返回false表示可以插队获取,然后如果读锁数小于MAX_COUNT,另外CAS又成功把当前锁状态数的共享锁数+1(再次提醒,共享锁数在高半位),然后就是一个利用ThreadLocal类分别记录每条读线程获取读锁的次数,此处待下面详细分析。
    3、如果步骤2失败,有可能出现多条读线程获取读锁,或者写线程尝试获取写锁的过程,因此调用fullTryAcquireShared利用自旋确保并发修改状态。

    我们先来分析利用ThreadLocal类记录每条读线程获取读锁的次数的代码段。为了实现这个功能,并且考虑到无竞争条件下,以及读锁重入的情况,进行了对应的优化加速读取。首先,此处涉及的成员变量有以下几个:
    private transient ThreadLocalHoldCounter readHolds;
    //针对读锁重入的优化
    private transient HoldCounter cachedHoldCounter;
    //针对无竞争读锁状态下优化
    private transient Thread firstReader = null;
    private transient int firstReaderHoldCount;
    第一个readHolds就是主要每条线程的本地线程变量,其中ThreadLocalHoldCounter实现如下:
    static final class HoldCounter {
        int count = 0;
        final long tid = Thread.currentThread().getId();
    }


    static final class ThreadLocalHoldCounter 
        extends ThreadLocal {
        public HoldCounter initialValue() {
            return new HoldCounter();
        }
    }
    ThreadLocal类实现了这样一个功能:每条线程里都保留对同一个ThreadLocal类的引用,但对每条线程的ThreadLocal类进行读写操作都不会影响其它线程,看起来像每条线程都拥有各自的ThreadLocal类一样。
    具体实现主要是在每条线程里创建不同的存储类,然后在该存储类里保留对同一个ThreadLocal类引用,以其hash作为key,并且同时把对应的ThreadLocal类的value类(在本实现中是HoldCounter)存储到对应位置上,这样调用ThreadLocal类来获取value类的值时,就会利用ThreadLocal类的hash作为key搜索对应的value,然后返回,这样就实现类同一个ThreadLocal类对于不同线程返回不同的value类。

    我们重新看回实现,为了尽量避免ThreadLocal类的查表操作,首先针对于读锁重入的情况,利用了cachedHoldcounter进行优化。cachedHoldCounter是上一次成功获取读锁的线程的ThreadLocal类的value类,在每次设置于当前线程的读锁数之后,利用cached记录,就可以避免如果下次相同线程再次重入获取读锁时的ThreadLoacal类查表操作。
    firstReader是第一个获取读锁的线程,firstReaderHoldCount便是这条线程的读锁获取数。对于没有发生竞争的获取读锁操作(只有单条线程在获取读锁),这两个变量便可以免去ThreadLocal的查表操作。
    了解完变量的大致作用之后,我们再来看回之前tryAcquiredShared代码的实现每条线程读锁数就会觉得很简单,其实现如下:
  if (r == 0) {
        firstReader = current;
        firstReaderHoldCount = 1;
    } else if (firstReader == current) {
        firstReaderHoldCount++;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            cachedHoldCounter = rh = readHolds.get();
        else if (rh.count == 0)
            readHolds.set(rh);
        rh.count++;
    }
    当读锁获取数r为0,意味着这是第一条获取读锁的线程,于是就记录当前线程为firstReader,并且把firstReaderHoldCount=1;然后如果r不为0,但firstReader等于当前线程,则表示第一条获取读锁线程重入,于是把firstReaderHoldCount加上一;如果以上都不是,则表示这是另外一条线程尝试获取读锁,如果cachedHoldCounter为null或者cachedHoldCounter表示的线程id不是和当前线程id相等,则表示cache失效,需要调用readHolds.get()获取当前线程的HoldCounter(如果不存在则会创建一个新的HoldCounter),另外如果cache是当前线程的HoldCounter,但此时count为0,则需要重新把cache设置回去,因为后面的release释放的时候会remove。这样确保cache是当前线程的HoldCounter之后,把当前cahce的count值加一。

    我们再看看当tryAcquireShared遇到并发导致修改失败时,调用fullTryAcquireShared的实现:
    final int fullTryAcquireShared(Thread current) {
        HoldCounter rh = null;
        for (;;) {
            int c = getState();
            if (exclusiveCount(c) != 0) {
                if (getExclusiveOwnerThread() != current)
                    return -1;
                // else we hold the exclusive lock; blocking here
                // would cause deadlock.
            } else if (readerShouldBlock()) {
                // Make sure we're not acquiring read lock reentrantly
                if (firstReader == current) {
                    // assert firstReaderHoldCount > 0;
                } else {
                    if (rh == null) {
                        rh = cachedHoldCounter;
                        if (rh == null || rh.tid != current.getId()) {
                            rh = readHolds.get();
                            if (rh.count == 0)
                                readHolds.remove();
                        }
                    }
                    if (rh.count == 0)
                        return -1;
                }
            }
            if (sharedCount(c) == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            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 != current.getId())
                        rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                    cachedHoldCounter = rh; // cache for release
                }
                return 1;
            }
        }
    }
    函数实现看上去有点复杂,但事实上只是tryAcquiredShared的逻辑基础上,增加了循环重试以及延迟的读锁数记录的逻辑。
    循环做里以下事情
    1、首先判断独占锁数是否为0,如果不为0,并且当前线程不是获取独占锁线程,则返回负值1表示获取读锁失败。
    2、如果独占锁数为0,则调用readerShouldBlock判断当前读线程是否需要入队。如果返回true,则要继续进行判断,因为如果是已经获得读锁的重入情况,则仍然必须让此线程能够获得锁,因此这里先进行firstReader判断,如果失败,则继续从cache获取HoldCounter,确保rh是当前线程的HoldCounter以后,如果rh.count为0,则表示不是重入情况,马上返回负值1表示获取读锁失败。
    3、接着,如果读锁数是否等于MAX_COUNT,如果是则要抛出异常。
    4、到达这里,就可以利用CAS更改当前锁状态数,如果成功来则是和tryAcquireShared类似的更新当前线程的获取读锁数,这里就不重复赘述。成功更改之后,返回正值1表示获取读锁成功,并且后继读线程可以继续获取。如果CAS失败,则必须重复以上步骤,保证并发操作能够成功修改。

    接着来看看readerShouldBlock的公平策略和非公平策略的实现:
    //NonfairSync
    final boolean readerShouldBlock() {
        return apparentlyFirstQueuedIsExclusive();
    }


    //FairSync
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
    非公平策略调用来AQS基类的一个方法,apparentlyFirstQueuedIsExclusive,我们来看看实现:
final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }
    此函数对于如果存在第一个在等待队列中的结点,是独占模式结点,则返回true。逻辑判断也很简单,直接获取头结点的后继结点判断是否独占模式即可。
    非公平策略判断读锁能否插队的时候,之所以要考虑到等待队列,是因为考虑到避免在等待中的写线程会无穷尽的等待,因为如果不设限的话,读线程可以在读锁已经被获取的情况下,无限制获取,这样在等待队列中的写线程就会被超长时间等待。
    公平策略实现很简单,和之前的writerShouldBlock实现一样,同样返回hasQueuedPredecessors即可。

    至于读锁的tryLock实现,由于与lock实现大体一样,只是去掉了readerShouldBlock的判断。
    接下来分析一下ReadLock的释放锁实现。

(3)ReadLock释放锁实现
    类似地,读锁的unlock只是仅仅调用了releaseShared。
    protected final boolean tryReleaseShared(int unused) {
        Thread current = Thread.currentThread();
        if (firstReader == current) {
            if (firstReaderHoldCount == 1)
                firstReader = null;
            else
                firstReaderHoldCount--;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                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;
        }
    }
    函数实现原理分为两大步骤:
    1、首先尝试把当前线程的读锁数减去一。首先会尝试判断是否firstReader,如果是并且firstReaderHoldCount刚好为1,则firstReader变为null,如果不为1,则减去1。如果不是firstReader线程,则尝试cache读取,然后判断当前线程的读锁数,如果少于等于1,则会remove掉当前线程的HoldCounter,但如果发现count的值少于等于0,则要抛出IllegalMonitorStateException表示当前线程没有获取到读锁但尝试释放读锁。
    2、然后,一个自循CAS更改当前的锁状态数为当前读锁数减去一。

    两个步骤里步骤1不需要自循,是因为所做变量更改都是基于ThreadLocal的,并不会影响其它线程,步骤2由于要CAS修改锁状态数,因此需要自循确保成功。

总结

    到此,ReentrantReadWriteLock的框架已经解析完成。当然,如果感兴趣的话,还有少部分关于锁状态的辅助函数,可以自行理解。
    整个读写锁的实现里,tryAcquireShared和tryReleaseShared的实现是重点,因为要考虑多条读线程并发修改读写,并且还要考虑到写锁的独占问题,需要利用到自循确保并发中的同步。另外,对于读锁数的记录的实现,也针对几种常见情况进行了优化,可见考虑是相当周到的。

你可能感兴趣的:(Java)