深入理解JUC——AQS 及 ReentrantLock

本文深入探讨了Java并发编程中的AQS(AbstractQueuedSynchronizer)框架及其在ReentrantLock中的应用。AQS是阻塞式锁和同步器的底层实现,维护了一个int类型的state变量和CLH等待队列。它提供了两种资源共享模式:独占和共享。自定义同步器只需实现state的获取和释放。ReentrantLock包括公平锁和非公平锁,支持可中断锁以及Condition功能。
摘要由CSDN通过智能技术生成

AQS 框架

AQS 阻塞式锁和相关的同步器工具的框架。核心思想是:如果被请求的共享资源空闲,则将资源分配给工作线程,并将共享资源设置为锁定的状态;如果资源被占用的话,就需要一套线程阻塞等待以及被唤醒时锁分配的机制,AQS将线程封装成节点,进入到队列当中(FIFO的双向队列),等待唤醒。

AQS 维护了一个 int 类型的 state 变量来表示资源的同步状态和一个CLH (FIFO) 线程等待队列。AQS有两种不同的资源共享模式: 独占式(Exclusive)共享式(Share) 。 不同的同步器竞争共享资源的方式不同,自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。(需要实现的方法如下):

protected boolean isHeldExclusively() // 该线程是否正在独占资源。只有用到 Condition 才需要去实现它。
protected boolean tryAcquire(int arg) // 独占方式。arg 为获取锁的次数,尝试获取资源,成功则返回 True,失败则返回 False。
protected boolean tryRelease(int arg) // 独占方式。arg 为释放锁的次数,尝试释放资源,成功则返回 True,失败则返回 False。
protected int tryAcquireShared(int arg) // 共享方式。arg 为获取锁的次数,尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int arg) // 共享方式。arg 为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回 True,否则返回 False。
  • 源码分析
    • 节点状态

      state 表示了资源的同步状态,可以通过 getState(),setState(),compareAndSetState() 来实现对state 的访问。具体不同state 值对应的状态如下:

      0:新结点入队时的默认状态。
      CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
      SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
      CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
      PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
      
      **负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。**
      
    • 方法解析

      tryAcquire(int):用于尝试获取独占资源,如果获取成功则返回 True;否则返回 False。 tryAcquire(int) 需要由自定义同步器来自己实现。

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

      acquire(int):AQS 独占模式下的核心方法,不可以被实现者修改。如果获取到资源则直接返回,否则就进入到等待队列当中,等待获取资源。

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

      addWaiter(): 将当前线程封装成节点,并加入到队列的结尾

      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;
      

      enq(): 通过CAS 将节点加入到队列的尾部

      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;
                  }
              }
          }
      }
      

      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; // help GC
                      failed = false; // 成功获取资源
                      return interrupted; //返回等待过程中是否被中断过,若中断过则进入到selfInterrupt()
                  }
      
                  //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
              }
          } finally {
              if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
                  cancelAcquire(node);
          }
      }
      

      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;
      }
      

      **parkAndCheckInterrupt():调用park() 来使线程挂起,进入waiting状态;在被唤醒后,返回 Interrupted 的中断标志位来判断线程是否在等待过程中被中断过。

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

      总结: 当 node头节点的下一个节点时,node有权利去竞争共享资源,如果竞争成功则获得锁,若竞争失败则进入 shouldParkAfterFailedAcquire方法中,第一次将前驱节点 (head)的 status 设置为 -1 (SIGNAL)。进行一次自旋再次尝试获取共享资源,若这一次还失败,则需要调用 park()进入 waiting状态,等待 unpark()interrupt()唤醒自己。

      锁的释放:

      **tryRelease(int): :用于尝试获取独占资源,如果获取成功则返回 True;否则返回 False。 tryRlease(int) 需要由自定义同步器来自己实现。

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

      release(int): 独占模式下线程释放共享资源的顶层入口,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

      public final boolean release(int arg) {
          if (tryRelease(arg)) { // 如果tryRlease成功,需要唤醒下一个节点的线程
              Node h = head;//找到头结点
              if (h != null && h.waitStatus != 0)
                  unparkSuccessor(h);//唤醒等待队列里的下一个线程
              return true;
          }
          return false;
      }
      

      **unparkSuccessor(Node):使用 unpark() 将最前面的未放弃的线程,并将他唤醒。

      private void unparkSuccessor(Node node) {
          // node一般为当前线程所在的结点。
          int ws = node.waitStatus;
          if (ws < 0)//置零当前线程所在的结点状态,允许失败。
              compareAndSetWaitStatus(node, ws, 0);
      
          Node s = node.next;//找到下一个需要唤醒的结点s
          if (s == null || s.waitStatus > 0) { //如果 当前线程的节点的下一个节点被 CANCEL了
              s = null;
              for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找,找到最前面的未放弃的节点
                  if (t.waitStatus <= 0) //从这里可以看出,<=0的结点,都是还有效的结点。
                      s = t;
          }
          if (s != null)
              LockSupport.unpark(s.thread);//唤醒
      }
      

ReentrantLock

  • 非公平锁

  • 公平锁

  • 可中断锁

    lockInterruptibly() 方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。当两个线程同时通过 lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用 threadB.interrupt()方法能够中断线程B的等待过程。注意,当一个线程获取了锁之后,是不会被 interrupt()方法中断的。而用 synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

  • Condition

  • ReentrantReadWriteLock

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值