JUC并发基石之AQS源码解析--独占锁的释放

JUC并发基石之AQS源码解析–独占锁的获取
上一篇文章中,我们分析了独占锁的获取操作, 这篇文章我们来看看独占锁的释放,释放锁的逻辑相对简单,我们来看源码:

public final boolean release(int arg) {
        // 由子类来实现具体的逻辑   
        if (tryRelease(arg)) {
            Node h = head;
            // 头节点不为空 并且waitStates不为0
            if (h != null && h.waitStatus != 0)
                // 唤醒后面的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

独占锁的涉及到两个函数的调用:

1.tryRelease(arg) ,该方法由AQS的子类来实现释放锁的具体逻辑
2.unparkSuccessor(h) ,唤醒后继线程

我们以ReentrantLock为例看看tryRelease(arg)的实现:

tryRelease(arg)

protected final boolean tryRelease(int releases) {
            // 首先将当前持有锁的线程个数减1(因为是由sync.release(1)调用的, releases的值为1)
            int c = getState() - releases;
            // 当前线程不是持有锁的线程,抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // c=0说明锁释放了,那么把占用锁的线程设置为空
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 设置状态
            setState(c);
            return free;
        }

我们可以看到,释放锁就是对State进行操作,不过因为是可重入锁,所以只有当state=0的时候,才是真正的释放锁。

假如真正释放锁之后,我们来看看唤醒线程的逻辑:

unparkSuccessor()

private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        // 如果head节点的ws比0小, CAS操作将它设为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 1.正常情况下,后继节点不为空,而且waitStatus <= 0
        //   即没有取消获取锁,那么就去唤醒后继节点
        // 2. 如果后继节点取消获取锁,那么从尾节点开始找起
        //    找排在最前面的等待获取锁的节点
        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);
    }

我们可以看到后继节点不为空,而且waitStatus <= 0,即没有取消获取锁,那么就去唤醒后继节点。
如果后继节点取消获取锁,那么从尾节点开始找,排在最前面的等待获取锁的节点

这里有一个的问题就是, 为什么要从尾节点开始逆向查找, 而不是直接从head节点往后正向查找, 这样不是更快么?

其实是跟入队的操作有关:

private Node addWaiter(Node mode) {
        // 首先创建一个Node节点,传入当前的线程以及Node.EXCLUSIVE
        Node node = new Node(Thread.currentThread(), mode);
       
        Node pred = tail;
        // 如果队列不为空,因为队列是懒加载的,所以可能为空
        if (pred != null) {
            // 1. 把当前节点的pre节点设置为尾节点
            node.prev = pred;
            // 2. 然后CAS设置当前节点为尾节点
            if (compareAndSetTail(pred, node)) {
                // 3. CAS成功就把之前尾节点的next节点设为当前节点
                pred.next = node;
                return node;
            }
        }
        // 执行到这里, 只有两种情况:
        // 1. 队列为空
        // 2. 其他线程在当前线程入队的过程中率先入队,导致尾节点的值改变,所以CAS操作失败
        enq(node);
        return node;
    }

因为这个阻塞队列是双向链表,所以入队的节点前进行第一步和第二步操作,把尾节点设为自己的pre节点,并且CAS设置自己为尾节点,设置成功了,再把之前尾节点的next节点设为当前节点。

所以如果我们unparkSuccessor从头开始遍历,如果CAS设置尾节点成功,但是pred.next的值还没有被设置成node,从前往后遍历的话,有可能遍历不到刚加入的尾节点的。

如果从后往前遍历的话,因为尾节点此时已经设置完成,node.prev = pred操作也被执行过了,那么新加的尾节点就可以遍历到了,并且可以通过它一直往前找。

如果找到了还在等待锁的节点,则唤醒它,也就是调用LockSupport.unpark(s.thread)。

其实也就是唤醒我们上一篇说的挂起的线程:

private final boolean parkAndCheckInterrupt() {
        // 挂起的线程会在这里被唤醒
        LockSupport.park(this); 
        return Thread.interrupted();
    }

唤醒的线程会在acquireQueued()方法里面继续尝试获取锁:

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;
            }
            // 在这里被唤醒之后,又会在for循环里面继续尝试获取锁
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这样,释放锁就和获取锁结合在一起了。

不过这里我们再关注一点,也就是中断。

上一篇我说过:LockSupport挂起线程,等待被唤醒,有两种情况会被唤醒

  1. 获得锁的线程在释放锁的时候,会调用release()方法,这个方法会调用LockSupport.unpark()唤醒挂起线程;被唤醒后,在acquireQueued()循环尝试获取锁。
  2. 当前线程被中断了,那么也会唤醒,唤醒后调用Thread.interrupted()方法返回true,同时中断标志位会被清空,不过在外层会把interrupted 设为 true。

我们可以看到,如果线程是被中断唤醒的,那么调用Thread.interrupted()会返回true,同时中断标志位会被清空。因为parkAndCheckInterrupt()返回true,所以interrupted 设置为true。

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;

之后如果获取锁,那么acquireQueued()返回的是interrupted,也就是true,我们再回到acquireQueued()方法调用的地方:

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

如果acquireQueued的返回值为true, 我们将执行 selfInterrupt():

static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

它其实就是中断了一下线程。

其实做了这么多,都是因为获取锁的时候是不响应中断的!

所以当在LockSupport.park(this)处被唤醒,可能是因为当前线程在等待中被中断了,因此我们通过Thread.interrupted()方法检查了当前线程的中断标志,并将它记录下来,当它抢到锁了,返回acquire方法后,如果发现当前线程曾经被中断过,就再中断自己一次,将这个中断补上。

你可能感兴趣的:(java并发,源码解析,多线程,java,多线程)