【Java并发知识总结 | 第三篇】深入理解并暴打AQS原理、ReentrantLock锁

3.深入理解AQS、ReentrantLock

在这里插入图片描述

3.1AQS

3.1.1AQS简介

AQS即队列同步器AbstractQueuedSynchronizer(后面简称AQS)是实现锁和有关同步器的一个基础框架。

在JDK5中,Doug Lea在并发包中加入了大量的同步工具,例如==重入锁(ReentrantLock)、读写锁(ReentrantReadWriteLock)、信号量(Semaphore)、CountDownLatch(倒计时锁)==等,都是基于AQS的。

其内部通过一个被标识为volatile的名为state的变量来控制多个线程之间的同步状态。多个线程之间可以通过AQS来独占式或共享式的抢占资源。

基于AQS,可以很方便的实现Java中不具备的功能。

例如,在锁这个问题上,Java中提供的是synchronized关键字,用这个关键字可以很方便的实现多个线程之间的同步。但这个关键字也有很多缺陷,比如:

  • 不支持超时的获取锁:一个线程一旦没有从synchronized上获取锁,就会卡在这里,没有机会逃脱。所以通常由synchronized造成的死锁是无解的。
  • 不可响应中断
  • 不能尝试获取锁。如果尝试获取时没获取到,立刻返回,synchronized不具备这一特性。

而ReentrantLock基于AQS将上述几点都做到了。

3.1.2核心结构

从AbstractQueuedSynchronizer的名字可以看出,AQS中一定是基于队列实现的(Queue)

在AQS内部,是通过链表实现的队列

链表的每个元素是其内部类Node的一个实现。然后AQS通过实例变量head指向队列的头,通过实例变量tail指向队列的尾。

img

其源码定义如下:

/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
private transient volatile Node head;

/**
 * Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;

/**
 * The synchronization state.
 */
private volatile int state;


static final class Node {
   

	/** 标识为共享式 */
    static final Node SHARED = new Node();
    /** 标识为独占式 */
    static final Node EXCLUSIVE = null;

	/** 同步队列中等待的线程等待超时或被中断,需要从等待队列中取消等待,进入该状态的节点状态将不再变化 */
    static final int CANCELLED =  1;

    /** 当前节点的后继节点处于等待状态,且当前节点释放了同步状态,需要通过unpark唤醒后继节点,让其继续运行 */
    static final int SIGNAL    = -1;

    /** 当前节点等待在某一Condition上,当其他线程调用这个Conditino的signal方法后,该节点将从等待队列恢复到同步队列中,使其有机会获取同步状态 */
    static final int CONDITION = -2;

    /** 表示下一次共享式同步状态获取状态将无条件的传播下去 */
    static final int PROPAGATE = -3;

	/* 当前节点的等待状态,取值为上述几个常量之一,另外,值为0表示初始状态 */
    volatile int waitStatus;

    /* 前驱节点 */
    volatile Node prev;

    /* 后继节点 */
    volatile Node next;

    /* 等待获取同步状态的线程 */
    volatile Thread thread;

    /* 等待队列中的后继节点 */
    Node nextWaiter;
    
    // ...
}
(1)设计模型

img

(2)组成部分

AQS 主要由三部分组成

  1. state 同步状态
  2. Node 组成的 CLH 队列
  3. ConditionObject 条件变量(包含 Node 组成的条件单向队列)。

state 用 volatile 来修饰,保证了我们操作的可见性,所以任何线程通过 getState() 获得状态都是可以得到最新值,但是 setState() 无法保证原子性,因此 AQS 给我们提供了 compareAndSetState 方法利用底层 UnSafe 的 CAS 功能来实现原子性。

(3)State关键字

对于 AQS 来说,线程同步的关键是对 state 的操作,可以说获取、释放资源是否成功都是由 state 决定的,比如 state>0 代表可获取资源,否则无法获取,所以 state 的具体语义由实现者去定义,现有的 ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 定义的 state 语义都不一样。

  • ReentrantLock 的 state 用来表示是否有锁资源,变量记录了锁的重入次数
  • ReentrantReadWriteLockstate 高 16 位代表读锁状态,低 16 位代表写锁状态
  • Semaphore 的 state 用来表示可用信号的个数
  • CountDownLatchstate 用来表示计数器的值

3.1.3实现的两类队列

  1. 同步队列:服务于线程阻塞等待获取资源
  2. 条件队列:服务于线程因某个条件不满足而进入等待状态。 条件队列中的线程实际上已经获取到了资源,但是没有能够继续执行下去的条件,所以被打入条件队列并释放持有的资源,以让渡其它线程执行,如果未来某个时刻条件得以满足,则该线程会被从条件队列转移到同步队列,继续参与竞争资源,以继续向下执行。
(1)同步队列
①CLH

同步队列是基于链表实现的双向队列,也是 CLH 锁的变种。CLH 锁是 AQS 队列同步器实现的基础。

以下图为CLH的构成

img

  1. CLH 锁是有由 Craig, Landin, and Hagersten 这三个人发明的锁,取了三个人名字的首字母,所以叫 CLH Lock
  2. CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。
  3. CLH 队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋
②Node

AQS 以内部类 Node 的形式定义了同步队列结点。这就是前文看到的第一个内部类。

static final class Node {
   

    /** 模式定义 */

    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    /** 线程状态 */

    static final int CANCELLED = 1;
    static final int SIGNAL = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    /** 线程等待状态 */
    volatile int waitStatus;

    /** 前驱结点 */
    volatile Node prev;
    /** 后置结点 */
    volatile Node next;

    /** 持有的线程对象 */
    volatile Thread thread;

    /** 对于独占模式而言,指向下一个处于 CONDITION 等待状态的结点;对于共享模式而言,则为 SHARED 结点 */
    Node nextWaiter;

    // ... 省略方法定义
}

Node 在 CLH 的基础上进行了变种。

CLH 是单向队列,其主要特点是自旋检查前驱节点的 locked 状态

而 AQS 同步队列是 双向队列,每个节点也有状态 waitStatus,而其并不是一直对前驱节点的状态自旋判断,而是自旋一段时间后阻塞让出 cpu 时间片(上下文切换),等待前驱节点主动唤醒后继节点

waitStatus 有如下 5 中状态:

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

从上面的代码中可以看出,位于 CLH 链表中的线程以 2 种模式在等待资源,即 SHARED 和 EXCLUSIVE,其中 SHARED 表示共享模式,而 EXCLUSIVE 表示独占模式。

共享模式与独占模式的主要区别在于,同一时刻独占模式只能有一个线程获取到资源,而共享模式在同一时刻可以有多个线程获取到资源。典型的场景就是读写锁,读操作可以有多个线程同时获取到读锁资源,而写操作同一时刻只能有一个线程获取到写锁资源,其它线程在尝试获取资源时都会被阻塞。

③主要行为

AQS 类成员变量 head 和 tail 字段分别指向同步队列的头结点和尾结点:

	 /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

其中 head 表示同步队列的头结点,而 tail 则表示同步队列的尾结点,具体组织形式如下图:

img

当调用 AQS 的 acquire 方法获取资源时,如果资源不足则当前线程会被封装成 Node 结点添加到同步队列的末端(入队),头结点 head 用于记录当前正在持有资源的线程结点,而 head 的后继结点就

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来自梦里的一条鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值