AQS源码分析

AQS源码分析


两种获取锁方法

  1. 尝试获取锁,失败则返回。这种获取方式不会加入队列进行自旋操作
  2. 一定要获取锁。这种操作一旦获取失败,就会加入等待队列等待锁资源

这里重点说一下第二种方法


提示:以下是本篇文章正文内容,下面案例可供参考

原理

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

tryAcquire即第一种方法,尝试获取锁,当尝试获取锁失败并且后面的方法返回true的时候,进行selfInterrupt。
这么现在来一步一步拆解着看

 private Node addWaiter(Node mode) {
     Node node = new Node(Thread.currentThread(), mode);
     // Try the fast path of enq; backup to full enq on failure
     Node pred = tail;
     if (pred != null) {
         node.prev = pred;
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     enq(node);
     return node;
 }

这里首先将当前加入的线程封装成一个新的节点,其中tail为当前等待队列的尾节点,如果尾节点不为空,则通过CAS操作将尾节点设置为当前节点的前置节点,并将尾节点设为当前节点的前置节点。这时候当前节点成为尾节点,并返回当前节点。
这里的compareAndSetTail为unsafe中的一个native的方法,提供原子操作,所以这里将当前节点置为尾节点的操作是线程安全的。
那么if里面的代码块会有线程安全的问题嘛?并不会,因为if里面的代码块只是将前置节点的指针指向了当前节点。
这是一个快速入队列的方法,如果失败了(即第一次自旋失败或者尾节点为空),则调用enq自旋入队列。

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

这里首先判断尾节点是否为空,如果是空的说明只有一个头结点。
注意,AQS使用了哨兵模式,头结点是一个哨兵结点,为什么要有这个节点呢?是为了防止只剩一个节点时,head指针和last指针同时指向该节点且同时发生出队列和入队列的情况下,出现空指针的问题。进队列调用last.next=cur,但是此刻发生出队列,那么last为空,自然就空指针异常了。这是线程不安全的。因此将头节点设为哨兵节点,既有监控之后节点状态的目的,也有线程安全的目的。
好了,继续源码分析。
上面说到如果尾节点为空,则将尾节点设为头节点,方便下一个节点入队。
如果尾节点不为空,就更换当前节点。之后的操作就和快速入队一样了。
那么为什么要分完全入队和快速入队呢?因为完全入队多了判空的操作。分离之后可以少判一次空?这是我的想法。有大佬说Java 9已经实现合并了,本人用的Java 8 还不太清楚。不知道有没有大佬告知一下。
接下来入队列操作就结束了,那么节点是如何自旋或者如何获取锁的呢?
接下来看看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;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这里可以看到,如果程序出现错误,那么就会直接取消获取锁资源的操作,这里就是把node的waitStatus置为CANCELLED状态。
关于获取锁的自旋,则判断当前节点的前置节点是否为头节点且当前节点获取锁成功了。如果有一个条件不满足,就会被挂起。
这里又有一个interrupted 的变量,分析代码可以看出,当线程需要被挂起并且成功挂起,interrupted 会被设置为true。是否需要被挂起则需要去判断节点的waitStatus,如果当前节点的前置节点的waitStatus为SIGNAL,即等待通知的状态,则代表当前线程需要被挂起。如果ws大于0,那么就是cancelled状态,就可以删除了。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

如果都满足,那么就调用如下方法将线程挂起

 private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);
     return Thread.interrupted();
 }

这里返回interrupted方法主要是为了将线程的中断标志取消。interrupted方法是判断线程是否被打上中断标记。采用中断原语LockSupport.park对线程进行挂起的话,外部对线程的中断的操作则不会抛出异常。这里主要是为了将外部对线程打上的中断标记传递到外部。因为在等待队列中的线程是无否对外部中断作出响应的。所有需要等到线程拿到锁之后再去处理中断请求。
释放锁则调用release方法。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

这里如果尝试释放锁成功的话,那么就要去唤醒下一个节点。这里的判断条件是头节点不为空且头节点有后继节点(waitStatus为0代表当前节点没有后继节点或者是头节点)
具体看unparkSuccessor方法

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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);
}

这里首先会释放头节点的线程,即将showStatus置为0。
然后查看下一个节点,若下一个节点为null或者showStatus为cancelled,则从尾节点向前遍历,找到一个最靠前的且showStatus为signal的节点并将其唤醒,唤醒操作为LockSupport.unpark,并将其作为head节点的下一个节点进行监听,并且该线程进行自旋操作获取锁资源。
从后往前是为了防止线程不安全的问题,在enq中,将当前节点的前置节点设置为尾节点的操作是原子性的,但是将尾节点的后继节点设置为当前节点是线程不安全的,从前往后可能会出现空指针异常(还没来得及设置)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值