AQS源码简单理解三:解锁

上一篇博客写了AQS加锁过程,文章后面只是浅谈了下阻塞线程被唤醒后该怎么执行。这篇博客主要描述解锁过程,以及再深入看下阻塞线程被唤醒后怎么走的。

解锁过程

reentrantLock.unlock() 方法走到 release(int arg) 。解锁的核心代码如下:

// AbstractQueuedSynchronizer.class
public final boolean release(int arg) {
    // 是否解锁成功? 
    if (tryRelease(arg)) {
        Node h = head;
        // 此时头结点的 waitStatus 是 -1
        if (h != null && h.waitStatus != 0)
            // unpark 线程
            unparkSuccessor(h);
        // 解锁成功。
        return true;
    }
    // 解锁失败。
    return false;
}

前面的博客提到过,ReentrantLock 加锁就是将它里面的同步器的 state 字段改变(原本是0,改成大于0的数字),同样的,解锁就是将 state 字段改回原值。如果解锁成功了,ReentrantLock 就变成了自由的,允许再被别的线程加锁。但是,那些等待的线程现在还在 ReentrantLock 的同步器中阻塞着呢。所以接下来还要做的事情就是唤醒其中一个沉睡的线程。

所以,解锁过程分两步走:(1)解锁;(2)唤醒一个沉睡的线程。

接下来看下代码是怎么走的:

解锁

// ReentrantLock.class
// releases == 1
protected final boolean tryRelease(int releases) {
    // 考虑到锁重入,解锁一次,state - 1,
    int c = getState() - releases;
    // 判断当前线程是不是所的持有者? 是的。
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是不是自由的。
    boolean free = false;
    // 当且仅当 state 的值被减到 0 时,解锁成功。
    if (c == 0) {
        free = true;
        // 因为自由了,当前锁没有持有者。
        setExclusiveOwnerThread(null);
    }
    // 设置 state == 0
    setState(c);
    return free;
}

这个过程还是很简单的,如果 state 能更新成 0,表示解锁成功,再将锁持有者置空就可以了。

唤醒一个沉睡的线程

唤醒哪一个沉睡的线程呢?因为公平锁先进先出,外加队头是空的,所以应该唤醒第二个节点中的线程。那如果第二个节点中的线程被取消了呢?

所以,唤醒的规则是:唤醒队列中离队头最近的并且没有被取消的线程。

基于这个思路,下面的代码就容易理解了。s最终要指向那个满足唤醒条件的节点,它起初先指向第二个节点(node1),再判断 node1 是不是满足唤醒条件,如果满足,那直接到方法底部唤醒。如果不满足,令 s == null ,再通过 for 循环找到满足唤醒条件的节点。

// node == head
private void unparkSuccessor(Node node) {
    // ws == -1
    int ws = node.waitStatus;
    if (ws < 0)
        // 设置队头节点的 waitStatus == 0
        compareAndSetWaitStatus(node, ws, 0);    
    // s 先指向队列中的第二个节点。
    Node s = node.next;
    // 如果第二个节点是空,或者被取消了。
    if (s == null || s.waitStatus > 0) {
        s = null;
        // t 从队尾开始,朝着队头的方向,扫一遍队列中的所有节点。
        for (Node t = tail; t != null && t != node; t = t.prev)
            // s 指向离队头最近的且没有被取消的线程节点。
            if (t.waitStatus <= 0)
                s = t;
    }
    // unpark s 指向的节点中的线程。
    if (s != null)
        LockSupport.unpark(s.thread);
}

两个小点

(1)if 中判断了s == null,会有这种场景吗?

有的。比如队列中有 T1、T2、T3 线程,过段时间后,当T3执行完加锁代码并且解锁时,同步器队列中只剩了队头节点,s == head.next == null

(2)队列中被取消的线程节点怎么处理?

假设队列中有 node0(head)、node1、node2、node3 四个节点,node1 被取消了。当线程释放锁后,node2 中的线程会被 unpark,在释放锁阶段,仅此而已。不会对被取消的线程做任何操作。(后面会做的)

阻塞线程被唤醒后怎么办

上篇博客提高过,线程在哪里被 park 的,unpark 后就从那里继续执行。继续上面的那个场景,队列中有 node0(head)、node1、node2、node3 四个节点,node1 被取消了,node2 unpark。

T2 从 parkAndCheckInterrupt() 中开始执行,外围是个 for 死循环。

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()) // park
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

T2 循环了两次,看下具体细节:

第一次循环:

//【head,node1,node2,node3】
// node2 从 parkAndCheckInterrupt()中开始执行。
for (;;) {
    // p == node1
    final Node p = node.predecessor();
    // p 不是 head,node2 拿锁失败。
    if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return interrupted;
    }
    // 判断要不要 park?
    if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt()) 
        interrupted = true;
}
// pred == node1, node == node2
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // ws == 1
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        // 循环执行的结果是:队列变成了【head,node2,node3】
        // 被取消的线程 T1 所在的节点 node1 被移出了队列。
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 自旋,不 park。
    return false;
}

第二次循环:

//  第二次循环
//【head,node2,node3】
for (;;) {
    // p == head。
    final Node p = node.predecessor();
    // 尝试拿锁,而且会拿锁成功。
    if (p == head && tryAcquire(arg)) {
        // 设置 node2 为队头。
        setHead(node);
        // 准备删除原队头。
        p.next = null; // help GC
        failed = false;
        return interrupted;
    }
    if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt()) 
        interrupted = true;
}

两个小点

(1)在加锁和解锁过程中,只在一个地方移出被取消的线程。那就是在shouldParkAfterFailedAcquire(Node pred, Node node) 方法中。

(2)被唤醒的线程尝试拿锁时,如果拿不到会自旋一次。上面的例子中分析的是因为 node1 是被取消的线程,所以自旋了一次。如果队列直接是【head,node2,node3】,node2 没有拿到锁,执行 shouldParkAfterFailedAcquire(head, node2) , 因为 head 的 waitStatus 字段的值是 0,所以依旧还会再自旋一次。

与打断有关的小问题

(1)处于阻塞的线程节点被唤醒了,它会有拿不到锁的场景出现吗?既然依旧拿不到锁,为什么还要唤醒呢?

这个题目描述的不太准确,unpark 是“唤醒” park中的线程,此时被唤醒的线程是能拿到锁的。但是 park 的线程可能会被 interrupt() 打断,从而继续向下执行。被打断的线程开始尝试拿锁,(之前的线程还没释放锁)会失败。

(2)如果最终结果是 park,被唤醒的线程会自旋一次,被打断的线程会自旋几次?

可能会自旋0次,1次。

0次:

队列【head,node1,node2,node3】,队列中节点都处在正常的 park 状态,加锁代码块正在被执行着,node2 被打断。node2 尝试拿锁失败,又因为node1的waitStatus 字段是 -1,所以判定 node2 直接进入 park。

1次:

队列【head,node1,node2,node3】,node1 被取消了,加锁代码块正在被执行着,node2 被打断。node2 尝试拿锁失败,因为 node1 的 waitStatus == 1 ,因为清理 node1 ,允许 node2 自旋一次,再尝试拿锁,第二次拿锁失败,判定要 park。

(3)如果被打断的线程拿到锁了,怎么办?

它会自己打断自己,把锁让出来。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值