并发多线程之AQS源码分析(上)

目录

    • AQS的实现原理
    • AQS的队列实现Node
    • AQS的核心属性和方法

在JUC原理一篇了解了JUC的都是围绕着AbstractQueuedSynchronizer(简称AQS)实现的,也就是说AbstractQueuedSynchronizer为JUC的核心,本篇就是探索AbstractQueuedSynchronizer的源码。

AQS的实现原理

在开始阅读源码之前再来梳理一下JUC的原理,那么在阅读源码时可以提高效率。

AQS的两个队列
在AQS中存在两个队列用于对线程的状态保存和记录,分别是同步队列和条件队列。
同步队列中的线程可以处于独占和共享两种状态,并且通过队列是一个双端队列。其结构如下:
并发多线程之AQS源码分析(上)_第1张图片
而条件队列中的线程状态必须为独占模式,线程在获取锁时不满足条件,则将线程添加至条件队列并等待唤醒。当条件队列中的线程被唤醒后,将线程从条件队列中剔除并添加至同步队列而等待执行,条件队列是单向链表结构,结构如下:
并发多线程之AQS源码分析(上)_第2张图片
在AQS中,实现队列的是通过AbstractQueuedSynchronizer类中的一个静态内部类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;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        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;
        }
 }

具体的字段和方法分析在下面详细解释。

AQS的工作流程
AQS的工作机制流程如下:
并发多线程之AQS源码分析(上)_第3张图片
这个的流程是独占锁的工作流程,流程如下:

  1. 首先线程获取锁,当线程获取锁失败,将当前线程添加至同步队列中;
  2. 将当前线程放入同步队列中后,线程仍然是通过for循环自旋转进行锁的争夺(不是直接挂起,类似于sychronized的自旋锁优化)。
  3. 在自旋转过程中,当前线程还是会争抢锁(可能存在当前线程还没挂起持有锁的线程已经执行完毕),同时会将当前线程在同步队列中的前一个节点状态修改为SIGNAL(该状态用于表示节点需要被唤醒)。
  4. 在自旋转过程中既没有获取锁,同时也将当前线程在同步队列中的前置节点状态修改为SIGNAL,则将当前线程阻塞。
  5. 当获取锁的线程执行完毕后,则从同步队列中从头节点开始向后遍历,唤醒第一个阻塞的线程。
  6. 线程被唤醒后,继续从步骤2继续执行,如此反复知道获取锁。
  7. 线程获取到锁后并执行完代码则唤醒队列中的线程。

AQS的原理机制及流程介绍完了,接下来来分析AQS相关的源码。当然本篇暂时不介绍条件队列的流程。

AQS的队列实现Node

首先从AQS中实现同步队列和条件队列的Node类说起。首先来介绍下其相关的属性及作用。

  1. 独占和共享的标识。

    	/** 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;
    

    锁分为独占锁和同步锁,而这两个标识用于表示线程是独占模式还是共享模式。

  2. 标识线程的状态。

    	static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
    

    CANCELLED :值为1。当线程等待超时或者被中断,则取消等待,设等待状态 为-1,进入取消状态则不再变化。
    SIGNAL : 值为-1。后继节点处于等待状态,当前节点(为-1)被取消或者中断 时会通知后继节点,使后继节点的线程得以运行。
    CONDITION值为-2。当前节点处于等待队列,节点线程等待在Condition上,当 其他线程对condition执行signall方法时,等待队列转移到同步队列,加入到对同步状态的获取。
    PROPAGATE:值为-3。与共享模式相关,在共享模式中,该状态标识结点的线程处 于可运行状态。

  3. 指定线程的等待状态。

    volatile int waitStatus;
    
  4. 实现队列的前置指针和后置指针。

    volatile Node prev;
    volatile Node next;
    
  5. 节点持有的线程。

    volatile Thread thread;
    
  6. 条件队列单向指针。

    Node nextWaiter;
    

    当然这个属性有两个用途,在条件队列中该属性为指向下一个节点的指针,当在同步同列中用于表示当前节点的线程是独占模式还是共享模式。

  7. 判断当前线程是独占还是共享。

    final boolean isShared() {
           
            return nextWaiter == SHARED;
     }
    
  8. 获取当前节点的前置节点。

    final Node predecessor() throws NullPointerException {
           
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
    
  9. 构造方法。

     	Node() {
               // Used to establish initial head or SHARED marker
        }
    
        Node(Thread thread, Node mode) {
                // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
    
        Node(Thread thread, int waitStatus) {
            // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    

    第一个无参构造,第二个是根据线程和锁占有模式构造独占节点还是共享节点。第三个指定节点的状态。

Node类的属性和方法相对来说没有那么复杂。接下来来梳理AbstractQueuedSynchronizer的主要方法,这里主要是独占锁相关的方法(共享锁的相关方法大同小异)。

AQS的核心属性和方法

这里梳理的属性和方法均是独占锁相关的属性和方法,同时并没有对可中断和指定时间等待方法做讲解,当然一下方法熟悉后那些功能也容易理解多了。

  1. 记录同步队列中头节点和尾节点属性。

     private transient volatile Node head;
     private transient volatile Node tail;
    

    等待队列的头节点,懒加载模式,除去初始化,只能在setHead中修改
    注意:如果头结点存在,它的状态waitSattus不能为CANCELLED,即1。
    等待队列的尾部,懒加载模式,只能在enq方法中新增加等待节点时修改。

  2. 资源状态。
    同步状态,用于统计获取锁的次数。

  3. 状态获取和修改方法。

    protected final int getState() {
           
        return state;
    }
    protected final void setState(int newState) {
           
        state = newState;
    }
    
  4. CAS设置state状态。

    protected final boolean compareAndSetState(int expect, int update) {
           
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
  5. 节点入队方法。

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

    将参数节点追加到同步队列中,该方法返回队列中原尾部节点。
    1>如果尾部节点为空,则初始化头部和尾部节点,新建一个Node实例(该Node实例 的waitStatus为int默认值为0),将head和tail指向该Node实例,然后将参数节点追加到新建的队列中。
    2>如果尾部节点不为空,队列存在,则将参数节点追加到队列中。
    该方法通过循环和CAS算法保证数据正确

  6. 向同步队列中增加节点。

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

    1>根据参数mode常见指定模式的节点。
    2>判断tail尾部节点是否问空,不为空尝试添加至同步队列,成功则返回新建的节点。
    3>不成功,则调用enq方法入队。
    4>返回新建节点。

  7. 设置头节点。

    private void setHead(Node node) {
           
        head = node;
        node.thread = null;
        node.prev = null;
    }
    

    将head指向参数节点,并将参数节点的线程置为空,将参数节点前置节点置为空。该方法是用在独占锁获取到锁之后,将获取到锁的当前节点这是为头节点。

  8. 唤醒后置节点。

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

    唤醒参数的后续节点。
    1>参数节点的waitSatus<0,则将其waitSatus修改为0。
    2>判断参数节点的后置节点waitStatus是否是<=0,如果是则唤醒。
    3>如果不是,则从同步队列中由尾部向前遍历,查找最靠前的满足thread不为空并且 waitStatus<=0的几点,将其线程唤醒。
    该方法用于在锁资源释放后然后调用该方法唤醒后续的线程。

  9. 取消抢占资源。

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

    1>忽略空节点。
    2>参数节点线程置为空。
    3>由给定节点向前遍历找到第一一个没有被取消的节点(pred)。
    4>如果参数节点为tail,则将tail指向pred并将pred指向null结束。
    5>如果参数节点不是tail,判断pred不是头节点,并且pred的状态为SIGNAL或者修改SIGNAL成功,则将参数节点后的节点追加到pred后,如果不满足则唤醒后续节点。
    主要就是将队列中的取消节点剔除,同时判断后继节点是不是追加到了head节点之后,如果是则就需要唤醒了,因为唤醒线程就是从头结点往后遍历唤醒的。

  10. 判断当前线程是否满足挂起的条件。

    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> 参数节点node的前置节点pred的waitStatus为SIGNAL,则返回true,证明当前 线程可以被阻塞,返回true。
    2> 如果前置节点pred的waitStatus>0则证明被取消,那么由参数节点node从队列中向前遍历,查找到第一个没有被取消的节点,将node追加到找到的节点。
    3>如果节点pred的waitstatus不是SIGNAL,则将pred节点状态修改为SIGNAL。2和3均未空,即不满足阻塞条件,所以需要继续抢夺资源并不断更改队列中位于node之前的节点的状态。
    该方法是在线程没有获取到锁时自旋转中一直调用的方法,因为一个线程被阻塞必须将其前置节点的状态修改为SIGNAL(因为唤醒时是判断状态为SIGNAL的),所以当前线程的前置节点状态没有修改为SIGNAL则当前线程会一直自旋转尝试将前置节点修改为SIGNAL。当然会存在这种情况:在当前线程一直循环修改前置节点状态时,前置节点获取到了锁,那么前置节点会将状态修改为0,则当前线程会继续循环,要么前置节点执行完毕,要么当前线程将前置节点状态修改为SIGNAL并阻塞当前线程。

  11. 自我中断。

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

    该方法调用了Thread的interrupt()方法修改了线程的状态。

  12. 唤醒线程并判中断状态。

    private final boolean parkAndCheckInterrupt() {
           
        LockSupport.park(this);
        return Thread.interrupted();
    }
    
  13. AQS核心方法。

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

    该方法实现与sychronized的轻量级锁的自旋类似。
    1>在循环不断判断是否队列中的第一个节点如果是则抢占资源,抢占成功则将参数节点置为head。
    2>如果不满足上述条件则将参数节点的前置节点waitStatue置为SIGNAL,成功则通过调用LockSupprot的park方法阻塞当前线程。
    3>如果不成功则一直循环尝试修改参数节点前置节点的waitStatus,指导成功阻塞当前线程。
    注意:在修改前置节点的waitStatus时也会一直判断参数节点是否是同步队列中的第一个节点,如果是就会抢占资源的
    4>在方法最后,如果线程被中断则调用10 cancelAcquire方法,将参数节点从同步队列中剔除,如果有必要唤醒后续节点。
    该方法的逻辑就是AQS的工作流程的实现,包括自旋转、修改前置节点状态、判断线程中断状态以及取消抢占等。

  14. 抢占资源方法。

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

    如上述代码所示,先通过tryAcquire()方法抢占资源,抢占不成功则调用addWaiter创建新节点并通过acquireQueued方法入队如果线程被中断过,则从同步队列中剔除并中断自己。

  15. 释放资源。

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

    该方法为释放资源,如果当前线程多次进入锁,即state释放一次后仍不为0,则该方
    方法返回false。

  16. 关于AbstractQueuedSynchronizer中未实现的几个方法:

    protected boolean tryAcquire(int arg)
    protected boolean tryRelease(int arg)
    protected int tryAcquireShared(int arg)
    protected boolean tryReleaseShared(int arg)
    

在AbstractQueuedSynchronizer中已经将所有的基础架构方法实现,那么通过这四个方法从而可以自己扩展实现不同的功能。

以上是介绍了AQS的独占模式的相关方法,下篇介绍其条件等待队列的运行机制及源码并发多线程之AQS源码分析(下)。

你可能感兴趣的:(并发多线程,并发编程,lock,juc,源码)