走进AQS体系(一)—— 独占模式下的AQS

文章很长,请耐心观看,有不同见解的地方欢迎指出,谢谢大伙

Node节点内容

static final class Node {
    // 节点引用相关开始
    // 用于表明节点是处于共享模式
	static final Node SHARED = new Node();
    // 用于表明节点是处于独占模式
    static final Node EXCLUSIVE = null;
    // 上一个节点
    volatile Node prev;
    // 后一个节点
    volatile Node next;
    
    // Condition模式使用,下一个等待条件的节点 
    Node nextWaiter;
    // 节点引用相关结束
    
    
    // 状态值开始
    static final int CANCELLED =  1; // 表明节点对应的线程已被取消
    static final int SIGNAL    = -1; // 表明后继节点的线程需要被唤醒
    static final int CONDITION = -2; // 表明线程正在等待某条件
    static final int PROPAGATE = -3; // 表明后续的共享锁需要无条件传播
    volatile int waitStatus;         // 当前节点保存的线程对应的等待状态
    // 状态值结束
    
    volatile Thread thread;           // 当前节点保存的线程
    
    
    // 是否为共享模式   
    final boolean isShared() {
    	return nextWaiter == SHARED;
    }
    
    // 获取前置节点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    
    Node() {    //用于初始化头节点或者共享标识
    }
    Node(Thread thread, Node mode) {     // 用于添加队列waiter,mode一般使用到的是SHARED或者EXCLUSIVE
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // 用于添加条件队列对象, waitStatus一般是CONDITION
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

CLH队列结构

在这里插入图片描述

CLH队列获取资源流程描述

  1. 尝试获取资源(子类锁需要提供的自定义方法tryAcquire);
  2. 获取不到资源,则入队(自旋+CAS直到入队成功);
  3. 唤醒入队节点 (也是线程被阻塞的位置);
  4. 如果入队节点的上一个节点是头节点,则直接尝试获取锁,如果获取成功就执行,并将其设置为头节点;
  5. 如果尝试获取锁不成功或者上一个节点非头节点,则判断其是否应该park(额外的处理: 前一个节点为SIGNAL,应该直接park;前一个节点是CANCELLED节点,则调整节点为非CANCELLED节点的后继;前一个节点非CANCELLED,则将其处理成SIGNAL; ),而后继续自旋,直到成功获取资源(因为被park醒来后也是在这个旋转内,会重复执行)
  6. 在1~5过程内异常导致线程在没有获取到资源的同时也没被park,就需要取消该线程当次获取资源的资格,处理流程为将该节点 的thread置空、waitStatus=CANCELLED,同时将CANCELLED节点 移出队列;但如果出现调整后该节点的前驱节点是头节点,则需要尝试唤醒该节点的后继节点;

CLH队列进队流程

AQS请求锁流程

  • addWaiter() 是在线程尝试获取不到锁的情况下将自己入队,此时仅仅是入队而已;

  • 当入队后,会再执行acquireQueued(),此时如果线程尝试获取不到资源,则会再此步骤被park,当被其他线程唤醒,就继续在自旋2里继续尝试获取资源,如果依旧获取不到,继续park;

  • 如果中途线程出现其他问题,如中断异常等,则会走cancelAcquire(),此步骤节点状态会被设置为Node.CANCELLED,节点线程被置空,并且可能会对队列里的CANCELLED节点进行丢弃,也可能会对节点 线程进行唤醒(有多条支路,所以说是可能 操作);

CLH队列进队(获取资源)源码解析

1、AQS队列独占锁获取的模板

public final void acquire(int arg) {
    /**
     * 1、尝试获取资源失败
     * 2、入队
     * 3、(自旋)获取资源,获取不到被park,当被unpark后依旧获取不到资源继续park
     */
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       
        /**
         * 这里需要重新响应 中断的原因是:
         * acquireQueued 内部会获取线程的中断信息进行保存并且返回标识是否被中断
         * 的布尔值,同时在获取的时候会将线程的中断信息擦除,不然无法对其park;
         * 那当整个获取锁的过程走完后,就需要根据之前的中断信息设置是否需要重新
         * 响应中断;
         */
        selfInterrupt();
}

2、tryAcquire(int arg)

// AQS资源尝试获取的定制方法, 由子类提供
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

3、addWaiter(Node mode)

// 按照参数给定的模式(共享或者独占)创建节点并入队
private Node  addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 尾插法+CAS设置尾部节点引用
    Node pred = tail;
    // 相当于队列不为空
    if (pred != null) {
        node.prev = pred;
        // CAS设置尾部节点引用 Unsafe.compareAndSwap
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    
    // 队列为空或者节点创建后CAS入队失败就会执行到这里
    enq(node);
    return node;
}

4、enq(final Node node)

// 将节点插入到队列里
private Node enq(final Node node) {
    // 自旋的原因是因为如果有多个线程操作的话
    // CAS 失败,就可以重新处理,直到正确入队
    for (;;) {
        Node t = tail;
        if (t == null) { // 初始化队列,创建一个空内容节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { // CAS将新节点设置为尾部并入队
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

5、AQS核心方法(非常重要)

// 已进队的节点去尝试获取资源 
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; // 当且仅当该线程获取到了锁才为false
    try {
        boolean interrupted = false; // 当且仅当该线程被park才为true
        /**
         * 当前自旋的作用,当线程走到parkAndCheckInterrupt()会被阻塞,
         * 下次被唤醒后继续往下执行,
         * 走if (p == head && tryAcquire(arg)) 这条支路
         * 如果为上一个节点为头节点并且能够拿到资源则作为头节点,同时线程运行
         */
        for (;;) {
            // 1、获取最新节点的上一个节点
            final Node p = node.predecessor();
            // 2、上一个节点为头部则直接尝试获取资源
            //    成功的话将node设置为头部、节点线程设置为空
            if (p == head && tryAcquire(arg)) {
                setHead(node);  
                p.next = null; // help GC
                failed = false;
                
                // 这里的interrupt实际返回的是Thread.interrupted()
                return interrupted;
            }
            /**
             * 3、获取资源失败或者当前节点的上一个节点并非头节点
             *    如果这个节点的前一个节点是SIGNAL,
             *    则直接Park并且在被唤醒之后返回线程是否被中断的标志, 
             *    如果前一个节点非SIGNAL,则继续循环直到获取到资源或者阻塞 
             */
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        /**
         * 顺利的情况下try里面的节点肯定是会获取到资源的,
         * 此时走到这一步failed=false
         * 但线程出现中断异常或者其他形式的异常,failed=true,
         * 则需要取消acquire,个人理解为修复节点状态
         */
        if (failed)
            cancelAcquire(node);
    }
}

6、shouldParkAfterFailedAcquire(Node pred, Node node)

// 该方法会针对上一个节点和当前节点去校验,最终返回当前节点是否应该park的布尔值
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    /**
     * 1、如果上一个节点的状态为SIGNAL
     *    则node最终会被前一个节点唤醒,此时安心park
     */
    if (ws == Node.SIGNAL)
        return true;
        
    // 往下的内容都暂时不被Park,因为不确定node能不能被唤醒
    
    /**
     * 2、如果上一个节点的状态为CANCELLED
     *    则往前找到非取消的节点,并将新节点的prev指向第一个非CANCELED的节点
     *    相当于丢弃中间这一大堆取消了的节点
     */
    if (ws > 0) {
        do {
            // pred = pred.prev;
            // node.prev = pred;
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /**
         * 3、前一个节点的状态 == 0 || == SIGNAL || == CONDITION || == PROPAGATE
         *    不论为上面四个状态值的哪个,都将其改为SIGNAL, 
         *    因为新节点入队后需要由前置节点唤醒
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

7、parkAndCheckInterrupt()

 // 该方法用于阻塞线程、设置锁和归还锁,并返回线程中断状态
 private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);

    /**
     * 这一步返回往往会被人忽略。但恰恰这句话个人觉得算是acquireQueued()的核心之一
     * 因为上一步虽然被park了,但程序唤醒过来后,咱也无法直接明确的说它就是被unpark的,
     * 毕竟也有可能是中断导致的唤醒,所以此时需要记录中断状态,同时也对其做个状态重置。
     *
     * 那假如获取了锁,线程继续跑,此时就有必要利用已保存的中断信息去重新响应中断。
     */
     return Thread.interrupted();
 }

8、LockSupport.park(Object blocker)

public static void park(Object blocker) {
    // 1、给当前线程设置锁; 2、park当前线程; 3、当线程被唤醒就将锁释放掉;
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

9、cancelAcquire(Node node)

// 此方法用于将节点取消,移出获取资源队列,同时有补充唤醒CANCELLED节点的后续节点的作用 
private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;
    /**
     * 1、将节点的thread置空,waitStatus设置为CANCELLED
     *    找到第一个非cancelled的节点,将节点的prev指向第一个非CANCELLED的节点
     *    这里可能会疑惑为什么thread和waitStatus的信息修改不需要CAS?
     *    原因是此处current线程修改自己的节点信息,不存在其他线程争抢
     */
    node.thread = null;
    // 找到第一个非cancelled的节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    // 用于后续CAS处理其他节点的后置节点
    Node predNext = pred.next;
    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;
    // 走到此处node的thread为空,waitStatus为CANCELLED
    /**
     * 2、如果当前节点为tail,则将当前节点的prev设置为tail
     *    因为此时当前节点已经为cancelled,移出队列
     */
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        /**
         * 3、如果当前节点非tail,则需要处理两个事情 
         *     3-1、如果当前节点的prev节点非队列的首节点(执行节点)
         *          则将当前节点的后续节点入队(移除CANCELLED节点)
         *     3-2、如果当前节点的prev为执行节点
         *          则需要尝试唤醒当前节点的后续节点
         */
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        // pred不是头节点、
        // (pred的状态是SIGNAL或者成功将pred的状态设置为SIGNAL)
        // pred的线程非空(也就是还没执行)
        // 则需要将当前节点后面的节点做一个链接
        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 {
            // 1、pred是头节点; 
            // 2、pred无法最终设置为SIGNAL;
            // 3、pred的线程已经被调度
            
            // 这里说明pred可能是头节点、
            // 并且线程被调度同时已经status为CANCELLED,
            // 即节点线程被取消执行或者执行完了
            // 所以需要对后继节点进行唤醒
            unparkSuccessor(node);
        }
        // TODO 作用暂时还未完全理解
        node.next = node; // help GC
    }
}

注: unparkSuccessor(node)放到释放资源章节一起讨论

会调整队列节点的代码位置, 如丢弃已经取消的节点

  • cancelAcquire(Node node)
  • shouldParkAfterFailedAcquire(Node pred, Node node)
  • enq(final Node node)
  • addWaiter(Node mode)

CLH队列释放资源流程

CLH队列释放资源

CLH队列出队(释放资源)源码解析

1、release(int arg)

// 独占模式下的资源释放方法。如果tryRelease返回true,则唤醒一个或者多个线程。
// 可被用于Lock的unlock()方法
public final boolean release(int arg) {
    if (tryRelease( arg)) {
        Node h = head;
        /**
         * CANCELLED SIGNAL PROGATION CONDITION
         *
         * CANCELLED 为什么也要唤醒?
         * 节点线程执行异常或者被中断等,同样需要释放当前资源, 唤醒后续线程获取处理机
         */
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

2、tryRelease(int arg)

// 独占锁需要重写的释放锁逻辑
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

3、unparkSuccessor(Node node)

// 用于唤醒后继节点   
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;
    /**
     * 1、将非CANCELLED的节点状态置为0
     *      node非中断状态, 为其他状态,
     *      说明线程已经正常执行到释放资源的位置
     */
    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.
     */
    /**
     * 2、唤醒后继节点或从尾往前找到的第一个非CANCELLED节点。
     *      2-1、正常情况下 唤醒情况是发生在node的next节点;
     *      2-2、但如果node的next为空或者已经被取消,
     *           则需要从tail往前找到第一个非空并且不等于node的节点,
     *           同时如果该节点的线程还没被调度则唤醒它;
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            // CONDITION  SIGNAL PROPAGATE 0
            if (t.waitStatus <= 0)
                s = t;
    }

本章完,下节更新条件队列的个人理解O(∩_∩)O

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值