`
春花秋月何时了
  • 浏览: 39746 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

Java并发包核心框架AQS之一同步阻塞与唤醒

 
阅读更多

一、引言

AQS,是AbstractQueuedSynchronizer的简称,它是Java5构建锁或者其他同步组件的基础核心框架,是理解整个Java并发包最关键的类,没有之一。同时,AQS解决了实现同步器时的大量细节问题,例如同步状态的基本操作、FIFO同步阻塞队列操作、Condition条件阻塞唤醒队列操作。基于AQS构建的同步器不但极大的减少了实现工作,而且还不必考虑处理竞争问题,所以Java并发包中的大部分同步器都是基于AQS实现的。使用AQS的主要方式是类继承,通过继承AbstractQueuedSynchronizer,并覆写特定的方法来实现具有不同功能的同步组件,因此,我们也非常容易实现自定义的同步组件,但这都要建立在真正的理解了AQS的原理基础上。

 

二、AQS同步组件概述与结构

        为了能够对AQS能够深入的理解,我们先从其类的结构开始入手(以下所有的分析与源码都基于JDK8)。通过分析JDK8中的AbstractQueuedSynchronizer源码,其内部结构从类层次大致分为三部分:

1)AbstractQueuedSynchronizer类本身2)静态内部类Node3)内部类ConditionObject

其中,AbstractQueuedSynchronizer类自身的大部分 + 静态内部类Node:主要是对同步状态的基础操作、FIFO同步阻塞队列的维护,相当于Lock锁的获取/释放操作。而AbstractQueuedSynchronizer类的小部分 + 内部类ConditionObject:主要实现了条件阻塞/唤醒队列的维护,提供了类似于Object类中的wait/notify/notifyAll提供的功能。

        关于内部类ConditionObject提供的条件阻塞唤醒队列(条件队列中的节点被唤醒之后会被转移到同步等待队列)相关的深入理解,由于其工作原理也建立在AbstractQueuedSynchronizer和静态内部类Node实现的FIFO同步阻塞队列维护基础上,所以我把它放在后面的章节进行介绍。我们先从同步状态的维护和FIFO同步阻塞队列的维护开始入手。AQS定义了以下两种资源共享方式:

1)Exclusive,独占方式,即独占式同步组件(又称独占锁):在任一时刻只能有一个线程获取到锁,可以执行。其他线程被阻塞,进入同步等待队列。如ReentrantLock。

2)Share,共享方式,即共享式同步组件(又称共享锁):通常情况下,共享锁内部维护了若干个执行许可,每个线程执行的时候获取一个或若干个许可,运行结束的时候释放许可。当一个线程请求执行时,如果共享锁剩下的许可数为0或者小于该线程请求的许可数,那么该线程就进入同步等待队列(如Semaphore/CountDownLatch),可见共享模式下,运行多个线程同时运行。其实,可以认为独占式同步组件是共享式同步组件的一个特例,其只有1个许可。

       通过对两种同步组件的理解可见,不论哪种同步组件,其工作原理都离不开在所谓的“执行许可”的获取与释放过程中对许可数量的维护同步等待队列的维护。而这两项工作也正是AQS的核心内容。

2.1 同步等待队列的维护

     AQS结合静态内部类Node对同步阻塞等待队列进行了完全的支持,因为AQS是基于模板方法设计的,它可以在我们调用相应的模板方法的时候,自动帮我们维护一个FIFO的线程等待队列,具体的说就是,在线程获取锁失败之后,将线程阻塞并封装成一个Node对象,然后加入到等待队列队尾。当等待队列中的线程被唤醒之后成功获取到锁,然后执行结束后,自动帮助我们将其对应的Node对象从等待队列中移除。这个FIFO等待队列被严格限制为先进先出,因此AQS框架不支持基于优先级的同步策略。

     AQS中维护的以下两个变量分别表示FIFO线程等待队列的头和尾:

private transient volatile Node head;

private transient volatile Node tail;

     该FIFO同步等待队列依赖一个双向链表来完成对同步等待队列状态的管理,其中每一个节点都是通过静态内部类Node构造的,节点中有一个32位的整型字段waitStatus字段标识的状态位与其线程状态密切相关。

static final class Node {
    //表示该节点在共享模式下等待
    static final Node SHARED = new Node();
    //表示该节点在独占模式下等待
    static final Node EXCLUSIVE = null;
    //下面是waitStatus字段的四种取值:CANCELLED,SIGNAL,CONDITION,PROPAGATE. 

    //超时或被中断时,节点被设置为取消状态,被取消的节点不参与锁竞争,也不能转换为其它状态,只能被踢出队列被GC回收。
    static final int CANCELLED =  1;
    //表示该节点的后继节点被阻塞了,当前节点释放资源时需要唤醒它。
    static final int SIGNAL    = -1;
    //表示该节点基于Condition对象发生了阻塞等待,处于条件队列中,等待被Condition唤醒。
    //当其他线程对Condition调用了signal()\signalAll()后,该节点将会从条件等待队列中转移到同步等待队列
    static final int CONDITION = -2;
    //使用在共享模式头节点有可能处于这种状态,表示共享模式下的释放操作应该被传播到其他节点
    static final int PROPAGATE = -3;
	//节点状态位,新节点的初始状态位为0.可以代表正在尝试去获取临界资源的线程所对应的Node的状态	
	volatile int waitStatus;
	//前驱节点
	volatile Node prev;
	//后继节点	
	volatile Node next;
	//代表的线程	
	volatile Thread thread;
	//下一个等待条件(Condition)的节点(与Condition有关,下一章条件队列详细介绍)
	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;
        }
		
	    Node() {    // Used to establish initial head or SHARED marker
        }
        //被addWaiter使用的构造方法
        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
        //被Condition条件队列使用的节点构造方法
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
}

 

2.2 许可数量的维护

     AQS用了一个volatile修饰的32位的整型字段state来表示同步状态或者叫执行许可。对许可数量的维护只提供了如下的基本的许可数量存取操作,而对许可数量的具体维护(获取和释放锁时)必须由开发人员通过覆写相应的方法根据自身的特性进行维护。因为AQS不可能知道在你的独占或共享同步组件内部到底有多少许可,以及每个线程在获取锁和释放锁时需要获得或释放多少数量的许可。

//通过state表示同步状态或者叫执行许可,初始值为0表示当前一个许可都没被占用
private volatile int state;

protected final int getState() { //获取当前同步状态的值,volatile读
        return state;
}

protected final void setState(int newState) { //直接设置当前同步状态的值,volatile写,可保证操作的可见性。
        state = newState;
}
//CAS方式修改state变量(只有在当前值是所期望的值时才会成功),也可保证其操作的原子性和对其他线程的可见性
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2.3 不同组件类型对应的不同模板方法

       AQS作为构建同步组件的基础框架。支持独占式地获取同步状态,也可以支持共享式的获取同步状态,这样就可以方便实现不同类型的同步组件(例如:ReentryLock、ReentryReadWriteLock、和CountDownLatch等都是在AQS的基础上编写的)。AQS同步队列器在维护许可数量和等待队列时,都分别针对独占式和共享式同步组件编写了不同的模板方法,所以在实现不同工作模式的同步组件时不但要使用不同的模板方法,而且要覆写不同的许可维护方法。


 注意:另外还有一个可选的boolean isHeldExclusively()方法在所实现的同步器用到Condition的时候才需要开发人员覆写,该方法用于判断当前线程是否正在独占共享资源(也即许可,或者锁)。

      一般来说,自定义同步器要么是独占方法,要么是共享方式,我们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种对共享资源或者说执行许可state变量的维护方法即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。默认情况下,需要覆写的方法的实现都是抛出一个UnsupportedOperationException。至于为何没有直接将需要覆写的方法声明为抽象方法,是出于实现不同工作模式的同步器时没有必要去覆写另一种工作模式需要覆写的方法的考虑。

 

三、AQS独占获取/释放源码分析

3.1 void acquire(int arg)

此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则被加入同步等待队列,直到获取到资源为止,在等待的整个过程忽略线程被中断的影响。也就是说由于线程获取共享资源失败加入到同步队列中,后续对线程进行中断操作时,线程不会立即从同步队列中移除,只有成功获取到共享资源之后才会查看是否之前被中断过,如果是才进行自我中断。该方法相当于lock()语义。

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

    这个方法执行过程如下:

    ①,调用被覆写过的方法tryAcquire(int)尝试直接获取资源,如果成功则直接返回。

    ②,第①步直接获取资源失败之后通过addWaiter()将该线程加入同步等待队列尾部,并标记为独占模式。

    ③,acquireQueued()使线程节点在同步等待队列中通过“自旋”并借助阻塞唤醒机制持续尝试获取资源,一直到获取成功之后才返回,返回值的boolean型结果表示线程是否在整个过程中被中断过,而在等待过程中如果被中断过它是不响应的。

    ④,在成功获取资源之后,如果发现在等待过程中被中断过,就通过selfInterrupt()进行自我中断,将中断补上。

通过以上的描述很难让我们理解acquire方法的原理,接下来我们通过逐个分析里面的每一个方法,进行深入理解acquire方法,当理解了每一个方法再回过头来就一切都明白了。

3.1.1 tryAcquire(int)

此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。此方法相当于tryLock()语义。

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}
因为对共享资源或者叫执行许可的具体维护需要开发人员针对不同的同步组件需要进行覆写,所以这个方法在没有被覆写的情况下是直接抛出UnsupportedOperationException异常的。至于为什么不直接声明为abstract,上面已经解释过了。对于这个方法的具体编写,开发人员可以利用state字段以及它的get、set、compareAndSetState方法在保证线程安全的情况进行自由实现,例如能不能重入等特性也由此处进行实现。
3.1.2 addWaiter(Node mode)

此方法用于将当前线程加入到同步等待队列的队尾,并返回当前线程所在的节点。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//以当前线程和给定的模式(EXCLUSIVE(独占)和SHARED(共享))构造Node节点。
        //尝试快速直接将该节点放入同步等待队列队尾,但是可能会失败
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {//这里的CAS操作可能会失败,因为可能队尾已经被其他线程更改了。
                pred.next = node;//成功之后将原来的尾节点的后继节点设置为新的节点
                return node;
            }
        }
        enq(node);//快速直接的放入队尾失败之后,则调用enq方法将其加入队尾。
        return node;
}

private Node enq(final Node node) {
        for (;;) {//CAS“自旋”,直到成功放入队尾
            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;
                }
            }
        }
}
3.1.3 acquireQueued(Node mode,int arg) 

走到这一步说明该线程获取资源失败,被放入了同步等待队列队尾,接下来将是最关键的时刻了:被放入等待队列的线程需要等待其他线程彻底释放资源后唤醒自己,自己再尝试获取资源,如果获取失败那么就将自己阻塞,依然待在队列中。但是这里的逻辑远不止这么简单,因为队列中的节点状态会不断变化,所以节点在排队的时候也需要根据实际情况作出调整。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true; //标记是否成功获取资源
        try {
            boolean interrupted = false; //标记在等待过程中是否被中断过
            for (;;) { //又是一个CAS“自旋”,就是死循环,只不过有阻塞唤醒机制来释放CPU,不会太消耗CPU
                final Node p = node.predecessor();//拿到前驱节点
                /*
                  如果前驱是头节点即该节点是第二节点,那么就有资格去尝试获取资源,
                  这是为了保证FIFO队列的公平性,而且头节点彻底释放资源之后会唤醒其后继节点
                 */
                if (p == head && tryAcquire(arg)) {
                    setHead(node); //获取到资源之后设置当前节点为头节点,并且会把当前节点的前驱节点置为null
                    p.next = null; //此处再将原来的头节点的后继置为null,是为了方便GC垃圾回收以前的节点
                    failed = false;
                    return interrupted;//返回等待过程中是否被中断过
                }
                //如果没有资格(存在假唤醒时)或者获取资源失败(比如第一次很可能失败),则重新排队(如果有必要的话),
                if (shouldParkAfterFailedAcquire(p, node) && 
                    parkAndCheckInterrupt())   //然后进行阻塞进入waiting状态,等到下一次的唤醒,被唤醒之后立即查看是否有被中断过
                    interrupted = true;
            }
        } finally {
            if (failed) //"自旋"失败,"但什么情况下会失败呢??一般很少吧。"
                cancelAcquire(node); //如果"自旋"获取资源失败,将节点从同步队列中移除
        }
}
/*
 这个方法会根据需要重新排序节点("因为排在前面的线程有可能取消了等待,但什么情况下会设置自己的状态位为CANCELLED呢?其实看下面3.3, 3.4节有这种情况"),
 重新排序会将当前节点排到一个正在等待的正常节点后面.如果重新排过序或者第一次告诉前驱通知自己就需要立即再次尝试获取资源,
 只有再次尝试获取资源失败之后,并且节点的位置没有更改和已经告诉前驱通知自己的时候,才阻塞当前线程进入waiting状态,等待下一次被唤醒。
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; //拿到前驱节点的状态
        if (ws == Node.SIGNAL)
           //如果已经设置了前驱的状态为SIGNAL(表示在前驱彻底释放资源后通知自己),就可以将自己阻塞起来等待通知了
            return true;
        if (ws > 0) {
            /*
             如果前驱取消了等待(只有状态为CANCELLED时才大于0),就一直往前找
             直到找到一个状态<=0的节点时(表示其是一个正常等待的节点),将自己排在他后面
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             CAS设置前驱的状态为SIGNAL(表示在前驱彻底释放资源后通知自己)
             不论设置成功与否都会重新尝试获取资源。
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
}
//该方法在尝试获取资源失败了,也通知了前驱通知自己之后,将自己阻塞起来,等待被下一次唤醒。
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);      //调用park()是当前线程阻塞进入waiting状态
        return Thread.interrupted(); //被唤醒之后,立刻查看自己是否在等待期间被中断过
}

 总结acquireQueued()方法如下,在通过3.1.2 的addWaiter(Node mode)方法将节点放入队尾之后:

  ①,有必要时会重新将当前节点排到一个正常的处于等待状态的节点后面,并通知它的前驱节点在彻底释放完资源之后通知自己。

  ②,做完①的事情之后,通过park()方法阻塞起来,等待被唤醒。

  ③,被唤醒之后,立即查看是否有被打断过,然后再尝试获取资源,如果成功获取资源就将头节点置为当前节点,返回是否被中断过的标记,如果获取资源失败,则继续流程①

现在再回过头去看 3.1 的 void acquire(int) 方法,应该就很清楚明了了。

 

3.2 boolean release(int arg)

上一小节3.1介绍了独占模式下获取同步资源的顶层方法acquire方法,现在我们开始介绍独占模式下释放同步资源的顶层方法release.该方法会释放参数指定量的共享资源,如果资源彻底释放了(即state=0),还要唤醒等待队列里的其他线程来获取资源。这也是unlock()的语义。

public final boolean release(int arg) {
        if (tryRelease(arg)) {      //调用自定义方法释放资源,并返回是否彻底释放掉资源
            Node h = head;          //如果已经完全释放掉资源就找到头节点,
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//通过头节点唤醒后驱节点
            return true;
        }
        return false;
}

可以看到该顶层方法会调用需要开发人员覆写的资源释放方法tryRelease方法,而且该顶层方法的返回值也完全取决于该自定义方法的返回值。而该自定义方法tryRelease的返回值表示是否已经彻底完全释放掉资源。  

3.2.1 tryRelease(int) 

此方法尝试去释放指定量的资源。

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

    跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

3.2.2 unparkSuccessor(Node)  

此方法用于唤醒同步等待队列中指定节点的下一个节点代表的线程。

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0) "置0当前节点的状态位,但允许失败,不明白为何要这样做?"
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 正常来说,直接唤醒他的后驱节点线程即可,但是其下一个节点线程有可能取消了等待或者为null
         * 所以需要从队尾开始往前找,一直找到最前面的那一个不是自己并且节点状态 <=0的节点,
         * 然后唤醒这个最前面的节点状态 小于等于0 的节点代表的线程
         */
        Node s = node.next; //拿到其后驱节点
        if (s == null || s.waitStatus > 0) { //如果后驱节点为null或者已经取消
            s = null;  
            for (Node t = tail; t != null && t != node; t = t.prev) //从队尾往前循环,找到一个节点状态 <=0 并且不是自己的节点
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);  //唤醒找到的有效节点代表的线程
}

 一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃的线程,结合前面3.1.3 的acquireQueued()方法,这个被唤醒的线程进入if (p == head && tryAcquire(arg))判断的时候,即使p != head也没关系,因为它会在接下来进入shouldParkAfterFailedAcquire()方法之后,重新进行排序,既然这个被唤醒的线程节点已经是等待队列中最前面的那一个未放弃的线程了,那么进行重新排队也必然会被重新排到头节点的后驱节点位置上,重新排队之后再一次的自旋时,必然就会满足p == head,然后执行tryAcquire(arg)成功之后,将自己设置为新的头节点,表示自己已经成功获取到资源,然后acquire(int)方法也就返回了。

 

3.3 void acquireInterruptibly(int arg) throws InterruptedException

上面的3.1 和3.2 小节将独占式获取和释放资源的基础方法的源码进行了介绍, 其中3.1小节的独占式获取同步资源的acquire(int)方法对中断不敏感,如果线程在等待过程中被中断后,该线程对应的节点会依然位于同步等待队列中等待着获取同步资源,直到成功获取资源之后才会响应中断。为了能够及时响应中断,AQS提供了acquireInterruptibly(int)方法,该方法在线程等待获取同步资源过程中,如果线程被中断了,会立刻响应中断抛出异常InterruptedException,而不需要等到获取到同步资源之后,让我们来看看具体是怎么立即响应中断的。

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted()) throw new InterruptedException();
        if (!tryAcquire(arg)) doAcquireInterruptibly(arg);
}
   从上面acquireInterruptibly(int)的源代码可见,该方法会先校验该线程是否已经中断了,如果是则抛出InterruptedException,否则执行tryAcquire(int arg)方法获取同步资源,如果获取成功,则直接返回,否则执行doAcquireInterruptibly(int arg)。可见对中断的响应的核心在doAcquireInterruptibly(int arg)方法中:
private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();//和非中断响应模式的acquire方法的区别所在
            }
        } finally {
            if (failed) //由于被打断,failed为true,将在cancelAcquire方法中将该节点状态置为CANCELLED
                cancelAcquire(node);
        }
}
   从doAcquireInterruptibly(int)的源码逻辑可见其和不响应中断的acquire(int)几乎是一样的,唯一的区别就在线程从parkAndCheckInterrupt()方法返回代表线程被中断的过的布尔值时不再是设置interrupted标志,而是抛出InterruptedException异常,以致于立即跳出了for(;;)的自旋操作并立即返回。这里能立即响应中断最重要的原因在于parkAndCheckInterrupt()方法中的LockSupport.park(this)语句,从上一章的LockSupport中对park()方法的介绍里我们知道在其他线程打断了当前线程时,LockSupport.park()方法会立即退出阻塞返回,因此这里对中断响应得到了立即响应。
值得一提的是,如果这时候被打断的线程在返回之前由于没有尝试获取资源,failed为true,所以将会执行cancelAcquire(node)方法,该方法会将对应的节点状态位设置成CANCELLED。这也是为什么会出现存在着节点放弃了等待的一种情况。

3.4 boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException

这是独占式获取同步资源的最后一个模板方法,该方法为acquireInterruptibly方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回false,否则返回true。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted()) throw new InterruptedException();
        return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
   从tryAcquireNanos的源码可见其确实响应中断,有InterruptedException异常抛出,并且和acquireInterruptibly源码类似,在直接通过tryAcquire(arg)获取同步资源失败之后,其主要的超时和中断响应逻辑都在doAcquireNanos方法中:
private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L) //超时时间小于等于0,立即返回false
            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; //如果已经超时了,立即返回false
                if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout); // 如果没有超时,并且剩下的时间相对来说还比较久就阻塞休眠
                if (Thread.interrupted()) //在线程被中断或者超时时间到达之后线程自动动parkNanos方法中退出阻塞或者剩下的时间已经很短了
                    throw new InterruptedException(); 如果线程被中断过,立即抛出中断异常返回。
            }
        } finally {
            if (failed) //由于被打断或者超时时间到之后依然没有成功获取资源,failed为true,将在cancelAcquire方法中将该节点状态置为CANCELLED
                cancelAcquire(node);
        }
}
    从doAcquireNanos方法的源码可见,主要是借助了LockSupport.parkNanos方法,在阻塞的过程中超时时间到达被中断时,LockSupport.parkNanos都会立即退出阻塞,如果线程被中断过,下一步执行if (Thread.interrupted())成立之后会立即抛出中断异常,如果是因为超时时间到达使LockSupport.parkNanos退出阻塞,那么在立刻开始下一次自旋操作中不论是否获取到了共享资源都会立即返回,只不过如果成功获取到了共享资源返回true,如果依然没有获取到资源,则if (nanosTimeout <= 0L) 成立返回false.

 

    值得一提的是,该方法中使用了一个spinForTimeoutThreshold常量其值为1000纳秒,当剩下的超时时间小于等于这个值时就不需要休眠了,直接进入快速自旋的过程。原因在于 spinForTimeoutThreshold 已经非常小了,非常短的时间等待无法做到十分精确,如果这时再次进行超时等待,相反会让nanosTimeout 的超时从整体上面表现得不是那么精确,所以在超时非常短的场景中,AQS会进行无条件的快速自旋。
 

由于篇幅限制,关于共享式同步资源获取/释放 相关模板方法的源码分析请看下一章。

 

  • 大小: 38.6 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics