JUC并发编程第十二章——AQS

1 前置知识

  • 公平锁和非公平锁
    • 公平锁:锁被释放以后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁
    • 非公平锁:锁被释放以后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁
  • 可重入锁
    • 也叫做递归锁,指的是线程可以再次获取自己的内部锁,比如一个线程获取到了对象锁,此时这个对象锁还没有释放,当其想再次获取这个对象锁的时候还是可以获取的,如果不可重入的话,会导致死锁。
  • 自旋思想
    • 当线程请求锁时,如果锁已经被其他线程持有,那么该线程会不断地重试获取锁,而不是被挂起等待,这种不断尝试获取锁的行为称为自旋
  • LockSupport
    • 一个工具类,用于线程的阻塞和唤醒操作,类似于wait()和notify()方法,但是更加灵活和可控
    • 提供了park()和unpark()两个静态方法用于线程阻塞和唤醒操作。
    • 优点在于可以在任意时刻阻塞和唤醒线程而不需要事先获取锁或监视器对象。
  • 数据结构之双向链表
    • 双向链表(Doubly Linked List)是一种常见的数据结构,它是由一系列结点(Node)组成的,每个结点包含三个部分:数据域、前驱指针和后继指针。其中,数据域存储结点的数据,前驱指针指向前一个结点,后继指针指向后一个结点。通过这种方式,双向链表可以实现双向遍历和插入、删除操作。
  • 设计模式之模板设计模式
    • 模板设计模式是一种行为型设计模式,定义了一种算法的框架,并将某些步骤延迟到子类中事先,这种设计模式的主要目的是允许子类在不改变算法结构的情况下重新定义算法中的某些步骤。
    • 优点是能够提高代码复用性和可维护性。

2 AQS入门级别理论知识

2.1 是什么?

AQS:全称AbstractQueueSynchronizer,抽象的队列同步器

AQS(AbstractQueuedSynchronizer)是Java并发编程框架中的一个核心组件,位于java.util.concurrent.locks包下。它是构建锁和其他同步器组件(如Semaphore、CountDownLatch、ReentrantLock等)的基础框架类。AQS的设计目标是为实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器提供一个高度可复用的框架

技术解释

  • 是用来实现锁或者其他同步器组件的公共基础部分的抽象实现
  • 是重量级基础框架及整个JUC体系的基石主要用于解决锁分配给”谁“的问题
  • 整体就是一个抽象的FIFO队列(先进先出的等待队列)来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态

2.2 AQS为什么是JUC内容中最重要的基石

和AQS有关的类:

  • java.util.concurrent.locks.ReentrantLock
  • java.util.concurrent.CountDownLatch
  • java.util.concurrent.locks.ReentrantReadWriteLock
  • java.util.concurrent.Semaphore

这些并发编程中常用的类,底层实现基本上都和AQS有关

ReentrantLock

CountDownLatch

ReentrantReadWriteLock

Semaphore

..............

可以看到以上类的内部都有一个继承了AQS的抽象类Sync。

进一步理解锁和同步器的关系

  • 锁,面向锁的使用者:定义了程序员和锁交互的使用层API,隐藏了实现细节,程序员调用即可实现功能
  • 同步器,面向锁的实现者:Java并发大神DoungLee,提出了统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的----公共基础部分

2.3 能干嘛?

所谓的AQS实际上就是一种抽象的队列同步器。主要作用就是:在进行等待的时候,后续线程唤醒等待的一种机制。通过维护state状态来实现这个功能。

加锁会导致阻塞------有阻塞就需要排队,实现排队必然需要队列

  • 抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占失败的线程继续去等待(类似于银行办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等待),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

  • 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象 (Node)(简单来说,正在排队的线程就是队列中的一个个Node对象),通过CAS、自旋以及LockSupport.park()的方式,维护着state变量的状态(这个状态用于标志资源是否被占用),使其达到同步的状态。

AQS源码说明:

        1.内部类Node:每一个等待线程都会被封装到一个Node对象中。

        2.由于这是一个双端队列的数据结构,每个节点有头尾指针:head、tail

        3.同步状态标识:state。被volatile修饰,保证了线程之间的可见性。

        4.对state变量的set、get方法。

源码总结:

  • AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作。
  • 将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配。
  • 通过CAS完成对State值的修改。

AQS同步队列的基本结构


3 AQS源码分析前置知识储备

3.1 AQS内部体系架构图

3.2 AQS内部体系架构----AQS自身

3.2.1 AQS的int类型变量state

  •  AQS的同步状态State成员变量
  • 银行办理业务的受理窗口状态
  • 零就是没人,自由状态可以去办理
  • 大于等于1,有人占用窗口,排队等待 

3.2.2 AQS的CLH队列

        CLH(三个大牛的名字组成)队列为一个双向队列

        类似银行候客区的等待顾客

小总结

  • 有阻塞就需要排队,实现排队必然需要队列
  • AQS实质:State变量+CLH双端队列

3.3 AQS内部体系架构----内部类Node

3.3.1 Node的int变量

  •  Node的等待状态:waitState成员变量 (注意和state变量区分)

  • 说人话
  • 等候区其他顾客(其他线程)的等待状态
  • 队列中每个排队的个体就是一个Node 

3.3.2 Node此类的讲解

  • 内部结构

  • 属性说明


4 AQS源码深度讲解和分析

4.1 ReentrantLock的原理

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的

4.2 从最简单的lock方法开始看看公平和非公平

这里我们可以看出,如果直接调用无参构造方法创建一个ReentrantLock对象,是一个非公平锁。如果传入了boolean类型的参数,则会根据传参创建公平/非公平锁。


ReentrantLock类中有公平/非公平两个内部类,都是继承了Sync(Sync继承了AQS)


    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            //CAS尝试修改state获取锁
            if (compareAndSetState(0, 1))//如果获取成功
                //设置当前独占锁的线程为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else//获取失败
                acquire(1);//调用acquire方法
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

acquire方法的代码示例:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//自我中断当前线程
            //当一个线程在等待锁的过程中被中断时,它需要能够及时地响应这个中断,
            //以便执行中断相关的清理工作或者重新评估其执行策略。
    }

在这个示例中,tryAcquire(arg)尝试获取锁,如果失败,则通过addWaiter(Node.EXCLUSIVE)将当前线程包装成一个等待者节点并添加到等待队列中,然后通过acquireQueued(addWaiter(Node.EXCLUSIVE), arg)等待锁的释放。

这看看NonfairSync中的lock方法:

  • lock中调用了compareAndSetState(0, 1)方法,CAS操作尝试修改state以获取锁。
  • 第一个参数:0表示当前线程希望当前state的值为0(表示无人使用锁的状态)
  • 第二个参数:1表示如果当前线程能成功获取锁,就把state的值改为1,表示有人获取了锁
  • 如果获取锁成功,调用setExclusiveOwnerThread(Thread.currentThread())方法。如果获取锁失败则调用acquire方法

FairSync中的lock类似


公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()——公平锁加锁时判断等待队列中是否存在排队的线程。

公平锁会调用hasQueuedPredecessors方法,在判断了前面没有排队的线程时,才可能获取锁

/**
 * 查询是否有线程等待获取的时间比当前线程长
 * 如果当前线程之前有一个排队的线程,返回true;如果当前线程处于队列的头部或队列为空,则返回false
 */
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

        对比公平锁和非公平锁的tryAcquire()方法的实现代码,其实差异就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

  • 公平锁:公平锁讲究先来后到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入到等待队列中;
  • 非公平锁:不管是否有等待队列,如果可以获取到锁,则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,它还需要参加竞争锁(存在线程竞争的情况下),后来的线程可能不讲武德插队夺锁了。


4.3 进一步解读非公平锁ReentrantLock()源码

以非公平锁ReentrantLock()为例正式开始源码解读(公平锁同理)


4.3.1 lock()

final void lock() {
    // cas修改state成员变量
    if (compareAndSetState(0, 1))
        // 修改成功,设置独占锁的线程为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 修改失败,当前线程抢占失败,排队等待后续抢占
        acquire(1);
}

4.3.2 acquire()

acquire方法主要有三条流程

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        // Node.EXCLUSIVE表示独占模式
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// 非公平锁的tryAcquire()方法实现
// true抢锁成功;false抢锁失败
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取state成员变量
    int c = getState();
    // 独占锁没有被占用
    if (c == 0) {
      // cas修改state成员变量
      if (compareAndSetState(0, acquires)) {
        // 修改成功,设置独占锁的线程为当前线程
        setExclusiveOwnerThread(current);
        return true;
      }
    }
    // 独占锁的线程为当前线程
    else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0) // overflow
        throw new Error("Maximum lock count exceeded");
      // 把锁的state成员变量再+1
      // 相当于当前线程重复获取锁,ReentrantLock是可重入锁
      setState(nextc);
      return true;
    }
    return false;
}

// 为当前线程和给定模式创建排队节点
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
      node.prev = pred;
      if (compareAndSetTail(pred, node)) {
        pred.next = node;
        return node;
      }
    }
    enq(node);
    return node;
}

// 入队
private Node enq(final Node node) {
    for (;;) {
      // tail为尾节点
      Node t = tail;
      // 尾节点为null,初始化队列
      if (t == null) { // Must initialize
        // 初始化队列设置头节点并不是传入的节点,而是new Node(),
        // 新创建叫做虚拟节点或者哨兵节点,作用就是占位,而且Thread=null,waitStatus=0
        // 注意:双向链表中,第一个节点为虚拟节点(也叫哨兵节点),其实并不存储任何信息,只是占位,真正的第一个有数据的节点,是从第二个节点开始的
        if (compareAndSetHead(new Node()))
          tail = head;
      } else {
        // 传入的节点的prev节点指向tail节点
        node.prev = t;
        // 传入的节点变为尾节点
        if (compareAndSetTail(t, node)) {
          // 把tail的next节点指向传入的节点
          t.next = node;
          return t;
        }
      }
    }
}

// 已经在队列中的当前线程尝试获取独占锁
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
      boolean interrupted = false;
      for (;;) {
        // 获取当前节点的前一个节点,声明为p
        final Node p = node.predecessor();
        // 如果p节点为头节点,且再次尝试获取锁成功
        if (p == head && tryAcquire(arg)) {
          // 设置头节点为当前节点
          setHead(node);
          // p节点的后一个节点设置为null
          p.next = null; // help GC
          failed = false;
          return interrupted;
        }
        if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
          interrupted = true;
      }
    } finally {
      // 异常情况,取消正在进行的获取独占锁的线程节点,出队
      if (failed)
        cancelAcquire(node);
    }
}

// 返回前一个节点,如果为空则抛出NullPointerException
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
      throw new NullPointerException();
    else
      return p;
}

// 检查并更新获取失败节点的状态。如果线程阻塞,返回true。这是所有循环获取锁环路中的主要信号控制。要求pred == node.prev。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前置节点的状态
    int ws = pred.waitStatus;
    // 如果是SIGNAL状态,即等待被占用的资源释放,直接返回true
    // 准备继续调用parkAndCheckInterrupt()方法
    if (ws == Node.SIGNAL)
      /*
       * This node has already set status asking a release
       * to signal it, so it can safely park.
       */
      return true;
    // ws>0说明是CANCELLED状态
    if (ws > 0) {
      /*
       * Predecessor was cancelled. Skip over predecessors and
       * indicate retry.
       */
      // 循环判断前置节点的前置节点是否也是CANCELLED状态,忽略该状态的节点,重新连接队列
      do {
        node.prev = pred = pred.prev;
      } while (pred.waitStatus > 0);
      pred.next = node;
    // waitStatus一定为0或PROPAGATE。表示我们需要信号,先不要停车。调用方需要重试以确保在停车前无法获取锁
    } else {
      /*
       * waitStatus must be 0 or PROPAGATE.  Indicate that we
       * need a signal, but don't park yet.  Caller will need to
       * retry to make sure it cannot acquire before parking.
       */
      // 设置前置节点的waitStatus为Node.SIGNAL=-1,用于后续唤醒操作
      // 程序第一次执行到这返回为false,还会进行外层第二次循环,最终从if (ws == Node.SIGNAL)返回true
      compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

// 方便方法停车,然后检查是否中断
private final boolean parkAndCheckInterrupt() {
    // 挂起当前线程,使得当前线程停留在队列中
    // 根据 park 方法 API描述,程序在下达三种情况会继续向下执行
    // 1.unpark
    // 2.被中断(interrupt)
    // 3.其他不合逻辑的返回才会继续向下执行
    LockSupport.park(this);
    // 因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态l/ 如泉由于被中断,该方法会返回 true
    return Thread.interrupted();
}

// 取消正在进行的获取独占锁的线程节点
// 不想等了,离队
private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
      return;

    node.thread = null;

    // 获取传入节点的前一个节点
    // Skip cancelled predecessors
    Node pred = node.prev;
    // 如果传入节点的前一个节点的waitStatus > 0
    // 这种情况是存在多个节点同时离队
    // 需要找到最近的一个不离队的节点赋值给node.prev
    while (pred.waitStatus > 0)
      node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    // 获取传入节点的前一个节点的下一个节点
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    // 设置传入节点的waitStatus为Node.CANCELLED:表示线程获取锁的请求已经取消
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    // 如果传入节点是尾节点,且传入节点的前一个节点设置为尾节点成功
    if (node == tail && compareAndSetTail(node, pred)) {
      // 将传入节点的前一个节点的下一个节点设置为null 
      compareAndSetNext(pred, predNext, null);
    } else {
      // If successor needs signal, try to set pred's next-link
      // so it will get one. Otherwise wake it up to propagate.
      int ws;
      // 传入节点的前一个节点不是头节点
      if (pred != head &&
          ((ws = pred.waitStatus) == Node.SIGNAL ||
           (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
          pred.thread != null) {
        // 获取传入节点的下一个节点
        Node next = node.next;
        // 如果传入节点的下一个节点不为null,且传入节点的下一个节点的waitStatus <= 0
        if (next != null && next.waitStatus <= 0)
          // 将传入节点的前一个节点的下一个节点设置为传入节点的下一个节点
          compareAndSetNext(pred, predNext, next);
      } else {
        unparkSuccessor(node);
      }

      node.next = node; // help GC
    }
}

// 当前线程自中断
static void selfInterrupt() {
  	Thread.currentThread().interrupt();
}

4.3.3 unlock()

// 试图释放此锁
public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    // 尝试释放锁
    if (tryRelease(arg)) {
      Node h = head;
      // 如果头节点不为null,且waitStatus不为0
      if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);
      return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    // 当前线程占用锁getState()为1,所以c=0
    int c = getState() - releases;
    // 如果当前线程不等于持有锁的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
      throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
      free = true;
      // 设置独占锁的所有者线程为null
      setExclusiveOwnerThread(null);
    }
    // 设置当前线程的state为c=0
    setState(c);
    return free;
}

// 唤醒节点的后继者(如果存在)
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    // 如果传入节点的waitStatus < 0,重新设置为0
    if (ws < 0)
      compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    // 获取传入节点的下一个节点
    Node s = node.next;
    // 下一个节点为null或者waitStatus > 0
    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;
    }
    // 下一个节点不为null,唤醒下一个节点
    if (s != null)
      // 之前的parkAndCheckInterrupt()方法就会return Thread.interrupted();返回false
      // 之后下一个节点继续执行acquireQueued()方法的循环,执行p == head && tryAcquire(arg)尝试获取锁
      LockSupport.unpark(s.thread);
}

4.3.4 总结ReentrantLock()的加锁过程主要分为三个阶段

  1. 尝试加锁
  2. 加锁失败,线程入队列
  3. 线程入队列后,进入阻塞状态
  • 56
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值