并发编程--AQS源码分析

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
 
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
        //更新节点状态,并通过enq方法,将节点重新加入同步队列中
    final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

一、简介

        AQS全称AbstractQueuedSynchronizer,它是为实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)提供的一个框架。AQS继承了AbstractOwnableSynchronizer类,这个类为创建锁和相关同步器提供了基础。AQS是Concurrent包的核心,lock就是在AQS的基础上实现的,阻塞队列,线程池,信号量等都离不开AQS的支持。

二、同步队列

AQS主要是维护了一个队列同步器,而队列中主要是存储Node节点

AQS类里有一个Node类,对线程进行了封装,每个线程都会被封装成一个Node节点放到同步队列中。每个Node节点保存了当前线程的同步状态,等待状态,前驱和后继节点等。

1.Node节点

static final class Node {
        // 共享模式下等待的标记
        static final Node SHARED = new Node();
        // 独占模式下等待的标记
        static final Node EXCLUSIVE = null;
 
        // 线程的等待状态 表示线程已经被取消
        static final int CANCELLED =  1;
        // 线程的等待状态 表示后继线程需要被唤醒
        static final int SIGNAL    = -1;
        // 线程的等待状态 表示线程在Condtion上
        static final int CONDITION = -2;
        
        // 表示下一个acquireShared需要无条件的传播
        static final int PROPAGATE = -3;
 
        /**
         *   SIGNAL:     当前节点的后继节点处于等待状态时,如果当前节点的同步状态被释放或者取消,
         *               必须唤起它的后继节点
         *         
         *   CANCELLED:  一个节点由于超时或者中断需要在CLH队列中取消等待状态,被取消的节点不会再次等待
         *               
         *   CONDITION:  当前节点在等待队列中,只有当节点的状态设为0的时候该节点才会被转移到同步队列
         *               
         *   PROPAGATE:  下一次的共享模式同步状态的获取将会无条件的传播
 
         * waitStatus的初始值时0,使用CAS来修改节点的状态
         */
        volatile int waitStatus;
 
        /**
         * 当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候才被分配,
         * 并且只在出队的时候才被取消(为了GC),头节点永远不会被取消,一个节点成为头节点
         * 仅仅是成功获取到锁的结果,一个被取消的线程永远也不会获取到锁,线程只取消自身,
         * 而不涉及其他节点
         */
        volatile Node prev;
 
        /**
         * 当前节点的后继节点,当前线程释放的才被唤起,在入队时分配,在绕过被取消的前驱节点
         * 时调整,在出队列的时候取消(为了GC)
         * 如果一个节点的next为空,我们可以从尾部扫描它的prev,双重检查
         * 被取消节点的next设置为指向节点本身而不是null,为了isOnSyncQueue更容易操作
         */
        volatile Node next;
 
        /**
         * 当前节点的线程,初始化后使用,在使用后失效 
         */
        volatile Thread thread;
 
        /**
         * 链接到下一个节点的等待条件,或特殊的值SHARED,因为条件队列只有在独占模式时才能被访问,
         * 所以我们只需要一个简单的连接队列在等待的时候保存节点,然后把它们转移到队列中重新获取
         * 因为条件只能是独占性的,我们通过使用特殊的值来表示共享模式
         */
        Node nextWaiter;
 
        /**
         * 如果节点处于共享模式下等待直接返回true
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
 
        /**
         * 返回当前节点的前驱节点,如果为空,直接抛出空指针异常
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
 
        Node() {    // 用来建立初始化的head 或 SHARED的标记
        }
 
        Node(Thread thread, Node mode) {     // 指定线程和模式的构造方法
            this.nextWaiter = mode;
            this.thread = thread;
        }
 
        Node(Thread thread, int waitStatus) { // 指定线程和节点状态的构造方法
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

AQS中的其他字段

//头节点
private transient volatile Node head;
//尾结点
private transient volatile Node tail;
//等待状态
private volatile int state;

2.同步队列

Node是构成同步队列的基础,看一下Node的结构

并发编程--AQS源码分析_第1张图片

 三、state状态

private volatile int state;
protected final int getState() {
        return state;
}
protected final void setState(int newState) {
        state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

其实各个实现类的实际目标,就是通过判断state来实现资源管理,比如ReentrntLock,只有当state为0的时候才可以获取到锁资源,如果是可重入锁,state递增大于1。释放资源的时候修改state的值state-1,如果是可重入锁,必须所有持锁操作释放锁,最终只有state=0的时候其他等待线程才可以获取锁资源。

四、两种模式--独占式

1.acquire方法

独占式的模板方法为

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

        通过调用acquire的方法获取同步状态,该方法忽略中断,线程获取同步状态失败后,进入同步队列,在对其进行中断操作后,线程不会从同步队列移除。

       (1) 首先调用tryAcquire方法获取同步状态,AQS并没有实现这个方法,具体的实现由它的继承类进行重写,比如ReentrantLock的Sync类。如果获取同步状态成功的直接返回true;如果获取同步状态失败的话,调用addWaiter方法把线程封装成一个Node节点添加到同步队列的尾部

        (2)然后又调用acquireQueued方法使节点以自旋的方式获取同步状态,如果获取同步状态失败,要挂起线程

        (3)最后,线程如果在获取同步状态中和同步队列中被中断过,要进行自我中断。

2.addWaiter方法

 /**
     * 把Node节点添加到同步队列的尾部
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);  // 以独占模式把当前线程封装成一个Node节点
        // 尝试快速入队
        Node pred = tail;  // 当前队列的尾节点赋给pred
        if (pred != null) {  // 先觉条件 尾节点不为空
            node.prev = pred;  // 把pred作为node的前继节点
            if (compareAndSetTail(pred, node)) { //利用CAS把node作为尾节点
                pred.next = node;    // 把node作为pred的后继节点
                return node;       // 直接返回node
            }
        }
        enq(node);  // 尾节点为空或者利用CAS把node设为尾节点失败
        return node;
    }
    /**
     * 采用自旋的方式把node插入到队列中
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 如果t为空,说明队列为空,必须初始化
                if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
                    tail = head;
            } else {    // 尾节点不为空的情况
                node.prev = t;  // 把t设为node的前驱节点
                if (compareAndSetTail(t, node)) {  // 利用CAS把node节点设为尾节点
                    t.next = node;   // 更改指针  把node作为t的后继节点
                    return t;   // 直接返回t
                }
            }
        }
    }

enq方法中采用了非常经典的自旋操作,只有通过CAS把node设为尾节点后,当前线程才能退出该方法,否则的话,当前线程不断的尝试,直到能把节点添加到队列中为止,这样就把并行添加变成了串行添加。

3.acquireQueued方法

/* 
     * 此主要是通过自旋方式获取同步状态
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;  // 默认线程没有被中断过
            for (;;) {
                final Node p = node.predecessor();  // 获取该节点的前驱节点p
                if (p == head && tryAcquire(arg)) {  // 如果p是头节点并且能获取到同步状态
                    setHead(node);                   // 把当前节点设为头节点
                    p.next = null;                  // 把p的next设为null,便于GC
                    failed = false;                 // 标志--表示成功获取同步状态,默认是true,表示失败
                    return interrupted;             // 返回该线程在获取到同步状态的过程中有没有被中断过
                }
                if (shouldParkAfterFailedAcquire(p, node) &&   // 用于判断是否挂起当前线程
                    parkAndCheckInterrupt())
                    interrupted = true;      
            }
        } finally {
            if (failed)   // 如果fail为true,直接移除当前节点
                cancelAcquire(node);
        }
    }
        在acquireQueued(final Node node,int arg) 方法中,当前 线 程在 死循 尝试获 取同步状
,而只有前 驱节 点是 头节 点才能 够尝试获 取同步状 什么?原因有两个,如下。
第一, 头节 点是成功 取到同步状 点,而 头节 点的 线 放了同步状 之后,将会
醒其后 继节 点,后 继节 点的 线 程被 醒后需要 检查 自己的前 驱节 点是否是 头节 点。
第二, 维护 同步 列的 FIFO 方法中, 点自旋 取同步状
并发编程--AQS源码分析_第2张图片

 4.shouldParkAfterFailedAcquire、parkAndCheckInterrupt方法

shouldParkAfterFailedAcquire和parkAndCheckInterrupt两个方法用来让当前节点找到一个合适的地方开始等待


    /**
     * 如果线程获取同步状态失败就要检查它的节点status,要保证prev = node.prev
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;  // 获取当前节点的前驱节点的waitStatus
        if (ws == Node.SIGNAL)     
            /*
             *  如果前驱节点的ws = singal,表示前驱节点释放后会唤起当前线程,    
             *  可以安全的挂起当前线程
             */
            return true;   // 能够挂起当前线程直接返回true
        if (ws > 0) {
            /*
             * 前驱节点的ws > 0,说明ws = Cancelled,表示前驱线程被取消,
             * 从前驱节点继续往前遍历,直到找到第一个前驱节点的ws <= 0 为止
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 这种情况表示前驱节点的 ws = 0 或者 ws = PROPAGATE,我们需要一个singal,但是
             * 不能挂起当前线程 
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

5.总结

(1)调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
(2)没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
(3)acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
(4)如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上

并发编程--AQS源码分析_第3张图片

 五、两种模式--共享式

独占式和共享式的最大不同就是在同一时刻能否有多个线程获取同步状态,通过调用acquireShared方法获取同步状态.

1.acquireShared方法

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    /**
     * 以共享非中断获取同步状态
     */
    private void doAcquireShared(int arg) {
        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);
        }
    }

tryAcquireShared是留给子类去重写的,如果tryAcquireShared方法返回值<0,说明获取同步状态失败,执行doAcquireShared方法,在doAcquireShared再次调用tryAcquireShared方法,判断其返回值,若返回值<0,获取同步状态失败,需要进入同步队列进行等待,

如果返回值=0,说明当前线程获取同步状态成功,其他线程无法获取,也就不需要唤醒它的后继节点进行传播.

如果返回值>0,此时当前线程获取同步状态后要唤醒它的后继节点,让其他线程也尝试去获取同步状态.

独占式获取同步状态之后,直接返回中断状态,结束流程,共享式则调用setHeadAndPropagate方法传播唤醒的动作.
 

2.setHeadAndPropagate方法

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // 保存当前的头节点
        setHead(node); // 把当前节点设为头节点
        /*
         * 这里有三种情况执行唤醒操作:1.propagate > 0,代表后继节点需要被唤醒
         *                          2. h节点的ws < 0或者 h=null
         *                          3. 新的头结点为空 或者 新的头结点的ws < 0
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;    // 找到当前节点的后继节点s
            if (s == null || s.isShared())   // s=null 或者 s是共享模式,调用doReleaseShared方法唤醒后继线程
                doReleaseShared();
        }
    }

3.doReleaseShared方法

接着看一下doReleaseShared方法

 private void doReleaseShared() {
        /*
         * 注意,这里的头结点已经是上面新设定的头结点了,从这里可以看出,如果propagate=0,
         * 不会进入doReleaseShared方法里面,那就有共享式变成了独占式.
         */
        for (;;) {  // 这里一个死循环直到满足条件h=head才能跳出
            Node h = head;
            if (h != null && h != tail) {  // 前提条件-当前的头结点不为null && h不是尾节点
                int ws = h.waitStatus;   
                if (ws == Node.SIGNAL) {  // 如果当前头结点的ws=signal,利用CAS把h的ws设为0
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    unparkSuccessor(h);  // 唤醒头结点的后继节点
                }   // 如果h的ws=0,就把h的ws设为PROPAGATE,表示可以向后传播唤醒
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)  // 如果头结点没有发生改变,表示设置完成,可以退出循环
                break;      // 如果头结点发生了变化,可能被唤醒的其他节点重新设置了头结点
        }                   // 这样头结点发生了改变,要进行重试,保证可以传播唤醒信号
    }

4.同步状态释放


    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {  // 
            doReleaseShared();
            return true;
        }
        return false;
    }

六、ConditionObject内部类

          等待队列,也称之为条件(condition)队列。这个队列和AQS的CLH队列有密切的联系,AQS有Node对象,其有两个用途: 形成等待队列和阻塞队列。Contidion必须在lock的同步控制块中使用,调用Condition的signal方法并不代表线程可以马上执行,signal方法的作用是将线程所在的节点从等待队列中移除,然后加入到同步队列中,线程的执行始终都需要根据同步状态(即线程是否占有锁)。

并发编程--AQS源码分析_第4张图片

1.为什么在平时使用Condition之前要获取锁?

因为我们在平时开发过程中生成Condition对象,都是通过显式锁的public方法newCondition实现的,newCondition方法不是静态方法必须通过具体实例对象调用,因此需要先获取锁对象。

    public Condition newCondition() {
        return sync.newCondition();
    }

   final ConditionObject newCondition() {
        return new ConditionObject();
   }

2.await方法

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();//将当前线程加入到链表最后,并返回该节点
            //释放当前线程占有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            /*
             * while循环通过isOnSyncQueue方法判断节点node是否在同步队列中
             * 
             * 这里有个理解难点,为什么需要判断节点Node是否在同步队列中呢?
             * 因为当线程调用signal或signalAll时,会从firstWaiter节点开始,
             * 将节点依次从等待队列中移除,并通过enq方法重新添加到同步队列中
             * 
             * 因此当其他线程调用signal或者signalAll方法时,该线程可能从条件(等待)队列中移除,并重新加入到同步队列中
             * 1. 如果没有,则阻塞当前线程,同时调用checkInterruptWhileWaiting检测当前线程在等待过程中是否发生中断,
             *    设置interruptMode表示中断状态。
             * 2. 如果isOnSyncQueue方法判断出当前线程已经处于同步队列中了,则跳出while循环
            */
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //利用acquireQueued方法循环尝试获取同步状态(锁)
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();//将等待队列中,不是Node.CONDITION状态的节点移除
            if (interruptMode != 0)//判断中断状态,
                reportInterruptAfterWait(interruptMode);
        }
 
        //将当前线程生成的节点加入到链表末尾,并返回该节点
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果最后一个节点
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();//这个方法就是将等待队列中不是Node.CONDTION状态的节点从链表中移除
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

3.signal方法源码解析

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
 
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
        //更新节点状态,并通过enq方法,将节点重新加入同步队列中
    final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

分析上面的源码,我们可以看出,其实signal方法的核心其实是doSignal和transferForSignal方法,doSignal的主要作用就是将条件(等待)队列中的头节点firstWaiter从队列中移除,transferForSignal方法的主要作用就是将doSignal方法中移除的firstWaiter节点通过enq方法重新添加到同步队列中,从这里也可以看出为什么会在await方法中调用isOnSyncQueue方法判断节点是否处于同步队列中了。

signalAll方法与signal方法原理一样,只是针对的是等待队列中所有的节点。signal针对的是第一个节点.

你可能感兴趣的:(并发编程,java,多线程)