AQS结构最最最简略流程整理

AQS是Lock的根底,之前一向想自己整理一下,怎样办一向没有静下心来整理,这里记录并做整理,文章根据JDK 1.8。

独占锁的上锁流程

AQS源码中给了咱们一个最常见的互斥锁的代码示例:

public class MutexCustom implements Lock {
    public static final int STATUS_ORIGIN = 0;
    public static final int STATUS_TARGET = 1;
    private Sync sync;
    public MutexCustom() {
        sync = new Sync();
    }
    public static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(STATUS_ORIGIN, STATUS_TARGET)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        @Override
        protected boolean tryRelease(int arg) {
            setState(STATUS_ORIGIN);
            setExclusiveOwnerThread(null);
            return true;
        }
        @Override
        protected boolean isHeldExclusively() {return true;}
        public Condition newCondition() {return new ConditionObject();}
        public int getCurrState() {return getState();}
    }
    @Override
    public void lock() {sync.acquire(STATUS_TARGET);}
    @Override
    public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(STATUS_TARGET);}
    @Override
    public boolean tryLock() {return sync.tryAcquire(STATUS_TARGET);}
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(STATUS_TARGET, unit.toNanos(time));
    }
    @Override
    public void unlock() {sync.release(STATUS_TARGET);}
    @Override
    public Condition newCondition() {return sync.newCondition();}
    public int getState() {return sync.getCurrState();}
}

咱们先来关注最根底的最根底的最根底的锁的功能,也便是acquire获取锁release开释锁

acquire获取锁
    public final void acquire(int arg) {
    	// Node.EXCLUSIVE为null
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这里有三个主要的办法:

  1. tryAcquire测验获取锁,得到true,也便是获取到锁直接回来,也便是不会走里边的办法体,直接履行临界区的代码了。
  2. tryAcquire回来false,履行addWaiter,望文生义,把自己加到等候的nodeList最后面
  3. acquireQueued

首要清晰锁是怎样收效的,假如这3个办法都很快的回来了成果,不管回来的是true仍是false,都会持续往下履行,差异只不过是执不履行selfInterrupt()办法算了,并没有影响当时线程持续往后履行后续办法,也便是临界区代码。

假如这三个办法里边有某一个办法没有很快的回来成果,而是把自己困在了一个while或许for(;;)循环里边,或许运用LockSupport.park()办法,那么这个线程就堵塞住了,不能持续往后履行后续的临界区代码,那这个时分就完成了堵塞。

在合理的逻辑下竞赛和开释这个堵塞的代码就完成了锁。
那究竟是在哪个办法里边堵塞住了呢?咱们挨个办法去看:

1.1. tryAcquire
// 需求被重写,给出获取资源逻辑
// 比方最简略的互斥锁,只需求在state为0的时分compareAndSetState(0,1)成功回来true就好了。
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

这个没有什么好说的,已然AQS是完成Lock的根底结构,那肯定就要把逻辑代码空出来让开发者自己去完成,回来true表明竞赛到了锁,能够履行临界区代码,反之没有竞赛到锁,需求等候锁的开释并重新竞赛。

1.2. addWaiter
    private Node addWaiter(Node mode) {
    	// mode假如是Node.EXCLUSIVE则为null,so,node的next现在为null
        Node node = new Node(Thread.currentThread(), mode);
        // 测验快速刺进到最尾端,也便是把新创建的node放到tail的next,然后用node取代tail。
        // tail是全局变量,初始化为null
        Node pred = tail;
        // 前提是tail不为空
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 假如tail为空则直接走enq办法。
        enq(node);
        return node;
    }
    // 其实比快速刺进不一样的便是假如没有tail,把tail赋值成head,然后再持续append
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
            	// 初始化状况tail为null,给head设置一个new Node(),一起把tail也指向这个head
            	// tail\head同为全局变量,初始化都为null
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

看着注释理一遍:
addWaiter办法望文生义,便是把自己的node刺进到链表的最尾端,加入抢占资源的排队序列,等候抢占资源。
需求留意的是,tail / head都是全局变量,都是初始化为null,刚开始运转的时分tail / head都为null,假如tail为null,会走enq办法,做了两件事

  1. tail为null,先把head初始化为一个new Node(),一起暂时把tail赋值为head。
  2. 把当时node赋值为head的next,一起当时node作为tail。

留意回来值是t,而不是当时node,也便是回来node的preNode,是t,上述情况下当时链表有两个值head->tail,回来的应该是是head而不是tail。当然这里调用enq办法并没有运用它的回来值。addWaiter办法回来值是当时node。

所以从空链表初始化完毕后,链表里应该有两个节点,而不是只有一个head节点,保证了工作节点能获取到一个pre节点。

compareAndSetHead和compareAndSetTail这些CAS办法都不是堵塞的,都是Unsafe办法完成,所谓乐观锁,不必关怀脏数据,只需关怀成功或许失利。

1.3. acquireQueued

这里附上一个Node状况的表格:

类型 阐明
CANCELLED 1 当时节点由于超时或许中止被撤销,节点进入这个状况以后将坚持不变。 注:这是唯一大于0的值,许多判别逻辑会用到这个特征
SIGNAL -1 后继节点会依据此状况值来判别自己是否应该堵塞、当时节点的线程假如开释了同步状况或许被撤销、会告诉后继节点、后继节点会获取锁并履行(当一个节点的状况为SIGNAL时就意味着其后置节点应该处于堵塞状况)
CONDITION -2 当时节点正处在等候行列中,在条件达到前不能获取锁。
PROPAGATE -3 当时节点获取到锁的信息需求传递给后继节点,共享锁模式运用该值
INITIAL 0 节点初始状况,默认值,表明当时节点在sync同步堵塞行列中,等候着获取锁
// 放入等候行列
    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)) {
                	// 假如前一个节点是head而且这时获取到了临界资源,把当时node设置为head,head的next置为null
                	// 两个条件:
                	// 			1. node是head的next节点,也便是轮到它了。
                	// 			2. tryAcquire(arg)得到了资源,也便是获取到了锁。
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 留意哈,这里是&&,也便是shouldParkAfterFailedAcquire办法回来true才会履行parkAndCheckInterrupt,两个办法一起回来true才会履行interrupted = true
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    // 检测失利的时分是否需求堵塞自己,我要睡觉了,等人来叫醒我再检测是不是需求排队。
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    	// 留意,ws是上一个节点的status
        int ws = pred.waitStatus;
        // SINGAL: 此node的后续node经过park把堵塞住了,所以当时node在release或许撤销acquire的时分有必要unpark一下后续的node。
        // (当一个节点的状况为SIGNAL时就意味着其后置节点应该处于堵塞状况)
        if (ws == Node.SIGNAL)
        	// 前置节点现已设置了SIGNAL,能够堵塞我的线程了,后续检测到Node.SIGNAL会唤醒我的线程
            return true;
        if (ws > 0) {
            // 前置节点被设置为cancel了,越过pre,我来当pre, ^_^
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 检测到自己是INITAL或许PROPAGATE,把pre的status设置为SIGNAL,持续循环
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    //park睡觉了,睡醒了之后检测是否需求中止。
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

注释很清晰了,整理一下:

acquireQueued办法望文生义,测验获取锁,获取不到就进入行列等候。

  1. acquire处在一个for(;;)死循环中, 获取锁需求两个条件:
    1.1. 前置节点是head,也便是轮到我自己获取锁了
    1.2. tryAcquire(arg)==true,也便是竞赛到了锁
  2. 没必要一向测验获取锁,把自己标记为需求唤醒,一起调用park堵塞,告诉predecessor,需求的时分unpark我。

LockSupport.park(this);之后当时线程会堵塞,直到有unpark操作才会持续往后履行。所以在for(;;)死循环中并没有一向测验获取锁,在获取不到之后会堵塞自己,并把node的状况标记为SIGNAL。后续看到node的状况为SIGNAL,在自己的锁开释后会调用unpark唤醒自己持续履行for(;;)中没有履行完的部分,持续测验tryAcquire获取锁,这时就能获取到锁并完毕循环了。
这便是最根底的获取锁的功能。

release开释锁

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            // head不为空,而且status不是初始状况,则需求唤醒unpark一下下一个节点。不然下个节点还没有堵塞,不需求unpark唤醒。
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // 需求unpark唤醒下一个节点
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 需求unpark的Thread被当时node的继任node持有,通常情况下是下一个node,但是有可能是cancel状况或许是null,
        // 从尾部逆序往前遍历以找到状况不是canceled的继任者。
        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);
    }

开释锁的流程看起来就简略了一些。tryRelease成功开释锁之后,找到下个节点,唤醒他。

至于为什么需求从后往前遍历而不是从前往后遍历:

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

这段代码是往tail刺进新的next节点,运用CAS保证安全,假如当时线程履行到了②,但是这时有一个新线程履行到了①,新线程只履行了把node的pre指向了tail,这时tail的next仍然为null,这时tail变了再履行CAS就会失利了。
这种情况下从后往前遍历是能够找到的,所以是安全的。

参阅链接:
www.zhihu.com/question/50…
blog.csdn.net/anlian523/a…
blog.csdn.net/java_lifeng…