AQS学习记录

AQS学习记录

一、AQS的独占模式

1、获取资源

acquire方法用于线程获取资源,获取不到就进入等待队列,并设置为独占模式,成功后,设置线程中断

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

AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,这里的接口没有定义成abstract是因为独占模式下只需实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。因此没必要定义成abstract类型。

   protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

如果获取资源失败,就调用addWaiter方法,构造当前线程的Node节点,设置为等待队列的尾节点

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 通过CAS,将新的Node设置为tail节点
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node); // 如果设置尾节点失败,则通过自旋的方式来设置尾节点
        return node;
    }

设置尾节点失败时,则调用enq(final Node node)方法,通过自旋的方式,将当前node设置为尾节点直到成功。

private Node enq(final Node node) {
        for (;;) {
            //这里有个细节,将volatile变量tail赋给t,这样不用每次去主存读取,提高了效率
            Node t = tail;
            // 如果 tail 是 null,就创建一个虚拟节点,同时指向 head 和 tail,称为 初始化。
            // 每个节点都必须设置前置节点的 ws 状态为 SIGNAL,所以必须要一个前置节点,
            // 对于第一个节点怎么办?它是没有前置节点的,这里需要创建一个假的。
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                 //通过CAS将新节点追加到 tail 节点后面,并更新队列的 tail 为新节点,直到成功
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

现在再回头看acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,该方法则是通过自旋方式获取资源,for循环中:判断当前node的前驱节点是不是Head节点(Head节点仅仅代表头结点,里面没有存放线程引用,表示当前线程已经获取到资源),如果是,则当前node尝试获取资源,获取资源成功,则将自己更新为Head节点,未获取到资源,则waite。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true; //标记是否成功拿到资源
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //获取node的前驱节点,注意node现在是尾节点了
   
                if (p == head && tryAcquire(arg)) {
                    setHead(node); //node设置为Head节点(Head节点不存放线程引用)
                    p.next = null; // 旧的头结点出列,帮助GC回收
                    failed = false;
                    return interrupted; //返回node中线程在等待过程中是否被中断过
                }
                // 当前线程进入waite状态,且将前驱节点的转态设置为signal,用于唤醒自己,返回中断状态状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true; //如果等待过程中被中断过就将interrupted标记置为true
            }
        } finally { 
            if (failed)
                cancelAcquire(node); //仅在获取锁发生异常的时候,取消当前node获取资源的资格
        }
    }

shouldParkAfterFailedAcquire方法主要是将前驱节点的转态设置为signal(如果前驱转态>0,即取消转态,那么就跳过它,找到未被取消的节点作为当前node的前驱节点),这样当前node就可以安全地进入阻塞状态,一旦waiteStatus为signal的前驱节点释放锁,就会唤醒当前Node。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
         // 如果他的上一个节点的 ws 是 SIGNAL,他就需要阻塞。
        return true;
    if (ws > 0) {
         // 如果前驱节点的状态为取消,则将前驱节点的前驱作为node的前驱
         // 按此流程,一直找到未取消状态的前驱节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         // 如果没有取消 || 0 || CONDITION || PROPAGATE,那么就将前任的 ws 设置成 SIGNAL.
         // 必须是 SIGNAL ,是希望自己的上一个节点在释放锁的时候,通知自己(让自己获取锁)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt该方法用于阻塞当前的线程,并返回当前线程的中断状态(注意:Thread.interrupted()方法会清除中断状态)

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);  //将当前线程阻塞
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}

上面提到acquireQueued(final Node node, int arg) 中,获取锁的方式是一个死循环,成功:会将的标志位failed(标记当前Node是否成功获取资源)置为false,如果失败:抛出异常,则执行执行finally中cancelAcquire(node)方法,取消当前线程请求资源的操作。该方法负责将需要取消的node踢出等待队列

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;

        // 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是tail节点,则比较简单,直接移出队列,将pred设置为tailed节点
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
          // 如果pred不是head节点,则一定要设置它的ws为signal,用于通知后面节点去获取资源
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                //走到这里,说明node不是tail,则next!=null? 这里将满足条件的next设置为pred.next,否则维持不变
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

当上面的方法cancelAcquire(node)中,pred为head节点,或者未成功设置signal时,如果取消了node,那么后继节点就没人能唤醒,unparkSuccessor(node) 方法负责从tail开始遍历,找到node后面第一个未被取消的节点,将其唤醒。
这里解析下:之所以从尾部开始遍历,是为了能保证遍历到所有的节点.比如,当前node是前tail节点,新的node2正在变成tail节点,但是addWaiterpred.next = node;并不是原子操作,很可能这步还未来得及执行,如果正向遍历,node.next,会为null,就会遗漏新加入的节点,但是从tail开始遍历肯定没问题,因为在设置tail节点时,compareAndSetTail(pred, node)是原子操作。

 private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //从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)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

释放资源

释放资源的的方法tryRelease(arg)tryAcquire(arg)一样,都是交给子类去具体实现,成功释放资源后,需要调用unparkSuccessor(h),从tail开始,唤醒一个等待状态为非取消状态的node。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

内部类Node中几个参数

  • static final int CANCELLED = 1 : 表示当前的线程被取消
  • static final int SIGNAL = -1 : 前驱节点等待状态设置为SIGNAL,在释放锁时,会通知后继节点。
  • static final int CONDITION = -2 :该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • static final int PROPAGATE = -3 :与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
  • 0状态:值为0,代表初始化状态, AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态。

你可能感兴趣的:(java)