Java并发编程之ReentrantReadWriteLock(五)

170 阅读6分钟

前言

前面几篇文章介绍了读锁的加锁和解锁流程,内容较多比较复杂,写锁的流程相对就简单一些了,根据源码来看一下具体流程。

加锁

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

前面两步和ReentrantLock一样,通过lock方法进行加锁,然后调用AQSacquire方法,先尝试去获取锁,如果成功了就结束,失败了就创建一个节点添加到阻塞队列进行等待,被唤醒之后继续尝试获取锁。

// ReentrantReadWriteLock.Sync
protected final boolean tryAcquire(int acquires) {
    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)
        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;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

首先获取锁的总数量和写锁的数量,如果锁的总数量不为0表示有读锁或者写锁

  • w == 0:写锁数量为0,说明此时有读锁,因为读写互斥,不能加写锁返回false
  • current != getExclusiveOwnerThread():有写锁而且持有锁的线程不是当前线程,因为写写互斥,所以加写锁失败返回false
  • 上述两个条件都不满足的话,表示当前线程已经持有了写锁,现在继续尝试加写锁,也就是发生了写锁重入。如果写锁数量不会达到最大值,锁数量就加1,因为写锁使用的是state的低16位,所以直接对state1就可以了。

如果c == 0,表示此刻没有线程持有锁,但是阻塞队列是否有线程不确定,所以这里先调用了writerShouldBlock方法,在公平锁和非公平锁中分别有不同的实现。

// ReentrantReadWriteLock.NonfairSync 
final boolean writerShouldBlock() {
    return false; 
}

在非公平锁的实现中直接返回了false,表示不管阻塞队列中是否有等待的线程,只要此时没有加锁当前线程就可以尝试去获取锁,所以是非公平的。

// ReentrantReadWriteLock.FairSync 
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
// AQS
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

而在公平锁的实现中,先去队列中查询是否有其他线程的节点正在等待,如果有的话当前线程就没有资格获取锁,必须遵循先来后到的准则到队列中去等待;如果没有其他等待的线程,那当前线程就可以去获取锁了,这就是公平的体现。

获取锁成功之后,通过setExclusiveOwnerThread方法把写锁的持有者修改为当前线程,然后返回true就结束了。

加锁失败

如果获取锁失败了,会继续执行第二个条件判断acquireQueued(addWaiter(Node.EXCLUSIVE), arg),这里分为两步:

  • addWaiter方法是把当前线程封装成Node节点添加到阻塞队列中;
  • acquireQueued方法继续尝试去获取锁,如果失败了就通过park进行等待。
添加到阻塞队列
// AQS
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}
// AQS
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

addWaiter方法首先把当前线程封装成Node节点,如果此时队列中有尾节点tail,就直接把当前节点添加到尾节点的后面,成为新的尾节点。 如果此时没有尾节点tail,说明队列还没有初始化,就调用enq方法先初始化队列,创建一个空节点(不关联线程)作为头结点head和尾节点tail,然后再把当前线程的节点添加到尾节点tail的后面,成为新的尾节点。

队列中尝试获取锁
// AQS
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
// AQS
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
// AQS
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

acquireQueued方法通过无限for循环来实现不断地获取锁-失败阻塞/成功退出。

  • 首先获取判断前驱节点是否是头节点head
    • 如果是的话表示当前线程有资格获取锁,就继续调用tryAcquire方法去获取锁
      • 如果获取锁成功了,就把当前节点修改为头节点,以便能够唤醒后继节点。
      • 如果获取锁失败了,向下执行阻塞流程。
    • 不是的话表示当前线程没有资格获取锁,向下执行阻塞流程
  • 进入阻塞流程,判断是否应该阻塞
    • 如果前驱节点的waitStatusNode.SIGNAL,表示具有唤醒后继节点的能力,当前节点就可以进行阻塞
    • 如果前驱节点的waitStatus是大于0,表示该节点被取消了,就继续向前找直到找到一个没被取消的节点,返回false继续尝试获取一次锁;
    • 如果前驱节点的waitStatus0-3waitStatus只有四个取值),就尝试把它修改为Node.SIGNAL-1),返回false继续尝试获取一次锁;
    • 上面三种情况可以总结为,只要当前线程一直获取锁失败,就一定能得到一个具备唤醒能力的前驱节点,然后让当前线程去等待,最终一定能返回true
  • 如果可以阻塞,就调用LockSupport.park(this)让当前线程进行等待,直到被其他线程唤醒或产生了中断。

释放锁

// ReentrantReadWriteLock.WriteLock
public void unlock() {
    sync.release(1);
}
// AQS
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}    

释放锁执行的是AQSrelease方法,整体上分为两个步骤:

  • 首先尝试释放锁,失败了就返回false,成功了继续执行第二步;
  • 头节点如果存在并且具备唤醒能力,就去唤醒后继节点。
尝试去释放锁
// ReentrantReadWriteLock.Sync
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;
}

首先判断当前线程是否是持有写锁的线程,如果不是直接抛出异常。否则对锁数量减1,然后判断写锁数量是否变成0,如果是0,表示释放了所有的写锁,就把写锁持有者置为null;如果不是0,表示有锁重入,就只更新state的值。

唤醒后继节点
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);
}

这个方法传入的参数是头节点head,首先尝试把headwaitStatus修改为0,是为了防止多个线程同时进入唤醒流程。

获取后继节点s,如果snull获取标记为被取消,就从队列中倒序(个人理解为因为下一个节点是null或被取消,可能没法通过它来继续向后查找)查找符合条件并且离head节点最近的节点,如果存在这个节点,就调用LockSupport.unpark(s.thread)来把这个节点的线程唤醒,这里唤醒的线程是加锁失败后最终调用LockSupport.park()阻塞的线程。