前言

由于AQS的源码太过凝练,而且有许多分支好比作废排队、守候条件等,若是把所有的分支在一篇文章的写完可能会看懵,以是这篇文章主要是从正常流程先走一遍,重点不在作废排队等分支,之后会专门写一篇作废排队和守候条件的分支逻辑。读源码万万别在每个代码分支中往返游走,先按一个正常的分支把流程看明了,之后再去重点关注其他分支,各个击破。我信赖看完正常流程,你再去剖析其他分支会加倍轻车熟路。本篇将主要方式名都做了目录索引,查看时可通过目录快速跳到指定方式的逻辑。

执行流程

AQS的执行流程大要为当线程获取锁失败时,会加入到守候行列中,在守候行列中的线程会凭据从头至尾的顺序依次再去实验获取锁执行。

当线程获取锁后若是还需要守候特定的条件才气执行,那么线程就加入到条件行列排队,当守候的条件到来时再从条件行列中凭据从头至尾的顺序加入到守候行列中,然后再凭据守候行列的执行流程去获取锁。以是AQS最焦点的数据结构实在就两个行列,守候行列和条件行列,然后再加上一个获取锁的同步状态。

AQS数据结构

AQS最焦点的数据结构就三个

  • 守候行列

    源码中head和tail为守候行列的头尾节点,在通过前后指向则构成了守候行列,为双向链表,学名为CLH行列。

  • 条件行列

    ConditionObject中的firstWaiter和lastWaiter为守候行列的头尾节点,然后通过next指向构成了条件行列,是个单向链表。

  • 同步状态

    state为同步状态,通过CAS操作来实现获取锁的操作。

public abstract class AbstractQueuedSynchronizer{
  
  /**
     * 守候行列的头节点
     */
    private transient volatile Node head;

    /**
     * 守候行列的尾节点
     */
    private transient volatile Node tail;
  
    /**
     * 同步状态
     */
    private volatile int state;
  
    public class ConditionObject implements Condition, java.io.Serializable {

          /** 条件行列的头节点 */
          private transient Node firstWaiter;
      
          /** 条件行列的尾节点 */
          private transient Node lastWaiter;
    }
}

Node节点

两个行列中的节点都是通过AQS中内部类Node来实现的。主要字段:

  • waitStatus

    当前节点的状态,详细看源码列出的注释。很主要,之后会在源码中解说。

  • Node prev

    守候行列节点指向的前置节点

  • Node next

    待行列节点指向的后置节点

  • Node nextWaiter

    条件行列中节点指向的后置节点

  • Thread thread

    当前节点持有的线程

static final class Node {
    /**  */
    static final Node SHARED = new Node();
    /**  */
    static final Node EXCLUSIVE = null;

    /** 标明当前节点线程作废排队 */
    static final int CANCELLED =  1;
  
    /** 标明该节点的后置节点需要自己去叫醒 */
    static final int SIGNAL    = -1;
  
    /** 标明当前节点在守候某个条件,此时节点在条件行列中 */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;

    /**
     * 守候状态,值对于上面的四个常量
     */
    volatile int waitStatus;

    /**
     * 守候行列节点指向的前置节点
     */
    volatile Node prev;

    /**
     * 守候行列节点指向的后置节点
     */
    volatile Node next;

    /**
     * 当前节点持有的线程
     */
    volatile Thread thread;

    /**
     * 条件行列中节点指向的后置节点
     */
    Node nextWaiter;

加锁

上面说明的数据结构我们先大致有个印象,现在通过加锁来一步步说明下详细的流程,上篇文章JUC并发编程基石AQS之结构篇,我们知道了AQS加锁代码执行的是acquire方式,那么我们从这个方式提及,从源码中看出执行流程为:tryAcquire——>addWaiter——>acquireQueued

tryAcquire为自己实现的详细加锁逻辑,当加锁失败时返回false,则会执行addWaiter,将线程加入到守候行列中,Node.EXCLUSIVE为独占锁的模式,即同时只能有一个线程获取锁去执行。

例子说明

首先假设有四个线程t0-t4挪用tryAcquire获取锁,t0线程为天选之子获取到了锁,则t1-t4线程接着去执行addWaiter。

acquire

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

addWaiter分支1

addWaiter方式,首先会初始化一个node节点,将当前线程设置到node节点中。然后判断head和tail节点是否为空,head和tail节点是懒加载的,当AQS初始化时为null,则第一次进来时if (pred != null) 条件不成立,执行enq方式。

例子说明

如果t1和t2线程同时执行到该方式,head节点未初始化则执行enq。

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

enq

此时可能多个线程会同时挪用enq方式,以是该方式中也使用CAS操作。for (;;)是个死循环,首先会CAS操作初始化head节点,且head节点是个空节点,没有设置线程。然后第二次循环时通过CAS操作将该节点设置我尾部节点,并将前置节点指向head,之后会跳出循环,返回天生的Node节点到addWaiter,从源码可以看到addWaiter方式后面没有逻辑,之后会挪用acquireQueued。

例子说明

t1和t2线程同时执行,t1线程上天眷顾CAS乐成,则流程为

  • 初始化head

  • t1线程的node节点加入守候行列

  • t2线程执行,node节点加入守候行列

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分支2

现在在来说t3和t4,t3和t4线程这时终于获取到了cpu的执行权,此时head节点已经初始化,则进入条件中的代码,实在也是通过CAS操作将节点加入到守候行列尾部,之后会挪用acquireQueued。

例子说明

如果t3线程先CAS乐成,之后t4乐成,此时的数据结构为

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

acquireQueued

这个方式有两个逻辑,首先若是该节点的前置节点是head会走第一个if,再次去实验获取锁???

获取锁乐成,则将头节点设置为自己,并返回到acquire方式,此时acquire方式执行完,代表获取锁乐成,线程可以执行自己的逻辑了。这里有下面几个注重点

  • p.next = null; // help GC 设置旧的head节点的后置节点为null
  • setHead方式 将t1节点设置为头节点,由于头节点是个空节点,以是设置t1线程节点线程为null,设置t1前置节点为null,此时旧的head节点已经没有任何指向和关联,可以被gc接纳,以是上面那一步会写个help GC 的注释。

例子说明

现在t1线程的前置节点为头结点,若是t1执行tryAcquire乐成则效果为

当获取锁失败或者前置节点不是头节点都市走第二个if逻辑,首先会判断当前线程是否需要挂起,若是需要则执行线程挂起。

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

private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

shouldParkAfterFailedAcquire

判断线程是否需要挂起,首先需要注重的是这个方式的参数是当前节点的前置节点。当线程需要挂起的时刻,它需要把身后事放置明了,挂起后让谁来把我叫醒。这个方式就主要做这个操作。我们再来看Node节点中的waitStatus状态,这个状态有一个Node.SIGNAL=-1,代表了当前节点需要将后置节点叫醒。这个明白可能有点绕。首先我们要明白一点,若是我需要被叫醒,那么我就要设置我们的前置节点的状态为Node.SIGNAL,这样当我的前置节点发现waitStatus=Node.SIGNAL时,它才知道,我执行完后需要去叫醒后置节点让后置节点去执行。以是这个方式是当前节点去设置自己的前置节点的状态为Node.SIGNAL

waitStatus初始化后是0,

第一次进入该方式,发现自己的前置节点不是Node.SIGNAL,需要先设置为Node.SIGNAL状态

第二次进入时发现前置节点已经是Node.SIGNAL状态,那么我就可以放心的挂起了,有人会叫醒我的。

以是这个方式实在是两个逻辑,先设置前置节点状态,再判断是否可以挂起。由于前面acquireQueued方式中for (; 也是个循环,以是会重复进入。

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

parkAndCheckInterrupt

将自己的前置节点设置为可叫醒的状态后进入该方式,线程挂起。

例子说明

此时t2-t4线程都执行到了此方式,则t2-t4线程都已经挂起不再执行,而且head-t3节点的waitStatus都为Node.SIGNAL,由于t4没有后置节点。

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

解锁

release

解锁方式的入口是AQS的release方式,首先会挪用tryRelease方式,这个是AQS实现类自己实现的方式,去CAS改变state状态,若是解锁乐成,则会进入if里的代码,获取head节点,判断waitStatus!=0,若是即是0代表没有后置节点需要去叫醒。之后挪用unparkSuccessor方式。

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时,代表为CANCELLED = 1状态,即线程作废排队,这个以后会细讲。先将头结点的waitStatus状态设为初始值0,之后查看后置节点的状态,若是>0代表后置节点作废了排队,不需要叫醒。然则当前节点需要去叫醒后续的节点让后续节点再去执行,以是会从尾结点最先寻找找到离当前线程最近的一个且waitStatus<0的去叫醒。之后会挪用LockSupport.unpark(s.thread);作废后续节点的挂起,让后续节点继续执行。

unparkSuccessor

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

例子说明

此时守候行列的数据,当t0线程执行完成后执行解锁操作,此时所有守候的线程都没有作废守候。

则t0线程会叫醒t1线程

若是t1和t3线程作废的排队时,t0线程会叫醒t2,从后往前找离head最近的一个没有作废派对的节点

线程执行到parkAndCheckInterrupt方式时被挂起,当被头节点叫醒后会继续执行,设置interrupted=true,示意被中止,会继续执行for循环逻辑,到现在一个正常的获取锁失败——>加入守候行列——>挂起——>被叫醒继续执行的流程已经整体走了一遍。

本篇文章都是自己凭据源码写出的阅读心得,可能有的地方没有推测到Doug Lea大神的意图,若是有明白纰谬的地方迎接一起探讨。

若有不实,还望指正

,

Sunbet

Sunbet www.sumeruminecraft.com Sunbet是Sunbet的大型娱乐网站,Sunbet简单快捷,业内口碑极好,是你的最佳选择。sunbet,就是要您玩得开心,赢得更开心!

Allbet Gaming声明:该文看法仅代表作者自己,与阳光在线无关。转载请注明:河北保定天气:JUC并发编程基石AQS之主流程源码剖析
发布评论

分享到:

欧博会员开户:​快珍藏!2020,伉俪共同财产支解的20个执法要点
1 条回复
  1. 环球UG客户端下载
    环球UG客户端下载
    (2020-10-17 00:00:01) 1#

    欧博手机版下载欢迎进入欧博手机版下载(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团的官方网站。欧博官网开放Allbet注册、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。很有水平的文

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。