AQS是什么?基于ReentrantLock解密!

AQS是什么?基于ReentrantLock解密!

思维导图如下:

avatar

前言

解释

AQS,AbstractQueuedSynchronizer,抽象队列同步器,

ReentractLock、ReadWriteReentractLock,锁API底层都是基于AQS来实现的,一般我们自己不直接使用,但是是属于java并发包里的底层的API,专门支撑各种java并发类的底层的逻辑实现

画图讲解AQS的原理

avatar
步骤详解:

  • 首先线程1如果抢到了锁,会将AQS中的state变量加1
  • 同时记录当前抢到锁的线程.
  • 然后线程2在抢锁失败后会被阻塞住,同时加入AQS的阻塞队列中去.等待被唤醒
  • 线程1执行完之后会释放锁,state变量减1,此时会唤醒队列里面的线程(不论是非公平还是公平策略,都会从头部开始唤醒)
  • 此时线程2可能会被唤醒,同时将state变量又加1,并记录AQS中的当前线程为线程2

基于ReentrantLock解密AQS

首先构造方法

public ReentrantLock() { //
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • 默认为非公平策略.只有使用fair指定的时候才能改变是否公平.
  • 默认的构造方法中创造了一个Sync,NonfairSync,是底层专门用于加锁和释放锁的核心组件

AQS组件Sync

  • abstract static class Sync extends AbstractQueuedSynchronizer
    • 可以看到其实Sync是继承了AQS的.是一个抽象的静态内部类,子类,多线程同步组件
    • AbstractQueuedSynchronizer
    • AQS抽象队列同步器.是java并发包各种并发工具(锁,同步器)的底层的基础性组件.
NonfairSync非公平策略
  • Sync的一个子类,覆盖重写了几个方法.代表了一个Sync的具体实现,不需要公平的去阻塞在队列的.可以在还没有唤醒队列的线程的时候就抢占到锁.无需等待阻塞.
FairSync公平策略
  • sync的一个子类.用于实现公平抢占锁.先来的先被唤醒去抢占锁,后来的添加在阻塞队列中去.

AQS核心

Node双向链表
  • 一个双向链表队列node
    • 该node内部包含了用于区分不同状态的static变量.以及前后指针.还有当前要加入node里面的被阻塞线程
static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;
    /** 已取消状态,该状态要等阻塞队列里某个线程被唤醒后抢锁失败抛异常才会被标记为1 */
    static final int CANCELLED =  1;
    /** 被加入阻塞队列 里面之后待唤醒状态 */
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    static final int CONDITION = -2;
    /** waitStatus value to indicate the next acquireShared should unconditionally propagate*/
    static final int PROPAGATE = -3;
    //该状态代表了上面几个静态final值.
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
state变量
  • 核心变量
    private volatile int state;// volatile保证了可见性
    protected final int getState() {
        return state;
    }
    protected final void setState(int newState) {
        state = newState;
    }

  • 该变量用于加锁,释放锁.加锁就state加1,释放锁就state减1.

lock方法

可以看到sync加锁的源码

public void lock() {
        sync.lock();
}
  • reentrantLock在加锁的时候是直接基于sync来进行的加锁lock
  • 可以看出来reentrantLock这个类其实就是比较外层的一个薄薄的封装的一个类了,Sync就是ReentrantLock底层的核心组件
  • 这里先看非公平锁的lock方法.上面的sync.lock()方法的具体实现如下.这里的lock方法是sync的具体实现类NonfairSync实现的.
 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

compareAndSetState方法cas

  • aqs中的核心变量state,在cas的时候进行检查.查看该变量是否是0,如果不是,代表被人加锁了.不是0自己就对state设置为1
  • compareAndSetState(0, 1):相当于是在尝试加锁,底层原来是基于Unsafe来实现的,JDK内部使用的API,指针操作,基于cpu指令实现原子性的CAS,Atomic原子类底层也是基于Unsafe来实现的CAS操作
  • return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

stateOffset是state变量对应的偏移量.expect传入的是0,update传入的是1.
这行代码可以保证说,在一个原子操作中,如果发现值是我们期望的这个expect值,说明符合要求,没人修改过,此时可以将这个值设置为update,state如果是0的话,就修改为1,代表加锁成功了
这个操作是CAS原子性的.
如果加锁成功了,compareAndSetState(0, 1)返回的是true,此时就说明加锁成功,他需要设置一下自己是当前加锁的线程,如下面的execlusiveOwnerThread方法.

exclusiveOwnerThread记录加锁线程

  • 可以从上面的代码的lock方法中看到,cas抢到锁之后,执行了setExclusiveOwnerThread(Thread.currentThread());方法.
  • 该方法记录了当前抢到锁之后的线程.也就是上面图片上的记录当前线程变量
public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** Use serial ID even though all fields transient. */
    private static final long serialVersionUID = 3737899427754241961L;

    /**
     * Empty constructor for use by subclasses.
     */
    protected AbstractOwnableSynchronizer() { }

    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * Sets the thread that currently owns exclusive access.
     * A {@code null} argument indicates that no thread owns access.
     * This method does not otherwise impose any synchronization or
     * {@code volatile} field accesses.
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    /**
     * Returns the thread last set by {@code setExclusiveOwnerThread},
     * or {@code null} if never set.  This method does not otherwise
     * impose any synchronization or {@code volatile} field accesses.
     * @return the owner thread
     */
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

AbstractOwnableSynchronizer是被AbstractQueuedSynchronizer继承的
Sync又继承了AbstractQueuedSynchronizer,然后NonfairSync又继承了Sync,所以NonfairSync可以直接使用AbstractOwnableSynchronizer中的方法

avatar

acquire(1)方法阻塞后续抢锁线程

当其他线程cas去进行抢占锁的时候,如果cas返回false,说明抢占锁失败,此时会走acquire方法.

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

tryAcquire方法

tryAcquire(1)此时首先会走这个方法,传递进去一个值是1,AQS的父类 实现是一个空,其实是留给子类来实现的,如下所示的nofairSync实现了tryAcquire方法.

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
  

nonfairTryAcquire方法

  • nonfairTryAcquire(1):这个方法会走到Sync(父类)这个父类里面定义了nonfairTryAcquire方法.
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 修改这个state的值,volatile保证了可见性
                setState(nextc);
                return true;
            }
            return false;
        }
  • 首先获取当前线程,然后去获取state变量的值,进行判断是否state变量为0.如果为0就进行cas
上面为什么又要再次判断state变量的值?

首先走这个nonfairTryAcquire方法其实是因为在前面cas失败才走去进行阻塞并加到阻塞队列的.这里却还要再次判断一下state的状态.

  • 其实该方法内部会再次进行判断state的值是否为0,是因为有可能走到这里之后c的值又因为第一个线程释放锁后被改回0了.此时当前正在阻塞的线程其实就不用去加入到阻塞队列里面去了.直接进行cas抢占锁就可以了.这也是为什么c等于0的话又重新cas了一次.

avatar

state实现可重入锁
  • 非0,并且当前线程就是先前加锁的线程,说明该线程进行了多次加锁.也就是走了可重入锁的逻辑,

此时会将state变量加1后重新进行setState(nextc);操作.让state变量递增.然后返回true

  • 如果当前线程不是先前加锁的线程,说明不是可重入加锁.此时返回false.说明是别的线程过来抢锁.继续下面的分析.

avatar

假设线程2过来抢占锁,会怎么走?

  • 上面分析了.如果nonfairTryAcquire方法返回false,那么!tryAcquire(arg)条件成立.继续走下面的

addWaiter(Node.EXCLUSIVE)方法.

addWaiter方法

  • 如下:

EXCLUSIVE是指独占模式来标记新增的node节点.(排他性,独占锁,同一时间只能有一个线程获取到锁,此时是排他锁,独占锁)

 if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            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;
}
  • addWaiter方法内部封装的一个Node节点.将当前线程放进去.然后将node节点放入链表.
  • 这里compareAndSetTail在设置节点的时候可能别人也在设置,所以也用cas进行抢占.
  • 但是如果pred为null,就不会走if语句.说明链表不存在,是第一次加锁被阻塞.

enq方法

此时直接走enq(node方法)
avatar

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

这个方法内部进行了for循环.进行各种指针的变换,从而将最终加入的node节点返回.

  • 第一次循环.t为null时,进入if语句,将tail节点指向一个新创建的node节点.该node节点是虚拟节点

avatar

  • 第二次循环进来,此时进入else中.将prev指向空node,然后next指针指向enq传过来的node.然后直接返回node.证明已经添加到链表中.

(注意,该链表的第一个节点其实是一个初始化过的虚拟node)
avatar

acquireQueued方法

  • 然后acquireQueued(addWaiter最终会返回node) 即 acquireQueued(node)
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);
        }
    }
  final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
  • 还是一个for循环.进入之后p指针指向node的predecessor()方法.返回的prev节点.
  • 可以知道在第一次的时候,第一个if语句虽然p等于head,但是tryAcquire方法会因为不是重入线程而返回false然后不满足条件退出第一个if语句.
  • avatar
  • 进入第二个if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())

shouldParkAfterFailedAcquire方法

  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;
    }
  • 该方法将要加入阻塞队列的node和node的prev指针也就是pred传过来.
  • 然后首先获取到了pred等待状态.,判断是否是signal ,也就是-1.waitStatus其实在node初始化的时候是0.

avatar

  • 继续看if显然不满足,继续走else compareAndSetWaitStatus(pred, ws, Node.SIGNAL);将pred对应的nodeHead虚拟节点的状态改为了SIGNAL=-1;
private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }

avatar

  • 然后shouldParkAfterFailedAcquire最终会返回false,又重新走一遍for循环,从头走起.又会走第九步:此时p指向head节点也就是nodeHead,并且重新尝试tryAcquire方法.防止线程1释放锁.那就不需要接着走了.线程2就可以去抢占锁了.
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;
}

avatar

  • 可以看上面,我们下面会继续假设线程1没有释放锁.所以tryAcquire还是会返回false,继续走shouldParkAfterFailedAcquire方法,为了方便查看,这里再粘贴一遍
  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;
    }

由上图知道此时pred的状态获取之后为-1,会直接在第一个if止住,返回true.那么对于下面if中就会继续走parkAndCheckInterrupt方法.就会阻塞中线程2,到此线程二走lock方法就到头了

parkAndCheckInterrupt方法

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

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

avatar

  • 此时线程2被阻塞之后,就会不走继续走了.这里要说明的是acquireQueued中try finally方法的finally是不会走的.原因可以看下面的拓展.:其实当try中被阻塞时,finally方法其实也是被阻塞的.直到try中return前try中的所有代码执行完才会走finally方法…所以除非线程1被释放,线程2被唤醒.不然不会走finally方法.
  • 那么阻塞队列中的node其实是在unlock的时候.可以继续往下看.下面会回答这里的疑问!

假设现在线程3也加入抢锁.

  • 繁琐的流程就不画了.基本跟线程2一个套路.最终会在线程2node的后面继续拼接一个线程3node阻塞节点.这里线程3的流程分别是 十三步,十四步,十五步
  • 需要特别注意,此时线程2node被更改为了-1

avatar

cancelAcquire(node)方法

  • 下面代码主要是讲node节点给标记为被取消…意思是获取锁的时候发生异常,导致被取消掉获取锁的权限.
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    node.thread = null;//将要移除的节点的线程置为null
    Node pred = node.prev;//获取当前要移除线程的前置节点,也就是head节点.
    while (pred.waitStatus > 0)//此时pred也就是head的状态是0,所以该循环不为0
        node.prev = pred = pred.prev;
    Node predNext = pred.next;
    node.waitStatus = Node.CANCELLED;
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}
  • waitStatus被设置为CANCELLED也就是1,此时走到else语句中.判断pred不等于head但是状态不匹配,然后继续走到unparkSuccessor中,

可以看到下面首先获取线程二node的状态,然后判断node的下一个节点是否存在. 我们知道node下面其实是有线程3node的.所以,此时是要执行LockSupport.unpark(s.thread);的. 而s是node的下一个节点,即线程3node,所以此时线程3node被唤醒.

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

如下图.

  • 上面代码顺序是被阻塞的第八步:accquireQueued方法在进行第九步,tryAcquire时发生异常,

然后判断tryAcquire(1)方法,这里注意了…这里假设tryAcquire方法线程2node在获取锁的时候失败了 比如超时,或被中断…此时是会报一个错的,此时就会进入到finally方法中去 .
/* * Various flavors of acquire, varying in exclusive/shared and control modes. Each is mostly the same, but annoyingly different. Only a little bit of factoring is possible due to interactions of exception mechanics (including ensuring that we cancel if tryAcquire throws exception) and other control, at least not without hurting performance too much. */
_上面文本来自_acquireQueued 方法上面的解释,具体意思如下:/ * *各种获取方式,在独占/共享控制模式中有所不同。每个都大致相同,但令人讨厌不同。由于异常机制(包括确保在tryAcquire抛出异常时我们取消)和其他控件的相互作用,因此只有少量分解是可能的,至少不会过度损害性能。 * /这里可以知道.当tryAcquire的时候,会抛出异常…一旦抛出异常就会走到finally中去取消.具体方法为cancelAcquire(node); -----> 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。

  • 进入finally,然后进入cancelAquire方法更改线程2的状态为被取消也就是1,然后唤醒线程2node的下一个node也就是线程3node
  • 最后cancelAcquire方法有一句代码 node.next = node; // help GC 将node2节点的next节点指向了自己. 这里写了可以GC.具体原因.下面会讲

疑问:这里怎么就帮助GC了…下面有讲

avatar

线程2被取消后唤醒线程3

  • 这里我们假设线程3node被唤醒之后依然没有获取到锁.还是在自旋等待.但是其中的指针有一些变化.

接着因为线程node3节点被唤醒,那么就会继续走第八步,在parkAndCheckInterrupt中被唤醒.然后经过一次循环又再次获取线程3node的前驱节点,然后判断是否等于head.很明显,线程3node的前驱节点其实是线程2node,并不是nodeHead,所以 for循环的第一个if语句其实是不满足的.,此时就会由进入到shouldParkAfterFailedAcquire方法中

 final boolean acquireQueued(final Node node, int arg) {
        .....
        try {
           ....
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                   ....
                }
                //继续走到这里.
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            ....
        }
    }



  • 而此时shouldParkAfterFailedAcquire方法中就会做一个重要的处理…那就是移除被取消了的节点2…

avatar
**

  • 首选获取pred也就是node2的状态,ws其实是1.被取消状态. …此时会进入do while循环.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        ...
        if (ws > 0) {
            do {
                
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            ...
        }
        return false;
    }
  1. node.prev = pred = pred.prev; 操作 最终将node3的prev指针指向了node2的prev指针所指向的nodeHead节点. 也就是跨过node2, node3的prev直接指向头结点
  2. 然后pred其实已经变为nodeHead头节点,继续执行while条件,不满足条件 nodeHead的值其实是0.
  3. 然后继续pred.next赋值给node.而node其实是线程3node. 也就相当于nodeHead的next指针 跨过node2 ** 直接指向node3**了如下图.

avatar

  1. 然后到此返回shouldParkAfterFailedAcquire,此时返回的是false.所以在for循环中还是会继续走一遍.不过此时发生变化的是获取的线程3node的prev节点也就是p节点是head了.满足p==head
  2. 我们假设此时tryAcquire方法还是没有获取锁成功,其他线程还未释放.所以继续走shouldParkAfterFailedAcquire方法.
  3. 此时因为nodeHead的ws状态为0,所以会被重新设置为-1.如下,继续返回.然后再走for循环,假设tryAcquire还是没有获取锁,继续回shouldParkAfterFailedAcquire,此时发现ws为-1.满足第一个if语句.直接返回true.
 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;
    }
  1. 然后又会走到parkAndCheckInterrupt方法被阻塞住.到此为止可以做个总结: 线程2被取消,然后唤醒线程3, 但是线程3依然未获取到锁,还是被阻塞

疑问: 啥时候才能让队列中的node3抢占到锁呢? 我们可以看下面加锁线程主动unlock释放掉锁.

unlock方法

代码如下,

public void unlock() {
    sync.release(1);
}

release方法

真正的代码在AQS的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;
}

tryRelease(arg)方法

  • 上面进入release方法首先会走if语句中的tryRelease方法.走进去发现啥代码没有就抛了一个异常.(这个异常后面有用到.这里注意一下)
   protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

  • 其实是该方法是被ReetrantLock的子类Sync重写了.因为Sync继承了AQS,所以可以直接重写.如下:
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;  当线程1来释放锁,此时c= 1 - 1=0;
    //如果当前线程不是加锁的线程,但是来进行释放,就报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        //这里设置释放free为true;并且当前加锁线程为null.
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c); //更新aqs的state为0.
    
    return free;
}

tryRelease返回true,就会进入release方法的if语句中.再贴一遍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;
    }

unparkSuccessor(Node node)方法

  • 注意.这里参数传入的是头结点.
  private void unparkSuccessor(Node node) {
      //获取的是头结点的状态.
      int ws = node.waitStatus;
      if (ws < 0) 
          //这里进行设置头结点的状态为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);
  }
  • 由上图可以知道头结点的状态是SINGNAL = -1 ; 上面第一个if下面的cas将头结点的状态改为了0. 获取到了头结点nodeHead的下一个节点即线程3node.然后因为线程3node不为空.并且线程3node的waitStatus是等于0的.不满足第一个条件

avatar

  • 然后会走unpark方法唤醒nodeHead的next节点也就是线程3节点.
if (s != null)
          LockSupport.unpark(s.thread);

线程3nodel被unpark唤醒之后做了什么?

  • 其实这里会走到上面acquireQueued方法那里.线程3其实被parkAndCheckInterrupt方法内部给park住了.一旦线程3被唤醒if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())这个条件就为真.就可以进入if语句内部,被中断标记设置为true,然后重新开始循环了.

parkAndCheckInterrupt内部的park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
被中断标记方法内部设置为true,因为parkAndCheckInterrupt将中断标记给清除了,所以当acquireQueued返回为true的时候,还要再interrupt中断一次.保证其中断标记存在.

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())//线程2在这里被唤醒.
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

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

  • 可以看到一旦线程3node被唤醒,就会重新开始进行循环.此时会再次获取线程3node的prev指针p.然后判断p是否等于head,其实是满足的
  • 然后走tryAcquire方法,我们假设线程3node此时可以抢占到锁…那么tryAcquire中cas获取到锁.并且将state状态设置为1.AQS的当前线程设置为线程3node的线程
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
        }

avatar

  • 然后此时tryAcquire会返回true,并将头结点nodeHead设置为线程3node自己.并且将node的prev设置为null,也就是取消对nodeHead的指向.
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
  • 然后执行下面语句,将nodeHead的next指针也指向空.然后返回并再次对自己也就是被唤醒后已经清除过一次中断标记的线程3node重新打一次标记.
p.next = null; // help GC
failed = false;
return interrupted;


static void selfInterrupt() {
    Thread.currentThread().interrupt();
}
  • 可以看下图.线程3node完全变为head虚拟节点.被唤醒之后链表内部的线程3node节点的线程被清空…

而线程2node的prev指针虽然还指向nodeHead节点,但是因为nodeHead的next指针没有指向任何节点.并且也没有被任何阻塞节点指向…
此时nodeHead和线程2node完全被阻塞链表孤立. ** jvm垃圾回收时就会回收掉nodeHead和线程2Node.**
avatar

for循环获取当前节点的前面的节点.因为当前节点的前面的节点为null.所以推出for循环.并且下一个if也不会走.直接返回.

  • 继续走node.next = node; // help GC,这里将node的next指针指向node.至于为什么能够帮助gc,这里也不太理解.因为毕竟node的next本来不就是null吗.这里重新将node的next指向node不是反而让next多了指向吗?怎么就帮助gc了.

下面是next指针的解释:
链接到后继节点,当前节点/线程释放后继节点。在排队过程中分配,在绕过已取消的前任对象时进行调整,并在出队时取消(出于GC的考虑)。 enq操作直到附加之后才分配前任的下一个字段,因此看到下一个空字段并不一定意味着节点在队列末尾。但是,如果下一个字段为空,则我们可以从尾部扫描上一个进行双重检查。被取消节点的下一个字段设置为指向节点本身而不是null,以使isOnSyncQueue的工作更轻松.
可以从最后一句话看到端倪.isOnSyncQueue方法的工作是啥呢?
Internal support methods for Conditions
翻译:可以看到是条件队列的内部方法.为了让条建队列

总结:

if语句中的cas失败会直接执行else语句

  • cas失败后相当于if语句不满足,此时会走else语句

对象初始化完成之后其实判空时是不为null的.即使里面的属性为null

  • jvm在初始化对象的时候有几个步骤,第一,先分配内存存储空间,第二: 后创建并初始化构造,第三:完成内存指针ref指向对象
  • 所以一旦对象初始化完成,其实判空时该对象已经有自己的内存空间并且有ref指针了.只不过内存中没有属性数据而已.

cancelAquire方法是从tryAcquire抛异常后才执行的.

执行完之后,节点的next指针指向自己.然后就返回了.如下:
avatar

node.next = node; 为什么可以保证GC

在上面线程2node被取消后,执行了node.next = node;自己指向自己,如上图. 其实此时还不是完全理解.因为从jvm角度来讲此时线程2node还没有到回收的时候呢!此时线程2node还指向nodeHead,而nodeHead还指向线程3node, 从垃圾回收GCRoot标记整理时会发现其实线程2node还是被间接引用的.
那么什么时候线程2node才真正被回收呢? 其实是直到线程3node被唤醒称为新的头结点,与nodeHead之间相互断开的时候,如下图.
avatar
断开就可以回收了吗?是的.了解jvm的看下图就可以明白,栈内存指向的对象一步一步往下,到达线程2Node的时候,被断开指向.
那么垃圾回收器在回收的的时候会执行一步操作,叫**老年代的标记整理算法 ** 该算法会将存活对象标记移动到相对紧凑的内存空间,然后将剩余的垃圾对象一次性回收掉. 那么线程3node可能被标记后移动走了…而此时线程2node和Nodehead节点则被断开孤立,等待的就是被清除的命运…

avatar
至此标题说node.next=node; 可以帮助回收其实是有一定道理的.

从栈内存栈帧指向来看…堆内存中的对象被自己的栈帧指向…并不是指向其他的对象…所以该对象就相当于孤立了…
或者说node.next=node 是从node对象整体来看…next只是一个属性…prev也是一个属性…不论该属性是否为空…还是指向自己,.从整体GCRoot来说,因为对象被孤立.所以都能被回收,这也就是源码中说node.next=node可以帮助回收的原因吧.

try,finally中程序的执行顺序

摘自:try,finally中都有return时程序的执行顺序
在Java中当try、finally语句中包含return语句时,执行情况到底是怎样的,finally中的代码是否执行,大家各有各的说法,刚好今天有个朋友问了我这个问题,下面我就提供两段代码,详细解释一下~
这是第一种情况:try代码块中包含return语句,finally代码块中不包含return语句;
Java代码:

package Exception;

public class Test02 {
	public static void main(String[] args) {
			System.out.println(test());
	}
	public static int test(){
			try{
				int i=1;
				return i;
			}
			finally{
				int i=2;
				System.out.println(i);
			}
	}
}

2
1

结果显示:finally代码块里如果没有return,finally代码块会优先在try代码块里的return前执行;

第二种情况是:try代码块和finally代码块中都有return语句
这也是我们需要重点讨论的地方;
java代码如下:

package Exception;
public class Test01 {
public static void main(String[] args) {
  
 	System.out.println(test());
}
 
public  static int  test(){
 	try{
   int i = 1;
   return i;
     }
 	finally{
   int j = 2;
   return j;
 	}
}
}

这次我在try语句块和finally语句块中都加入了return语句,猜猜会输出什么结果?i = 1?还是 i = 2 ? 还是让程序告诉我们答案吧!
java代码如下:
2
有的小伙伴就说了,肯定会输出2啊!程序里有try-catch结构时,代码块里如果有finally代码块,无论如何都会执行finally代码块,上面这段代码
finally代码块里有一个return,在执行finally的return方法时,程序的结果就返回了。所以主程序输出的是2。
讲解如下:
主程序在调用test方法的时候会先执行try代码块里的代码,并先return i=1;但并不会直接把return的结果返回给主函数,而是在暂时储存在栈空间里;
口说无凭!那就让神奇的debug看告诉我们事实,在MyEclipse中用debug进行调试会发现,程序会先执行try代码块里的i=1;此时紧接着会马上return i=1;
然后再执行finally代码块中的 代码, int j = 2; return j ; 此时执行完finally代码块的return j ; 后 ,程序就结束了,并不会再去执行返回try代码块中在栈空间里存储的 i=1;
栈空间里的 i = 1 ; 随着程序的结束 也就自动消失了~
这两个例子的结论就是:
1.如果try代码块里有return语句,而finally代码块里没有return语句,程序会先执行finally代码块里的代码然后再执行try代码块里的return语句;
2.如果try代码块和finally代码块里都有return语句,try代码块里的return语句会优先finally代码块里的return语句执行,但不会把返回的结果返回给主函数,
而是会把finally代码块里return的结果返回给主函数。

结论:finally中的代码比return 和break语句后执行
自己尝试: try和finally中当try阻塞时 (try中有return,finally没有) 是谁先执行?

对于aqs中的acquireQueued方法来说,最先执行try中的方法,直到到达return返回前才会执行finally,所以可以断定acquireQueued中的finally方法需要在acquireQueued中的阻塞线程被唤醒或者中断后,进行return之前的所有代码执行完之后才执行.

public class ReentractLockDemo {
	public static void main(String[] args) {
		System.out.println("accquireQueued最终返回!"+acquireQueued());
	}
	public static boolean acquireQueued() {
		boolean failed = true;
		try {
			boolean interrupted = false;
			for (;;) {
				if (!failed) {
					System.out.println("try方法return开始执行!");
					return interrupted;
				}
				if (block(failed)){
					System.out.println("结束阻塞!并将循环条件failed设置为false");
					interrupted=true;
					failed=false;
				}
			}
		} finally {
			if (!failed)
				finallyMethod();
		}
	}
	private static boolean block(boolean failed) {

		System.out.println("开始阻塞!1000毫秒");
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return true;
	}
	public static  void finallyMethod(){
		System.out.println("进入method方法");
	}
}

结果:
开始阻塞!1000毫秒
结束阻塞!并将循环条件failed设置为false
try方法return开始执行!
进入method方法
accquireQueued最终返回!true
总结:finnal不会被执行除非try执行完毕.到达return的时候.

最后

  • 不管你是从上往下一步一图跟着画,跟着看源码看完的,还是直接鼠标直接划到底的,你都了解了其实要想完全了解AQS和ReentranLock 光靠简单的语言是不可能详细的完全理解各种场景的.必须要很大的篇幅才能讲清楚.所以能看到这里还是恭喜你.
  • 不管是不是白嫖,来个三联吧.啊哈哈.毕竟花了两三天熬夜写出来的,还是很有成就的.
  • 讲点心里话,想要学好,真的要静下心来.慢慢画图,当自己就是写代码的人.沉浸其中.掌握画图技巧,刚开始也不会画,各种指针.各种饶,但是尝试总有收获.感觉再复杂的算法通过画图都可以看明白.理解明白.
  • 可以看看我下面画图跟着源码每个场景都画一副甚至多副图,这样就有了步骤性.记录性.
  • 总之,画图就是明灯,照亮你的源码之路.有任何疑问可以留言问我.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值