AbstractQueuedSynchronizer(AQS)JDK并发编程核心

AbstractQueuedSynchronizer类是JDK并发编程的核心,是我们常见的可重入锁(ReentrantLock)和CountDownLatch、ThreadPoolExceutor里面的Worker都是这个抽象类的具体实现。

AbstractQueuedSynchronizer是一个FIFO双向队列,每个队列节点Node里面都有一个线程代表着是哪个线程想要获取锁,如果一个锁获取成功则直接把这个存的设置成null,因为获取成功了,这个记录已经没用了;如果获取失败则可能会被挂起。这个挂起的动作才是存储thread的意义。AQS并不控制线程的执行,只控制线程的挂起。

如图所示,这是一个双向队列,头节点意味着获取到了同步锁,Node存储的线程可以继续执行了。后面的各个节点都在死循环不断的检查是不是头结点,是不是同步状态为获取到锁。在自旋的过程中线程可能被挂起,被去取消等等。现在开始看具体的代码

1、AbstractQueuedSynchronizer的属性

AbstractQueuedSynchronizer的重点属性就只有三个:head、tail、state

/** 双向队列的头指针 */
private transient volatile Node head;
/** 双向队列的尾指针 */
private transient volatile Node tail;
/** 他是同步状态的记录,0代表着已经释放,>0意味着已经被占用。
为什么不是1呢,原因就是可重入锁,每进入一次锁,
就会占用一重,state都会加一次。 */
private volatile int state;

2、Node的重要属性

/** 一个final类,可以公开的一个默认节点 */
static final Node SHARED = new Node();
/** 用于指示节点正在独占模式下等待 */
static final Node EXCLUSIVE = null;
 
/** 枚举,已经取消,该节点的线程或许被中断或超时 */
static final int CANCELLED =  1;
/** 此节点的后续节点已(或即将)被阻止(通过park),
*因此当前节点在释放或取消时必须取消其后续节的连接为了避免竞争,
*acquire方法必须首先指示它们需要信号,然后重试原子acquire,然后在失败时阻塞
*/
static final int SIGNAL    = -1;
/** 此节点当前位于条件队列中。 */
static final int CONDITION = -2;
/**
 * releaseShared应该传播到其他节点。
这是在doReleaseShared中设置的(仅针对头部节点),
以确保传播继续进行,即使其他操作已经介入。
 */
static final int PROPAGATE = -3;
/** 传播状态,里面的值都使用上面的几个枚举 */
volatile int waitStatus;
/** 双向队列指向前面的指针 */
volatile Node prev;
/** 双向队列指向后面的指针 */
volatile Node next;
/** 等待获取锁的线程,
*如果一个锁获取成功则直接把这个存的设置成null,
*因为获取成功了,这个记录已经没用了;
*如果获取失败则可能会被挂起。
*这个挂起的动作才是存储thread的意义。
*AQS并不控制线程的执行,只控制线程的挂起。
*/
volatile Thread thread;
Node nextWaiter;
 
final boolean isShared() {
    return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

3、获取独占锁(在队列里加入一个Node,然后获取同步状态)

/**
* 首先调用自定义同步器实现的tryAcquire(int arg)方法,
* 该方法保证线程安全的获取同步状态。如果同步状态获取失败,
* 则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)
* 并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部。
* 最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。
* 如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的
* 出队或阻塞线程被中断来实现。
*/
public final void acquire(int arg) {
    if (!tryAcquire(arg) && 
 /** 这里的Node.EXCLUSIVE是个null,意味着他的后继结点是null的 */
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
 
private Node addWaiter(Node mode) {
   /** 注意这个时候创建的node里面保存的是当前线程,代表这个线程在尝试获取锁 */
    Node node = new Node(Thread.currentThread(), mode);
    /** 快速尝试吧本节点设置到尾部,如果有尾巴,而且CAS设置成功了,就返回了,这就叫“快速” */
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    /** 如果快速设置tail失败,则死循环不断尝试CAS设置尾巴 */
    enq(node);
    return node;
}
 
private Node enq(final Node node) {
    /** 死循环不断尝试CAS设置尾巴 */
    for (;;) {
        Node t = tail;
        if (t == null) {
/** 如果是第一次来,先初始化尾巴为头。当然也是CAS操作,如果设置不成功也没关系,死循环嘛 */
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            /** 真正的把节点设置到尾部,当然,返回之后当然得取消这个节点 */
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

注意 AbstractQueuedSynchronizer类里面的已compareAndSet开头的方法,都是CAS方法,其底层调用的都是native方法,在java里没有实现

下面来看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)) {
/** 把本节点设置成头节点,把老的头节点的next设置为空,方便回收 */
/** 这里只是个锁,不做任何业务逻辑处理,所以获取到锁之后可以直接return中断状态。
* 而且不需要返回获取锁状态,因为没有获取到锁的都在这里自旋呢,获取到锁的才会返回出去
*/
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
/** 这里先调用shouldParkAfterFailedAcquire判断是不是可以挂起,
* 而parkAndCheckInterrupt具体执行挂起并返回线程中断状态,
* 如果是中断的,则返回true。这个方法没啥内容,不展示了 */
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        /** 如果失败了,取消这个节点,其实只有上面的自旋抛异常才会走到这里 */
        if (failed)
            cancelAcquire(node);
    }
}
 
 
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /** 如果前置节点是SIGNAL类型,则本节点可以安全的挂起,
        * 因为前置节点的SIGNAL状态承诺自己执行完之后会唤醒后续节点 */
        return true;
    if (ws > 0) {
        /** 如果前置节点是cancle的,则循环往前找到不为cancle的节点,同时更改连表结构 */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /** 其他情况,则把前置节点设置为signal */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    /** 注意上面两个if分支都没有返回,
    * 直接走到这里返回了false,不能挂起。
    * 可能是因为希望外部再检查一次 
    */
    return false;
}
 
/** 其实就是三种情况,
* 1、如果是尾节点则直接设置为空。
* 2、如果是头则唤醒下一个节点,把自己放弃。
* 3、如果是中间节点,则改变链表结构 
*/
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    node.thread = null;
    Node pred = node.prev;
    /** 跳过所有已经消除的节点,并改动链表结构 */
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    Node predNext = pred.next;
    /** 设置本节点为取消 */
    node.waitStatus = Node.CANCELLED;
    /** 如果本节点是tail,则通过CAS方式把pred节点与本节点替换 */
    if (node == tail && compareAndSetTail(node, pred)) {
        /** 通过CAS把尾节点设置为null */
        compareAndSetNext(pred, predNext, null);
    } else {
        /** pred不是头结点 && pred的线程不为空 
        * && pred.ws = singal 利用CAS把node的next设为pred的next节点 
        */
        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
    }
}

3、释放独占锁

比较有意思的是,所有把节点消除的方法都在获取锁的方法的finally里面。因为上一个线程释放锁的动作会触发下一个线程的获取锁的finally动作,因为自旋退出了

/** 释放的逻辑 */
public final boolean release(int arg) {
	/** 这个是子类方法实现的尝试释放逻辑 */
    if (tryRelease(arg)) {
        Node h = head;
/** 头节点不是null,而且waitStatus不是0,为啥不是0呢,与unparkSuccessor里有关,
* 他的第一步就是通过CAS设置waitStatus为0,一种不在正常枚举类型中的状态。为什么这么设置呢,
* 因为上面加锁的过程可以看到,我们在获取到锁Node节点退出后,是把这个将要退出的节点设置给了head,
* 原来的head抛弃。那在这个过程中,该节点可能还没有完成抛弃,那怎么才能标志抛弃呢?
* 就是waitStatus设置一个绝对不存在的枚举 
*/
        if (h != null && h.waitStatus != 0)
			/** 唤醒下一个节点 */
            unparkSuccessor(h);
        return true;
    }
    return false;
}
/** 唤醒下一个节点 */
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
		/** 把本节点设置为0,因为本节点不是被删除,就是已经获取到锁准备退出了 */
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
		/** 如果下个节点是空或者已经取消,那么从尾部开始找到离node最近的有效节点 */
        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);
    }

4、占用共享锁

AbstractQueuedSynchronizer类中有专门给共享锁获取和释放的方法。共享锁就是在一些特殊状态下,可以允许多个线程同时访问共享空间,例如读。Node中非常重要的结构是这个

// 下一个节点,是共享锁的非常重要的东西
Node nextWaiter;

本身AbstractQueuedSynchronizer就是一个双向队列,队列头意味着获取到锁。同时每个node里面还有个链表,头就是nextWaiter,这个链表里面存储的都是希望持有共享锁的线程,回过头来看加共享锁的方法

public final void acquireShared(int arg) {
    /** 尝试获取能否加共享锁,在子类里面实现。<0意味着获取锁失败,
    * 加入队列;等于0意味着获取成功但是后面的线程不能再次获取锁了;
    * >0意味着后面的线程也可以获取到锁 */
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
 
private void doAcquireShared(int arg) {
    /** 以共享锁的方式构建节点,addWaiter在上面已经提到了,
    * 看进去就可以明白,新建了一个Node把nextWaiter设置成了一个空node。
    * 其他的跟独占锁一样 */
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        /** 自旋检查自己是不是第一个节点,如果是第一个则获取到了锁 */
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    /** 设置头,并唤醒接下来的所有的共享锁 */
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
 
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; /** 记录老head */
    /** 把本节点设置成head */
    setHead(node);
    /** 如果自定义实现的是>0也就是说后续节点也可以获取到锁,
    * 或者头节点是null,或者头节点是失效的,或者新的头是null,
    * 或者新的头是失效的 */
    /** propagate > 0很有意思,这个是tryAcquireShared返回的,
    * 前面说过,如果返回=0,只能有一个共享锁占有,因此不能唤醒后面的节点 */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        /** 如果下个节点是空的,或者需要获取的是共享锁,那么就把后面的所有共享锁都唤醒;
        * 换句话,如果后面的节点是独占锁,那么不能唤醒,得等他自动获取到锁才行 */
        if (s == null || s.isShared())
            doReleaseShared();
    }
}
/** 释放共享锁 */
private void doReleaseShared() {
    /** 死循环是为了查看是否有队列结构的变更 */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            /** 如果是signal状态,则先通过CAS设置为0,
            * 就是head应有的状态,如果成功则释放head的下一个节点 */
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            } 
    /** 如果head已经是0,则设置头告诉现在是共享模式下呢,
    * 然后一直循环下去,直到后面获取到锁的节点把现在的head给替换掉 */
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        /** 跳到这里只可能有两种情况,
        * 1、已经释放了该释放的 
        * 2、上面的if直接跳过了,根本没走。所以break跳出循环 */
        if (h == head)                   // loop if head changed
            break;
    }
}

4、释放共享锁

/** 这个就很简单了,上面的release已经有了 */
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

5、占用有超时时间的独占锁

这部分没有什么特殊的,只不过有一个超时时间,死循环发现超过了一定时间后就退出,获取锁失败。而释放锁的动作与正常的释放独占锁一样,就不单独写了

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值