图解 AQS!

  • 理解 AQS!

  • Java中 AQS 是 AbstractQueuedSynchronizer 类,AQS 依赖 FIFO 队列来提供一个框架,这个框架用于实现锁以及锁相关的同步器,比如信号量、事件等。

  • 在 AQS 中,主要有两部分功能,一部分是操作 state 变量,第二部分是实现排队和阻塞机制。

  • 注意,AQS 并没有实现任何同步接口,它只是提供了类似 acquireInterruptible 的方法,调用这些方法可以实现锁和同步器。

  • 1 管程模型

  • Java 使用 MESA 管程模型来管理类的成员变量和方法,让这个类的成员变量和方法的操作是线程安全的。下图是 MESA 管程模型,里面除了定义共享变量外,还定义了条件变量和条件变量等待队列:

  • 图解 AQS!_第1张图片

  • 上图中有三个知识点:

    • MESA 管程模型封装了共享变量和对共享变量的操作,线程要进入管程内部,必须获取到锁,如果获取锁失败就进入入口等待队列阻塞等待。
    • 如果线程获取到锁,就进入到管程内部。但是进入到管程内部,也不一定能立刻操作共享变量,而是要看条件变量是否满足,如果不满足,只能进入条件变量等待队列阻塞等待。
    • 在条件变量等待队列中,如果被其他线程唤醒,也不一定能立刻操作共享变量,而是需要去入口等待队列重新排队等待获取锁。
  • Java 中的 MESA 管程模型有一点改进,就是管程内部只有一个条件变量和一个等待队列。下图是 AQS 的管程模型:

  • 图解 AQS!_第2张图片

  • AQS 的管程模型依赖 AQS 中的 FIFO 队列实现入口等待队列,要进入管程内部,就由各种并发锁的限制。而 ConditionObject 则实现了条件队列,这个队列可以创建多个。

  • 下面就从入口等待队列、并发锁、条件等待队列三个方面来带你彻底理解 AQS。

  • 2 入口等待队列

  • 2.1 获取独占锁

  • 独占, 忽略 interrupts

  • public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
  • 这里的 tryAcquire 是抽象方法,由 AQS 的子类来实现,因为每个子类实现的锁是不一样的。

  • 2.1.1 入队

  • 上面的代码可以看到,获取锁失败后,会先执行 addWaiter 方法加入队列,然后执行 acquireQueued 方法自旋地获取锁直到成功。

  • addWaiter 代码逻辑如下图,简单说就是把 node 入队,入队后返回 node 参数给 acquireQueued 方法:

  • 图解 AQS!_第3张图片

  • 这里有一个点需要注意,如果队列为空,则新建一个 Node 作为队头。

  • 2.1.2 入队后获取锁

  • acquireQueued 自旋获取锁逻辑如下图:

  • 图解 AQS!_第4张图片

  • 这里有几个细节:

  • 1.waitStatus

    • CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化;
    • SIGNAL(-1):后面节点等待当前节点唤醒;
    • CONDITION(-2):Condition 中使用,当前线程阻塞在 Condition,如果其他线程调用了 Condition 的 signal 方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁;
    • PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;
    • 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。
  • 2.获取锁失败后挂起

  • 如果前置节点不是头节点,或者前置节点是头节点但当前节点获取锁失败,这时当前节点需要挂起,分三种情况:

    • 前置节点 waitStatus=-1,如下图:
  • 图解 AQS!_第5张图片

    • 前置节点 waitStatus > 0,如下图:
  • 图解 AQS!_第6张图片

    • 前置节点 waitStatus < 0 但不等于 -1,如下图:
  • 图解 AQS!_第7张图片

  • 3.取消获取锁

  • 如果获取锁抛出异常,则取消获取锁,如果当前节点是 tail 节点,分两种情况如下图:

  • 图解 AQS!_第8张图片

  • 如果当前节点不是 tail 节点,也分两种情况,如下图:

  • 图解 AQS!_第9张图片

  • 4.对中断状态忽略

  • 5.如果前置节点的状态是 0 或 PROPAGATE,会被当前节点自旋过程中更新成 -1,以便之后通知当前节点。

  • 2.1.3 独占 + 响应中断

  • 对应方法 acquireInterruptibly(int arg)。

  • 跟忽略中断(acquire方法)不同的是要响应中断,下面两个地方响应中断:

    • 获取锁之前会检查当前线程是否中断。
    • 获取锁失败入队,在队列中自旋获取锁的过程中也会检查当前线程是否中断。如果检查到当前线程已经中断,则抛出 InterruptedException,当前线程退出。
  • 2.1.4 独占 + 响应中断 + 考虑超时

  • 对应方法 tryAcquireNanos(int arg, long nanosTimeout)。

  • 这个方法具备了独占 + 响应中断 + 超时的功能,下面2个地方要判断是否超时:

    • 自旋获取锁的过程中每次获取锁失败都要判断是否超时;
    • 获取锁失败 park 之前要判断超时时间是否大于自旋的阈值时间 (spinForTimeoutThreshold = 1ns) 另外,park 线程的操作使用 parkNanos 传入阻塞时间。
  • 2.2 释放独占锁

  • 独占锁释放分两步:释放锁,唤醒后继节点。

  • 释放锁的方法 tryRelease 是抽象的,由子类去实现。

  • 我们看一下唤醒后继节点的逻辑,首先需要满足两个条件:

    • head 节点不等于 null;
    • head 节点 waitStatus 不等于 0。这里有两种情况(在方法 unparkSuccessor):
    • 情况一,后继节点 waitStatus <= 0,直接唤醒后继节点,如下图:
  • 图解 AQS!_第10张图片

    • 情况二:后继节点为空或者 waitStatus > 0,从后往前查找最接近当前节点的节点进行唤醒,如下图:
  • 图解 AQS!_第11张图片

  • 2.3 获取共享锁

  • 之前我们讲了独占锁,这一小节我们谈共享锁,有什么不同呢?

  • 2.3.1 共享,忽略 interrupts

  • 对应方法 acquireShared,代码如下:

  • public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
  • 2.3.2 tryAcquireShared

  • 这里获取锁使用的方法是 tryAcquireShared,获取的是共享锁。获取共享锁跟获取独占锁不同的是,会返回一个整数值,说明如下:

    • 返回负数:获取锁失败。
    • 返回 0:获取锁成功但是之后再由线程来获取共享锁时就会失败。
    • 返回正数:获取锁成

你可能感兴趣的:(java,开发语言)