Java源码学习--并发锁05--01--AQS

深入解析AbstractQueuedSynchronizer(AQS)的源码,包括类结构、基本属性、同步队列、条件队列和核心方法。阐述了AQS如何支持锁的获取与释放、条件队列的使用以及线程协作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本属性

  • 总体流程
    在这里插入图片描述

一、类结构

  • 源码
    1. 代码
      public abstract class AbstractQueuedSynchronizer
          extends AbstractOwnableSynchronizer
          implements java.io.Serializable {
          ....
      }
      
    2. 解释
      1. AQS是抽象类,是给子类使用,同样的,抽出了很多共用的方法,但是同样在共用方法中也抽出了让子类必须实现的方法,如何获取锁,如何释放锁,如果在子类没有实现该功能,调用父类方法,会直接报错,可以让实现的子类强制必须实现此方法,或者不要使用。这种强制手段可以在平时代码中使用。
      2. 继承了AbstractOwnableSynchronizer,此接口只是用来记录当前获取到锁的线程,可以更好的监控,从而也可以体验,单一职责原则,这个类只负责记录占有锁的线程,如果对占有锁的线程进行监控,只要查看此抽象类即可
        public abstract class AbstractOwnableSynchronizer
            implements java.io.Serializable {
        
            protected AbstractOwnableSynchronizer() { }
        
            //独占锁的线程
            private transient Thread exclusiveOwnerThread;
        
            //将获取锁的线程,set进来
            protected final void setExclusiveOwnerThread(Thread thread) {
                    exclusiveOwnerThread = thread;
        }
        

二、类属性

  • AQS属性分为四种
    1. 同步器简单属性
    2. 同步队列属性
    3. 条件队列属性
    4. 公用Node
1. 同步器简单属性
  • 源码
    // 同步器的状态,子类会根据状态字段进行判断是否可以获得锁
    // 比如 CAS 成功给 state 赋值 1 算得到锁,赋值失败为得不到锁, CAS 成功给 state 赋值 0 算释放锁,赋值失败为释放失败
    // 可重入锁,每次获得锁 +1,每次释放锁 -1
    private volatile int state;
    
    // 自旋超时阀值,单位纳秒
    // 当设置等待时间时才会用到这个属性
    static final long spinForTimeoutThreshold = 1000L;
    
  • state此字段是判断是否可以获取锁,而且使用volatile,保证了可见性,不同线程可以获取到最新的state值
2. 同步队列属性
  • 同步队列的用处:阻塞得不到锁的线程,并且在适当的时候释放这些线程
    1. 当多个线程获取锁时,同一时刻只有一个线程可以获取到锁,其他线程会插入到同步队列中,并且阻塞自己
    2. 当有线程释放锁时,就会从同步列队的头部开始释放一个线程,让线程去竞争锁
  • 同步对列是双向链表,是具有头节点是一个空节点,状态为SIGNAL,尾节点则是指向,最后一个节点,在AQS中记录了同步队列的头结点和尾节点
    // 同步队列的头。
    private transient volatile Node head;
    
    // 同步队列的尾
    private transient volatile Node tail;
    
  • 思考一波,为何要有头节点呢
    • 个人理解,每个节点的阻塞式通过设置前一个节点的状态,然后才将自身阻塞,换言之,当前节点的唤醒时依赖前驱节点获取到锁之后,才能唤醒自身,所以前驱节点必不可少,而且前驱节点的状态必须是SIGNAL(个人理解,此状态是表示可以唤醒后继节点的状态,也说明了存在后继节点)
3. 条件队列的属性
  • 条件队列和同步队列的功能一样,都是管理获取不到锁的线程,底层数据结构也是链表队列,但条件队列不直接和锁交互,常常配置锁来使用,是对锁的一种补充
    • 条件队列是可以快速的查出头节点和尾节点的单向链表,不存在空的头节点,这个是和同步队列最重要的不同之处
  • 属性
    // 条件队列,从属性上可以看出是链表结构
    public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        // 条件队列中第一个 node
        private transient Node firstWaiter;
        // 条件队列中最后一个 node
        private transient Node lastWaiter;
    
        //一些关于条件对列中节点的方法
    }
    
  • 理解
    1. 实现了Condition接口,可以得知,对条件对列中的属性进行了控制
    2. 可以快速获取到头节点和尾节点
    3. 内部使用的节点和同步队列的节点一样,不同对象,使用一个类Node实现,肯定存在一样的属性,不同的属性如何区分,个人理解,对于AQS而言,不管是同步队列的节点,还是条件队列的节点,都是AQS的节点,所以在设计的时候就使用一种结构来实现
4. Node节点属性
  • 正是因为存在两个队列使用公共的属性,所以必然会出现,有些属性是公用的(不然没意思合在一起),一些存在于同步队列中,一些存在于等待对列中,通过下面的源码来证实。
  • 代码
    static final class Node {
    
            //-----------------同步队列单独的属性---------------
            //node 是共享模式
            static final Node SHARED = new Node();
    
            //node 是排它模式
            static final Node EXCLUSIVE = null;
    
            // 当前节点的前节点
            // 节点 acquire 成功后就会变成head
            // head 节点不能被 cancelled
            volatile Node prev;
    
            // 当前节点的下一个节点
            volatile Node next;
    
            //-----------------两个队列共享的属性-----------------
            // 表示当前节点的状态,通过节点的状态来控制节点的行为
            // 普通同步节点,就是 0 ,条件节点是 CONDITION -2
            volatile int waitStatus;
    
            // waitStatus 的状态有以下几种
            // 被取消
            static final int CANCELLED =  1;
    
            // SIGNAL 状态的意义:同步队列中的节点在自旋获取锁的时候,如果前一个节点的状态是 SIGNAL,那么自己就可以阻塞休息了,否则自己一直自旋尝试获得锁
            static final int SIGNAL    = -1;
    
            // 表示当前 node 正在条件队列中,当有节点从同步队列转移到条件队列时,状态就会被更改成 CONDITION
            static final int CONDITION = -2;
    
            // 无条件传播,共享模式下,该状态的进程处于可运行状态
            static final int PROPAGATE = -3;
    
            // 当前节点的线程
            volatile Thread thread;
    
            //-----------------存在不同含义的属性-----------------
            // 在同步队列中,nextWaiter 并不真的是指向其下一个节点,我们用 next 表示同步队列的下一个节点,nextWaiter 只是表示当前 Node 是排它模式还是共享模式
            // 但在条件队列中,nextWaiter 就是表示下一个节点元素
            Node nextWaiter;
    }
    
  • 理解
    1. 同步队列:Node的pre next属性是同步队列中的链表前后指向字段,nextWaiter在同步队列中,只是一个标识符,表示当前节点是共享还是排他模式,同步队列的Node节点是有前驱和后继节点,是双向链表。
    2. 条件队列:Node的nextWaiter是条件队列中下一个节点的指向字段,是单向链表

三、Condition

  • Condition是接口,定义了线程间的协作关系
    1. 当Lock代替synchronized来加锁时,Condition就可以用来代替Object中相应的监控方法,比如Object.wait()
    2. 提供了一种线程协作的方式,一个暂停的线程,可以被其他线程唤醒
    3. Condition实例是绑定在锁上的,通过Lock#newCondition方法可以产生该实例
    4. 除了特殊说明外,任意空值作为方法的入参,都会抛出空指针
    5. Condition提供了明确的语义和行为
  • 源码
    public interface Condition {
    
        //阻塞等待,可以响应中断,或者被其他线程通过signal或者signalAll唤醒
        void await() throws InterruptedException;
    
        void awaitUninterruptibly();
    
        // 返回的 long 值表示剩余的给定等待时间,如果返回的时间小于等于 0 ,说明等待时间过了
        // 选择纳秒是为了避免计算剩余等待时间时的截断误差
        long awaitNanos(long var1) throws InterruptedException;
    
        // 虽然入参可以是任意单位的时间,但底层仍然转化成纳秒
        boolean await(long var1, TimeUnit var3) throws InterruptedException;
    
        boolean awaitUntil(Date var1) throws InterruptedException;
    
        // 唤醒条件队列中的一个线程,在被唤醒前必须先获得锁
        void signal();
    
        // 唤醒条件队列中的所有线程
        void signalAll();
    }
    
  • 条件队列实现类此接口,说明了条件队列中的节点,拥有阻塞,等待,唤醒的操作,多个线程之间,具有了协作的能力
  • 条件队列实现了此接口,条件队列中的线程被唤醒的情况
    1. 有线程执行了signal方法,正好唤醒了条件队列中的当前线程
    2. 有线程使用了signalAll方法
    3. 其他线程打断了当前线程,并且当前线程支持被打断
    4. 被虚假唤醒

四、同步器

  • 同步器有两个状态:一个是锁的状态,一个是节点的状态
    • state是锁的状态,表示是否获取了锁
    • waitStatus是节点的状态 CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)

重要方法

一、获取锁

  • acquire属于共同的流程,AQS已经实现,tryAcquire是需要子类根据自己的需求来决定具体的流程
  • acquire方法制定了获取锁的框架,即整体流程,先尝试使用tryAcquire获取锁,获取不到,进入同步队列中等待锁。在父类AQS中,tryAcquire直接抛出了一个异常,强制实现此父类的子类,必须实现此方法。
  • 子类可以根据同步器的state状态来决定是否能够获取锁。
1. 获取排他锁
  • 源码
    1. acquire方法
      • 流程

        
            执行一次tryAcquire
                |-> 成功
                    |-> 返回
                |-> 失败
                    |-> 尝试进入同步队列
                        |-> addWaiter加入同步队列对尾
                        |-> acquireQueued(作用:1.阻塞当前节点,2.节点唤醒时,得到锁)
                |-> 都不执行,线程中断
        
        
      • 步骤

        1. tryAcquire获取锁,获取到直接返回,否则走2
        2. addWaiter将当前线程,创建一个新的节点,加入同步对列的尾部
        3. 自旋+CAS 将当前节点的前面节点的状态置为SIGNAL,并且阻塞自身线程
          • 最终的结果就是,除了最后一个节点,是初始化状态,其他节点都是SIGNAL
      • 代码

        // 排它模式下,尝试获得锁
        public final void acquire(int arg) {
            // tryAcquire 方法是需要实现类去实现的,实现思路一般都是 cas 给 state 赋值来决定是否能获得锁
            if (!tryAcquire(arg) &&
                // addWaiter 入参代表是排他模式
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        
    2. addWaiter:加入同步队列
      • 流程
        • 将新建的节点,加入到同步对列的对尾,跟新对尾,头节点是一个空的Node节点
      • 值得学习的地方
        • 一般加入对尾的节点,使用一次CAS就可以达到目的,所以在加入对尾的流程中,先通过CAS加入一次对尾,如果加入不成功,然后再进行自旋+CAS加入对尾节点
      • 源码
        // 方法主要目的:node 追加到同步队列的队尾
        // 入参 mode 表示 Node 的模式(排它模式还是共享模式)
        // 出参是新增的 node
        // 主要思路:
        // 新 node.pre = 队尾
        // 队尾.next = 新 node
        // 返回新增加的节点
        private Node addWaiter(Node mode) {
            // 初始化 Node
            Node node = new Node(Thread.currentThread(), mode);
            // 这里的逻辑和 enq 一致,enq 的逻辑仅仅多了队尾是空,初始化的逻辑
            // 这个思路在 java 源码中很常见,先简单的尝试放一下,成功立马返回,如果不行,再 while 循环
            // 很多时候,这种算法可以帮忙解决大部分的问题,大部分的入队可能一次都能成功,无需自旋
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            //自旋保证node加入到队尾
            enq(node);
            return node;
        }
        
        // 线程加入同步队列中方法,追加到队尾
        // 这里需要重点注意的是,返回值是添加 node 的前一个节点
        private Node enq(final Node node) {
            for (;;) {
                // 得到队尾节点
                Node t = tail;
                // 如果队尾为空,说明当前同步队列都没有初始化,进行初始化
                // tail = head = new Node();
                if (t == null) {
                    if (compareAndSetHead(new Node()))
                        tail = head;
                // 队尾不为空,将当前节点追加到队尾
                } else {
                    node.prev = t;
                    // node 追加到队尾
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
        
      • 执行此流程之后
        1. 如果之前队列为空,执行之后,同步队列中,头节点是一个空节点,状态为初始化,加入节点的状态也为初始化,加到队尾,尾节点指向新增加的节点
          【图一个节点】
        2. 如果之前队列不为空,执行之后,同步队列中,头节点是一个空节点,状态为SIGNAL,同步队列中除了队尾节点,都是SIGNAL,队尾节点是初始化,当前节点加入队尾,状态为初始化,尾节点更新为新加入的节点
          在这里插入图片描述
    3. acquireQueued:阻塞当前节点,注意点,因为头节点是空节点,所以在唤醒之后的获取锁的方式,是如果前一个节点是头节点,其实是无用节点,内部都没有线程,那么将头节点的后继节点设置为头节点,此时头节点内部的线程已经被唤醒,在获取到锁之后,需要清理同步队列中的信息,此刻正好将获取锁的节点,置为空节点,然后更新头节点即可。
      // 主要做两件事情:
      // 1:通过不断的自旋尝试使自己前一个节点的状态变成 signal,然后阻塞自己。
      // 2:获得锁的线程执行完成之后,释放锁时,会把阻塞的 node 唤醒,node 唤醒之后再次自旋,尝试获得锁
      // 返回 false 表示获得锁成功,返回 true 表示失败
      final boolean acquireQueued(final Node node, int arg) {
          boolean failed = true;
          try {
              boolean interrupted = false;
              // 自旋
              for (;;) {
                  // 选上一个节点
                  final Node p = node.predecessor();
                  // 有两种情况会走到 p == head:
                  // 1:node 之前没有获得锁,进入 acquireQueued 方法时,才发现他的前置节点就是头节点,于是尝试获得一次锁;
                  // 2:node 之前一直在阻塞沉睡,然后被唤醒,此时唤醒 node 的节点正是其前一个节点,也能走到 if
                  // 如果自己 tryAcquire 成功,就立马把自己设置成 head,把上一个节点移除
                  // 如果 tryAcquire 失败,尝试进入同步队列
                  if (p == head && tryAcquire(arg)) {
                      // 获得锁,设置成 head 节点
                      // 将获取到锁的节点,置为空节点,然后更新头节点
                      setHead(node);
                      //p被回收
                      p.next = null; // help GC
                      failed = false;
                      return interrupted;
                  }
      
                  // shouldParkAfterFailedAcquire 把 node 的前一个节点状态置为 SIGNAL
                  // 只要前一个节点状态是 SIGNAL了,那么自己就可以阻塞(park)了
                  // parkAndCheckInterrupt 阻塞当前线程
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      // 线程是在这个方法里面阻塞的,醒来的时候仍然在无限 for 循环里面,就能再次自旋尝试获得锁
                      parkAndCheckInterrupt())
                      interrupted = true;
              }
          } finally {
              // 如果获得node的锁失败,将 node 从队列中移除
              if (failed)
                  cancelAcquire(node);
          }
      }
      
    4. shouldParkAfterFailedAcquire:把前一个节点的状态置为SIGNAL,当前节点阻塞 parkAndCheckInterrupt
      • 流程
        
           1. 前一个节点状态SIGNAL
                |-> 直接返回
           2. 找到前一个没有取消的节点
                |-> 把前一个节点的状态设置为SIGNAL
        
        
      • 源码
        // 当前线程可以安心阻塞的标准,就是前一个节点线程状态是 SIGNAL 了。
        // 入参 pred 是前一个节点,node 是当前节点。
        
        // 关键操作:
        // 1:确认前一个节点是否有效,无效的话,一直往前找到状态不是取消的节点。
        // 2: 把前一个节点状态置为 SIGNAL。
        // 1、2 两步操作,有可能一次就成功,有可能需要外部循环多次才能成功(外面是个无限的 for 循环),但最后一定是可以成功的
        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            // 如果前一个节点 waitStatus 状态已经是 SIGNAL 了,直接返回,不需要在自旋了
            if (ws == Node.SIGNAL)
                return true;
            // 如果当前节点状态已经被取消了。
            if (ws > 0) {
                // 找到前一个状态不是取消的节点,因为把当前 node 挂在有效节点身上
                // 因为节点状态是取消的话,是无效的,是不能作为 node 的前置节点的,所以必须找到 node 的有效节点才行
                do {
                node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            // 否则直接把节点状态置 为SIGNAL
            } else {
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
        
1. 获取共享锁
  • 与获取互斥锁不同的地方
    1. 获取锁的地方
      //共享情况下,尝试获得锁,tryAcquireShared首先尝试获取锁,返回值小于0,表示没有获得锁
      //共享锁和排他锁的区别在与:对于同一个共享资源
      //排他锁只能让一个线程获得
      //共享锁可以让多个线程获得,arg可以被子类当做任意参数
      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();
      
                  //为什么要重新获取一次,因为对于共享锁而言,可以由多个节点获取线程,所以,头节点肯定是获取线程的节点,头节点后面的节点也有可能获取到锁
                  //如果可以由三个线程获取线程那么会执行下面的流程 A->B->C->D
                  //执行流程
                  // A获取到锁,B加入队列中,C加入队列中,D加入队列中
                  // B发现前一个节点为A头节点,那么设置B为头节点,B的状态为PROPAGATE,同时唤醒C
                  // C发现前一个节点是B头节点,那么设置C为头节点,C的状态为PROPAGATE,同时唤醒D
                  // D获取锁,获取失败,此时的同步队列为 C->D,执行shouldParkAfterFailedAcquire,设置C的状态为SIGNAL,D为初始化状态
                  // 进一步理解了,只有SIGNAL节点,才具有唤醒后面节点的功能
                  if (p == head) {
                      //区别,如果前驱节点是头节点,再次获取锁
                      int r = tryAcquireShared(arg);
                      if (r >= 0) {
                          //如果获得成功,将当前节点置为Head节点,并且唤醒,节点的后续节点
                          //所以,同步队列的节点需要获取前驱节点和后继节点
                          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);
          }
      }
      
    2. 在节点获取到锁之后,如果是排他锁,仅仅把自己设置为同步队列的头结点即可(setHead方法),如果是共享锁,还会去唤醒自己的后续节点,一起来获得该锁(setHeadAndPropagate方法)
      • setHeadAndPropagate:设置当前获得锁的节点为头节点,唤醒后面的节点
        private void setHeadAndPropagate(Node node, int propagate) {
            //1. 设置当前节点为头节点
            Node h = head; // Record old head for check below
            setHead(node);
        
            //propagate>0表示已经有节点获得共享锁了
            if (propagate > 0 || h == null || h.waitStatus < 0 ||
                 (h = head) == null || h.waitStatus < 0) {
                 Node s = node.next;
                 //2. 共享模式,唤醒后面的节点
                 if (s == null || s.isShared())
                     doReleaseShared();
             }
         }
        
      • doReleaseShared:释放后置共享节点
        private void doReleaseShared() {
             for (;;) {
                 Node h = head;
                 //还没有到对尾,此时队列中至少有两个节点
                 if (h != null && h != tail) {
                     int ws = h.waitStatus;
                     //如果队列状态是SIGNAL,说明后续节点都需要唤醒,再一次证实需要空头节点的必要性
                     if (ws == Node.SIGNAL) {
                         //CAS保证只有一个节点可以运行唤醒的操作,如果设置成功了,才进行唤醒,其他线程无法进行唤醒操作
                         if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                             continue;            // loop to recheck cases
                         //进行唤醒操作
                         unparkSuccessor(h);
                     }
                     //在上面进行唤醒之后,需要将头节点的状态改成无条件传播状态,表示可以执行
                     //无条件传播,共享模式下,该状态的进程处于可运行状态
                     else if (ws == 0 &&
                              !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                         continue;                // loop on failed CAS
                 }
                 //第一种情况,头结点没有发生移动,结束
                 //第二种情况,因为此方法可以被两处调用,一次是获得锁的地方,一次是释放锁的地方
                 //加上共享锁的特性就是可以多个线程获得锁,也可以释放锁,这就导致了头节点可能会发生变化
                 //如果头节点发生了变化,就继续循环,一直循环到头节点不变化时,结束循环
                 if (h == head)                   // loop if head changed
                     break;
             }
        }
        
  • 不多解释,上面介绍了底层如何实现互斥锁和共享锁的获取方式,接下来,分析一波如何释放锁

二、释放锁

1. 释放排他锁release
  • 流程:从队头开始,找它的下一个节点,如果下一个节点是空的,就会从尾开始,一直找到状态不是取消掉的节点,然后释放该节点,先从前面找,不符合就从尾部开始找,这逻辑,为何如此,深究一波代码
  • 代码
    • release
      • 源码
      public final boolean release(int arg) {
          //1. tryRelease交给实现类去实现,一般就是用当前同步器状态减去arg,如果返回true说明成功释放锁
          if (tryRelease(arg)) {
              Node h = head;
              //2. 头节点不为空,并且非初始化状态
              //非初始化可以理解为SIGNAL状态时,才会唤醒需要唤醒的节点,从头节点开始唤醒,因为头节点是无用节点,唤醒头节点的后继节点
              if (h != null && h.waitStatus != 0)
                  unparkSuccessor(h);
              return true;
          }
          return false;
      }
      
    • unparkSuccessor(Node node)
      • 解释:当线程释放锁成功之后,从node开始唤醒同步队列中的节点,通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
      • 关键点:因为头节点是空节点,所以,永远都是唤醒节点的后继节点,不得不说,加一个头节点,影响全部流程呀
      • 源码
      private void unparkSuccessor(Node node) {
          //node节点,正式执行完业务逻辑,释放的节点
          int ws = node.waitStatus;
      
          //1. 如果节点已经被取消,把节点的状态置为初始化
          if (ws < 0)
              compareAndSetWaitStatus(node, ws, 0);
          }
          //2. 获取释放节点后面的节点
          Node s = node.next;
      
          //3. 如果后面的第一个节点为空,或者后一个节点被取消
          if (s == null || s.waitStatus > 0) {
              s = null;
      
              //4. 从尾进行迭代,而不是从头进行迭代是有原因的
              //主要是因为节点被阻塞的时候,是在acquireQueued方法里面被阻塞的
              //唤醒时也一定会在acquireQueued方法里面被唤醒,唤醒之后的条件是
              //判断当前节点的前置节点是否是头节点,这里判断的是前置节点,
              //所以这里必须从尾到头进行迭代顺序,目的是为了过滤掉无效的前置节点
              //不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞
              for (Node t = tail; t != null && t != node; t = t.prev)
                  if (t.waitStatus <= 0)
                      s = t;
          }
      
          //唤醒头节点的后继节点
          if (s != null)
              LockSupport.unpark(s.thread);
      }
      
2. 释放共享锁releaseShared
  • 流程
    1. tryReleaseShared尝试释放当前共享锁,失败返回false,成功走2
    2. 唤醒当前节点的后续阻塞节点,这个之前已经看过,线程在获得共享锁的时候,就会唤醒后面的节点,方法为doReleaseShared
  • 代码
    • releaseShared
      // 共享模式下,释放当前线程的共享锁
      public final boolean releaseShared(int arg) {
          if (tryReleaseShared(arg)) {
              // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
              doReleaseShared();
              return true;
          }
          return false;
      }
      
    • doReleaseShared在前面已经说明过

三、条件队列

  • 条件队列存在的意义:存在一些场景,如果单纯的使用同步队列+锁的方式无法达到最后的效果,同步队列中保存着没有获取到锁的线程,但是如果需要让获取到锁的线程,在某些情况下,进行等待的话,就需要通过等待队列来配置实现。所以必不可少
  • 在AQS中,等待队列为ConditionObject内部类实现
1. 入队列等待await
  • 发生的情况:对于获取到锁的线程,如果在执行过程中,某些条件没有满足,需要让线程等待某个条件满足时,才继续执行,这时就进行入队操作,浅绿器箭头流向
  • 代码
    • await
      • 源码
        //线程入条件队列
        public final void await() throws InterruptedException {
        
            //1. 如果线程已经中断,响应中断
            if (Thread.interrupted())
                throw new InterruptedException();
        
            //2. 加入条件队列的尾部
            Node node = addConditionWaiter();
        
            //3. 标记位置A
            //加入条件队列后,会释放lock时申请的资源,唤醒同步队列队列头的节点
            //自己马上就要阻塞了,必须马上释放之前lock的资源,不然自己不被唤醒的话
            //别的线程永远得不到该共享资源了
            int savedState = fullyRelease(node);
            int interruptMode = 0;
        
            //4. 当前节点,不在同步队列上,然后进行阻塞,如果还在同步队列的话,是不能够进行上锁的
            //存在的可能性:
            //1. node刚被加入到条件队列中,立马就被其他线程signal转移到同步队列中去了
            //2. 线程之前在条件队列中等待,被唤醒后加入到同步对列中
            while (!isOnSyncQueue(node)) {
        
                //5. 阻塞当前节点的线程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
        
            // 标记位置 B
            // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中的数据结构中去了
            // 所以这里节点苏醒了,直接尝试 acquireQueued
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
        
      • 理解
        1. 在位置A处,节点在进入条件队列之前,一定会先释放当前持有的锁,不然自己进入条件队列了,其余的线程都无法获得锁
        2. 在位置B处,此时节点Condition.signal或者signalAll方法唤醒,此时节点已经成功的被转移到同步队列中去了(蓝色流程),可以直接执行acquireQueued方法
        3. Node在条件队列的命名,都是使用Waiter命令
    • addConditionWaiter()
      • 内容:把节点加入到条件队列的队尾,流程很简单,就是删除不是等待的节点,将新节点加到等待队列队尾
      • 源码
        // 增加新的 waiter 到队列中,返回新添加的 waiter
        // 如果尾节点状态不是 CONDITION 状态,删除条件队列中所有状态不是 CONDITION 的节点
        // 如果队列为空,新增节点作为队列头节点,否则追加到尾节点上
        private Node addConditionWaiter() {
        
            //1. 获取最后一个节点
            Node t = lastWaiter;
        
            // 2. 如果最后一个节点不是CONDITION,删除
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
        
            //3. 新建一个节点,加入等待队列对尾
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
        
    • unlinkCancelledWaiters()
      • 内容:整理等待队列中的所有节点,将非CONDITION的节点删除,将CONDITION的节点重新整理起来
      • 源码:
        private void unlinkCancelledWaiters() {
        
            //1. 获取头节点
            Node t = firstWaiter;
        
            //用来记录上一个状态,通过此节点,记录上一个非Null节点,然后将所有的非Null节点串联起来
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
        
                //2. 如果当前节点的状态不是CONDITION,删除自己
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
        
                    //2.1. 如果trail是null,表示,从头节点开始的,说明当前节点是头节点,将头节点,指向之前头节点的下一个节点
                    if (trail == null)
                        firstWaiter = next;
                    //2.2 如果上一个节点非null,说明存在节点,将上一个节点的下一个节点设置为当前遍历的节点
                    else
                        trail.nextWaiter = next;
                    //2.3 如果当前节点为null,将尾节点置为上一个节点
                    if (next == null)
                        lastWaiter = trail;
                }
                //3. 如果当前节点是CONDITION,让trail指向上一个节点
                else
                    trail = t;
                //4. t指向下一个节点,从头开始遍历
                t = next;
            }
        }
        
      • 流程图走起
        在这里插入图片描述
2. 唤醒节点
2.1 单个唤醒signal
  • 内容:唤醒条件队列中的节点,如果队列满了,有了一些线程因为take操作而被阻塞进条件队列中,突然队列中的元素被线程A消费,线程A就会调用signal方法,唤醒之前阻塞的线程,会从条件队列的头节点开始唤醒(蓝色部分)
  • 代码
    • signal
      • 源码
      // 唤醒阻塞在条件队列中的节点
      public final void signal() {
          if (!isHeldExclusively())
              throw new IllegalMonitorStateException();
          // 从头节点开始唤醒
          Node first = firstWaiter;
          if (first != null)
              // doSignal 方法会把条件队列中的节点转移到同步队列中去
              doSignal(first);
      }
      
    • doSignal
      • 流程
      
          1. 获取头节点
          2. 记录头节点的下一个节点,作为新的头节点
          3. 将头节点从等待队列中移除
          4. 尝试将节点移动到同步队列
              * 如果头节点取消,继续1,2,3,来移动有效的头节点
              * 如果头节点移动成功,移动成功,doSignal方法结束
      
      
      
      • 源码
      // 把条件队列头节点转移到同步队列去
      private void doSignal(Node first) {
          do {
              // nextWaiter为空,说明到队尾了
              if ( (firstWaiter = first.nextWaiter) == null)
                  lastWaiter = null;
      
              // 从队列头部开始唤醒,所以直接把头节点.next 置为 null,这种操作其实就是把 node 从条件队列中移除了
              // 这里有个重要的点是,每次唤醒都是从队列头部开始唤醒,所以把 next 置为 null 没有关系,如果唤醒是从任意节点开始唤醒的话,就会有问题,容易造成链表的割裂
              first.nextWaiter = null;
      
              // transferForSignal 方法会把节点转移到同步队列中去
              // 通过 while 保证 transferForSignal 能成功
              // 等待队列的 node 不用管他,在 await 的时候,会自动清除状态不是 Condition 的节点(通过 unlinkCancelledWaiters 方法)
              // (first = firstWaiter) != null  = true 的话,表示还可以继续循环, = false 说明队列中的元素已经循环完了
          } while (!transferForSignal(first) &&
                   (first = firstWaiter) != null);
      }
      
    • transferForSignal
      • 内容:
      
          1. 如果当前节点的状态为非CONDITION,直接返回
          2. 将当前节点加入同步队列的队尾
          3. 获取到同步队列的前一个节点
          4. 修改前一个节点的状态为SIGNAL,
              1. 如果前一个节点是取消状态
              2. 修改SIGNAL状态失败
                  |-> 遇到上面两个情况,直接唤醒当前节点,让他在获取锁的自旋中,对前一个节点进行修正
      
      
      • 源码
      // 返回 true 表示转移成功, false 失败
      // 大概思路:
      // 1. node 追加到同步队列的队尾
      // 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒
      // 可以看出来 node 的状态此时是 0 了
      final boolean transferForSignal(Node node) {
      
          // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
          if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
              return false;
      
          // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点
          // 看命名是 p,实际是 pre 单词的缩写
          Node p = enq(node);
          int ws = p.waitStatus;
          // 状态修改成 SIGNAL,如果成功直接返回
          // 把当前节点的前一个节点修改成 SIGNAL 的原因,是因为 SIGNAL 本身就表示当前节点后面的节点都是需要被唤醒的
          if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
              // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
              LockSupport.unpark(node.thread);
          return true;
      }
      
2.2 唤醒全部节点signalAll
  • 内容:唤醒等待队列中的全部节点
  • 代码:
    public final void signalAll() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 拿到头节点
        Node first = firstWaiter;
        if (first != null)
            // 从头节点开始唤醒条件队列中所有的节点
            doSignalAll(first);
    }
    // 把条件队列所有节点依次转移到同步队列去
    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
            // 拿出条件队列队列头节点的下一个节点
            Node next = first.nextWaiter;
            // 把头节点从条件队列中删除
            first.nextWaiter = null;
            // 头节点转移到同步队列中去
            transferForSignal(first);
            // 开始循环头节点的下一个节点
            first = next;
        } while (first != null);
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值