大厂面试必会AQS(2)——从ReentrantReadWriteLock源码认识AQS

读写锁(ReadWriteLock)概述

看过我上一博文《大厂面试必会AQS(1)——从ReentrantLock源码认识AQS》了解到可重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低。所以就提供了读写锁。

读写锁维护了一对锁,一个读锁一个写锁,通过分离读锁和写锁来提高并发能力
可以看到ReadWriteLock 接口只提供了读锁和写锁获取这两个方法,本文通过ReentrantReadWriteLock来认识读写锁

public interface ReadWriteLock {
 Lock readLock();
 Lock writeLock();
}

说到底,读写锁的代码不复杂,难点终究还是AQS,接下来接着上一文章研究一下AQS的共享式同步状态的获取与释放,我们知道AQS通过int变量state来表示锁被一个线程重复获取的次数,那么读写锁的实现关键即是怎么在一个int变量上维护多种状态——按位切割,高16位表示读,低16位表示写,通过位运算获取读锁或者写锁的状态,这里有一个逻辑会用在源码中:如果State不等于0,而写状态等于0(写状态是低16位,只需将state&0X0000FFFF抹去高16位即可得到),那么读状态大于0(读状态为高16位,State无符号位移16位即可获取),即读锁已经被获得。

二 读锁的获取与释放

1.读锁获取

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

读锁的加锁调用acquireShared,仍然是首先尝试获取锁(以共享式)
源码如下

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    //写锁被别的线程持有返回-1
    if (exclusiveCount(c) != 0 &&
    //保证获得写锁的线程可以再次获得读锁,避免死锁
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    //是否需要阻塞
    if (!readerShouldBlock() &&
    //持有线程小于最大数(65535)
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {// 当前线程为第一个读线程,表示第一个读锁线程重入
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
              // 如果最后一个线程计数器是 null 或者不是当前线程,那么就新建一个 HoldCounter 对象
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();   // 给当前线程新建一个 HoldCounter
            else if (rh.count == 0)
             // 如果不是 null,最后一个获取读锁的线程是当前线程,且 count 是 0,就将上个线程的 HoldCounter 覆盖本地的。
                readHolds.set(rh);
                //计数器加1
            rh.count++;
        }
        return 1;
    }    
    // 死循环获取读锁。包含锁降级策略。
    return fullTryAcquireShared(current);
}

注意:更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数,这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。

分析:相比ReentrantLock中看到的tryAcquireShared方法,出现了几个陌生的方法:
1.exclusiveCount:获得写状态

//c即为state c & EXCLUSIVE_MASK 即可获得低16位的状态,即写状态
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
//将1左移16位-1得到 0X0000FFFF
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

static final int SHARED_SHIFT   = 16;

2.sharedCount: 获得读状态

//c 无符号位移16位获得高16位  即读状态
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

static final int SHARED_SHIFT   = 16;

3.readerShouldBlock读是否应该被阻塞
公平锁会考虑前面是不是有已经等待的线程(不管是获取读锁还是写锁)

final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

非公平锁则只关注第一个等待节点是不是获取写锁

final boolean readerShouldBlock() {
    /* As a heuristic to avoid indefinite writer starvation,
     * block if the thread that momentarily appears to be head
     * of queue, if one exists, is a waiting writer.  This is
     * only a probabilistic effect since a new reader will not
     * block if there is a waiting writer behind other enabled
     * readers that have not yet drained from the queue.
     */
    return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

4.cachedHoldCounter
保存的是最后一个成功获取到读锁的线程,每当有新的线程获取到读锁,这个变量都会更新。这个变量的目的是:当最后一个获取读锁的线程重复获取读锁,或者释放读锁,就会直接使用这个变量,速度更快,相当于缓存。

/**
 * The hold count of the last thread to successfully acquire
 * readLock. 
 */
private transient HoldCounter cachedHoldCounter;

5.firstReader

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

firstReader是获取读锁的第一个线程。如果只有一个线程获取读锁,很明显,使用这样一个变量速度更快。firstReaderHoldCount是firstReader的计数器

6.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;
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

最开始时readHolds也没有值,readHolds.get()方法会导致ThreadLocalHoldCounter类initialValue方法执行,也就会返回count=0以及tid为当前线程tid的属性。当下次该线程再次获取读锁时将会执行( **当前线程曾经获取到读锁并释放了,再次获取读锁,且期间没有任何其他线程获取 读锁!!! **)

else if (rh.count == 0)
    readHolds.set(rh);

如果有几个线程同时竞争,可能导致compareAndSetState失败进入fullTryAcquireShared自旋获取到锁(CAS 设置失败,或者队列有等待的线程(公平锁)情况下)

final int fullTryAcquireShared(Thread current) {
    /*
     * This code is in part redundant with that in
     * tryAcquireShared but is simpler overall by not
     * complicating tryAcquireShared with interactions between
     * retries and lazily reading hold counts.
     */
    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 != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
          // 如果读锁次数达到 65535 ,抛出异常
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
               // 尝试对 state 加 65536, 也就是设置读锁,实际就是对高16位加一。
        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;
        }
    }
}

当tryAcquireShared方法尝试失败后将进入doAcquireShared获取读锁,下面看下doAcquireShared该方法内部到底做了些什么:

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);
    }
}

可以看到,如果前驱节点是头结点,则尝试获取读锁,成功的话就唤醒其它读锁

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    /*
     * Try to signal next queued node if:
     *   Propagation was indicated by caller,
     *     or was recorded (as h.waitStatus either before
     *     or after setHead) by a previous operation
     *     (note: this uses sign-check of waitStatus because
     *      PROPAGATE status may transition to SIGNAL.)
     * and
     *   The next node is waiting in shared mode,
     *     or we don't know, because it appears null
     *
     * The conservatism in both of these checks may cause
     * unnecessary wake-ups, but only when there are multiple
     * racing acquires/releases, so most need signals now or soon
     * anyway.
     */
     /*
         * propagate>0,说明还有资源
         * h.waitStatus < 0 h的waitStatus要么是-1,要么是-3,
         * 满足以上2个条件之一,就能进入doReleaseShared(),释放后继节点了
         */
  if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

private void doReleaseShared() {
    /*
     *会先判断head节点的waitStatus,doReleaseShared只被2个方法调用,1个是setHeadAndPropagate,1个是releaseShared,我只讨论setHeadAndPropagate方法调用的情况
     *所以进来的head的waitStatus也只有3种可能,0,-1,-3.
     *如果是0的情况说明还有资源,但是后继节点为空,则会把waitStatus设置成-3,便结束这个方法.当节点释放锁的时候,会执行releaseShared,如果这个时候waitStatus还是0,说明后面还是没节点,如果后面有节点,则必定会更改waitStatus为-1
     *如果是-1,则会更改成0并且唤醒后继节点
     */
    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;
    }
}

2.读锁释放

读锁释放相对简单

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 释放成功后,如果是 0,表示读锁和写锁都空闲,则可以唤醒后面的等待线程
        doReleaseShared();
        return true;
    }
    return false;
}
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {//当前线程是第一个读线程
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)//进入锁的次数为1,把读线程置null(后续代码会将读状态再减1<<16)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;//最后一个获得读锁的线程id及计数器
        if (rh == null || rh.tid != getThreadId(current))//最后一个获得读锁的线程不是当前线程,新建一个HoldCounter 
            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;//1 << 16
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
             // 修改成功后,如果是 0,表示读锁和写锁都空闲,则可以唤醒后面的等待线程
            return nextc == 0;
    }
}

tryReleaseShared方法将读状态置0,doReleaseShared方法负责唤醒后继节点,与setHeadAndPropagate方法中被调用的逻辑一致

三 写锁的获取与释放

看下WriteLock类中的lock和unlock方法:
1.加锁

public void lock() {
    sync.acquire(1);
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

可以看到就是调用的独占式同步状态的获取,因此真实的实现就是Sync的 tryAcquire

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();//获取状态
    int w = exclusiveCount(c);//写状态
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        // 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
        // 如果写锁状态不为0且写锁没有被当前线程持有返回false
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)//如果写锁已经被获取,若写锁获得次数超过最大值抛出异常
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
         //更新状态
          //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
        setState(c + acquires);
        return true;
    }
    //到这里说明此时c=0,读锁和写锁都没有被获取
    //写锁获得是否应该阻塞,公平锁需判断有没有前驱节点,非公平锁直接返回false
    if (writerShouldBlock() ||
    //更新状态
        !compareAndSetState(c, c + acquires))
        return false;
        //获得写锁成功并设置当前线程为拥有者
    setExclusiveOwnerThread(current);
    return true;
}

过程较为简单,只需注意写锁的获得必须是读锁没有被占用

2.写锁的释放

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
//先调用tryRelease方法
    if (tryRelease(arg)) {
        Node h = head;
        //唤醒后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
protected final boolean tryRelease(int releases) {
//当前线程是否为写锁拥有线程,不是则抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
        //写锁的新线程数
    int nextc = getState() - releases;
    //写锁重入数减一后是否为0
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
   // 写锁拥有的线程置null
        setExclusiveOwnerThread(null);
   //设置写锁的新线程数
    //不管独占模式是否被释放,更新独占重入数
    setState(nextc);
    return free;
}

可以看到首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。

四 锁降级

什么是锁降级?

锁重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。注意:先释放写锁再获取读锁不是锁降级!!!

代码体现:在tryAcquireShared,fullTryAcquireShared等方法中存在如下代码

if (exclusiveCount(c) != 0) {
    if (getExclusiveOwnerThread() != current)
        return -1;

分析:当写锁被持有时,如果持有该锁的线程不是当前线程,就返回 “获取锁失败”,反之就会继续获取读锁。称之为锁降级。

锁降级的好处

线程1先释放写锁,再获得读锁的时间差里,数据存在被其他线程修改的可能,如果先获得读锁在释放写锁,那么其他线程想要获得写锁必须等待线程1释放读锁,另外锁降级获得读锁的过程是不需要参与资源竞争。

有没有锁升级?

答案是没有,因为写锁是共享的,线程1在读的同时,别的线程也在读取数据,而且在写锁的获取源码中,若当前存在读锁,则获取失败。

你可能感兴趣的:(JAVA并发高级)