AQS唤醒线程的时候为什么从后向前遍历

简介: AQS唤醒线程的时候为什么从后向前遍历

`先来熟悉一下代码,挂起和唤醒这两部分`

1. 尾部遍历源码
```java
privatevoidunparkSuccessor(Node node){
   //获取wait状态
   int ws = node.waitStatus;
   if(ws <0)
       compareAndSetWaitStatus(node, ws,0);// 将等待状态waitStatus设置为初始值0
   /**
    * 若后继结点为空,或状态为CANCEL(已失效),则从后尾部往前遍历找到最前的一个处于正常阻塞状态的结点
    * 进行唤醒
    */

   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);//唤醒线程
}

```

注意for循环中的逻辑:从尾部开始向前遍历,找到最前的一个处于正常阻塞状态的结点,直到节点重合(即等于当前节点)
2. 高并发下入队逻辑
既然采用了从尾部遍历的逻辑,那么肯定是为了解决可能会出现的问题。而这个问题就在enq(…)方法中
```java
privateNodeenq(finalNode node){
   for(;;){
       Node t = tail;
       if(t ==null){// Must initialize
           //队列为空需要初始化,创建空的头节点
           if(compareAndSetHead(newNode()))
               tail = head;
       }else{
           node.prev = t;
           //set尾部节点
           if(compareAndSetTail(t, node)){//当前节点置为尾部
               t.next = node;//前驱节点的next指针指向当前节点
               return t;
           }
       }
   }
}

```

3. 原子性问题
在该段方法中,将当前节点置于尾部使用了CAS来保证线程安全,但是请注意:在if语句块中的代码并没有使用任何手段来保证线程安全!

也就是说,在高并发情况下,可能会出现这种情况:

线程A通过CAS进入if语句块之后,发生上下文切换,此时线程B同样执行了该方法,并且执行完毕。然后线程C调用了unparkSuccessor方法。

假如是从头到尾的遍历形式,线程A的next指针此时还是null!也就是说,会出现后续节点被漏掉的情况。

4. 图解流程
线程A执行CAS将当前节点置为尾部:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/5da3629c29ee487bb350b31327bb2525.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBARW5nbGlzaENvZGU=,size_18,color_FFFFFF,t_70,g_se,x_16)

原本线程A要执行t.next = node;将node2的next设置为node3,但是,此时发生上下文切换,时间片交由线程B,也就是说,此时node2的next还是null

线程B执行enq逻辑,最终CLH队列如图所示:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/c6fb4f43193b411aa1001d8258138a29.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBARW5nbGlzaENvZGU=,size_20,color_FFFFFF,t_70,g_se,x_16)

此时发生上下文切换,时间片交由线程C,线程C调用了unparkSuccessor方法,假如是从头到尾的遍历形式,在node2就会发现,next指针为null,似乎没有后续节点了。

此时发生上下文切换,时间片交由线程A,A将node2的next=node3。奇怪的现象发生了:对于线程C来说,后续没有node3和node4,但是对于其它线程来说,却出现了这两个节点

5. 结尾
从头部遍历会出现这种问题的原因我们找到了,最后我们再来说说为什么从尾部遍历不会出现这种问题呢?

其最根本的原因在于:
node.prev = t;先于CAS执行,也就是说,你在将当前节点置为尾部之前就已经把前驱节点赋值了,自然不会出现prev=null的情况

先来熟悉一下代码,挂起和唤醒这两部分

  1. 尾部遍历源码
private void unparkSuccessor(Node node) {
    //获取wait状态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);// 将等待状态waitStatus设置为初始值0
    /**
     * 若后继结点为空,或状态为CANCEL(已失效),则从后尾部往前遍历找到最前的一个处于正常阻塞状态的结点
     * 进行唤醒
     */
    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. 注意for循环中的逻辑:从尾部开始向前遍历,找到最前的一个处于正常阻塞状态的结点,直到节点重合(即等于当前节点)
  2. 高并发下入队逻辑
    既然采用了从尾部遍历的逻辑,那么肯定是为了解决可能会出现的问题。而这个问题就在enq(…)方法中
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;
            //set尾部节点
            if (compareAndSetTail(t, node)) {//当前节点置为尾部
                t.next = node; //前驱节点的next指针指向当前节点
                return t;
            }
        }
    }
}
  1. 原子性问题
    在该段方法中,将当前节点置于尾部使用了CAS来保证线程安全,但是请注意:在if语句块中的代码并没有使用任何手段来保证线程安全!
    也就是说,在高并发情况下,可能会出现这种情况:
    线程A通过CAS进入if语句块之后,发生上下文切换,此时线程B同样执行了该方法,并且执行完毕。然后线程C调用了unparkSuccessor方法。
    假如是从头到尾的遍历形式,线程A的next指针此时还是null!也就是说,会出现后续节点被漏掉的情况。
  2. 图解流程
    线程A执行CAS将当前节点置为尾部:

    原本线程A要执行t.next = node;将node2的next设置为node3,但是,此时发生上下文切换,时间片交由线程B,也就是说,此时node2的next还是null
    线程B执行enq逻辑,最终CLH队列如图所示:

    此时发生上下文切换,时间片交由线程C,线程C调用了unparkSuccessor方法,假如是从头到尾的遍历形式,在node2就会发现,next指针为null,似乎没有后续节点了。
    此时发生上下文切换,时间片交由线程A,A将node2的next=node3。奇怪的现象发生了:对于线程C来说,后续没有node3和node4,但是对于其它线程来说,却出现了这两个节点
  3. 结尾
    从头部遍历会出现这种问题的原因我们找到了,最后我们再来说说为什么从尾部遍历不会出现这种问题呢?
    其最根本的原因在于:
    node.prev = t;先于CAS执行,也就是说,你在将当前节点置为尾部之前就已经把前驱节点赋值了,自然不会出现prev=null的情况
相关文章
|
3月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
87 0
|
3月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
39 0
|
5月前
|
Java
使用notifyAll唤醒所有等待线程的方法与比较
使用notifyAll唤醒所有等待线程的方法与比较
|
5月前
|
Oracle Java 关系型数据库
面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?
面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?
120 0
|
6月前
|
安全 Java
利用AQS(AbstractQueuedSynchronizer)实现一个线程同步器
利用AQS(AbstractQueuedSynchronizer)实现一个线程同步器
|
Java
线程等待唤醒(等待通知)机制
线程等待唤醒(等待通知)机制
41 0
线程发生阻塞,怎么唤醒线程?
线程发生阻塞,怎么唤醒线程?
218 0
|
算法 Java 调度
线程的挂起和唤醒
线程的挂起和唤醒
并发编程-17AQS同步组件之 Semaphore 控制并发线程数的信号量
并发编程-17AQS同步组件之 Semaphore 控制并发线程数的信号量
68 0
|
安全 Java C++
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
216 1
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)