并发编程夯实之路-抽象队列同步器AQS

AQS(AbstractQueuedSynchronizer)

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。它是一个Java提高的底层同步工具类,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

在这里插入图片描述

AQS具备特

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

1源码详解

1.1 结点状态waitStatus

这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0

  • **CANCELLED(1) **:表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • **SIGNAL(-1) **:表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • **CONDITION(-2) **:表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • **PROPAGATE(-3) **:共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • **0 **:新结点入队时的默认状态。

1.2 acquire(int)

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

函数流程如下:
1.tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);

2.addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;

3.acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

4.如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
 

1.2.1 tryAcquire(int)

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

此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义。
 

1.2.2 addWaiter(Node)

  private Node addWaiter(Node mode) {
      //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
      Node node = new Node(Thread.currentThread(), mode);
      
      //尝试快速方式直接放到队尾。
      Node pred = tail;
      if (pred != null) {
          node.prev = pred;
          if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     
     //上一步失败则通过enq入队。
     enq(node);
     return node;
 }

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

1.2.3 enq(Node)

  private Node enq(final Node node) {
      //CAS"自旋",直到成功加入队尾
      for (;;) {
          Node t = tail;
          if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
              if (compareAndSetHead(new Node()))
                  tail = head;
          } else {//正常流程,放入队尾
              node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
 }

此方法用于将node加入队尾。

 

1.2.4 acquireQueued(Node, int)

  final boolean acquireQueued(final Node node, int arg) {
      boolean failed = true;//标记是否成功拿到资源
      try {
          boolean interrupted = false;//标记等待过程中是否被中断过
          
          //又是一个“自旋”!
          for (;;) {
              final Node p = node.predecessor();//拿到前驱
              //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。
             if (p == head && tryAcquire(arg)) {
                 setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。
                 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
                 failed = false; // 成功获取资源
                 return interrupted;//返回等待过程中是否被中断过
             }
             
             //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
         }
     } finally {
         if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
             cancelAcquire(node);
     }
 }

在等待队列中排队拿号,直到拿到号后再返回。
 

1.2.5 shouldParkAfterFailedAcquire(Node, Node)

  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      int ws = pred.waitStatus;//拿到前驱的状态
      if (ws == Node.SIGNAL)
          //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
          return true;
      if (ws > 0) {
          /*
           * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
           * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
          */
         do {
             node.prev = pred = pred.prev;
         } while (pred.waitStatus > 0);
         pred.next = node;
     } else {
          //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
     }
     return false;
 }

此方法主要用于检查状态,看看线程是否真的可以被阻塞了。
 

1.2.6 parkAndCheckInterrupt()

 private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);//调用park()使线程进入waiting状态
     return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
 }

此方法就是让线程去休息,真正进入等待状态。

总结

1.调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回。

2.没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式。

3.acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

4.没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值