Android—JUC闭锁与AQS技术

132 阅读8分钟

AbstractQueuedSynchronizer简介

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

AQS 核心思想

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

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

AQS 对资源的共享方式

AQS定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

AbstractQueuedSynchronizer数据结构

AbstractQueuedSynchronizer类底层的数据结构是使用CLH队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。

编辑

添加图片注释,不超过 140 字(可选)

AQS概要

AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),在JDK1.5时,Doug Lea引入了J.U.C包,该包中的大多数同步器都是基于AQS来构建的。AQS框架提供了一套通用的机制来管理同步状态(synchronization state)、阻塞/唤醒线程、管理等待队列。如ReentrantLock、CountDownLatch、CyclicBarrier等同步器都是基于AQS框架实现,同时AQS提供了了以下功能:

  • 支持一套模板框架;
  • 支持中断和超时;
  • 支持独占和共享模式;
  • 支持Condition条件;

AQS方法说明

  • 模板方法,其中大多数方法都是final或是private的,我们把这类方法称为Skeleton Method,也就是说这些方法是AQS框架自身定义好的骨架,子类是不能覆写的。
方法名描述
tryAcquire排它获取
tryRelease排它释放
tryAcquireShared共享获取
tryReleaseShared共享释放
  • CAS操作,即CompareAndSet,在Java中CAS操作的实现都委托给一个名为UnSafe类来保证字段的原子性。
方法名描述
compareAndSetStateCAS修改同步状态值
compareAndSetWaitStatusCAS修改结点的等待状态
compareAndSetTailCAS修改等待队列的尾指针
compareAndSetHeadCAS修改等待队列的头指针
  • 等待队列操作
方法名描述
addWaiter入队操作
unparkSuccessor唤醒后继结点
doReleaseShared释放共享结点
  • 资源获取操作
方法名描述
acquire独占地获取资源
acquireShared共享地获取资源
acquireQueued尝试获取资源,获取失败尝试阻塞线程
shouldParkAfterFailedAcquire判断是否阻塞当前调用线程
  • 资源释放操作
方法名描述
release释放独占资源
releaseShared释放共享资源

AQS实现原理

我们先来看下AQS的定义模型如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
//指向同步队列队头
private transient volatile Node head;
//指向同步的队尾
private transient volatile Node tail;
//同步状态,0代表锁未被占用,1代表锁已被占用
private volatile int state;
.........
}

我们可以看到AQS模型内部围绕着如何实现同步状态管理,线程同步队列的管理以及线程的阻塞和唤醒操作等方面来展开。

  • 同步状态 同步状态,其实就是资源。AQS使用state来保存同步状态,并暴露出getState、setState以及compareAndSetState操作来读取和更新这个状态。
  • 当state=0时,则说明没有任何线程占有共享资源的锁;
  • 当state=1时,则说明有线程目前正在使用共享资源,其他线程必须加入同步队列进行等待;
  • 等待队列 等待队列(CLH)是管理排队等待线程的同步工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。以下是同步队列的模型图:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

CLH队列模型图 说明: head和tail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。其中Node结点是对每一个访问同步代码的线程的封装,从图中的Node的数据结构也可看出,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类,其数据结构如下:

static final class Node {
​
    // 共享模式结点
    static final Node SHARED = new Node();
​
    // 独占模式结点
    static final Node EXCLUSIVE = null;
​
    // 1 - 取消,表示后续结点被中断或超时,需要移出队列;
    static final int CANCELLED =  1;
​
    //  -1- 发信号,表示后续结点被阻塞了;(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)
    static final int SIGNAL    = -1;
​
    // -2- Condition专用,表示当前结点在Condition队列中,因为等待某个条件而被阻塞了;
    static final int CONDITION = -2;
​
    // -3- 传播,适用于共享模式。(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。)
    static final int PROPAGATE = -3;
​
  //等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4种,值为0,代表初始化状态。
    volatile int waitStatus;
​
    // 前驱指针
    volatile Node prev;
​
    // 后驱指针
    volatile Node next;
​
    // 结点所包装的线程
    volatile Thread thread;
​
    // Condition队列使用,存储condition队列中的后继节点
    Node nextWaiter;
}

具体说明: 1.SHARED和EXCLUSIVE常量分别代表共享模式和独占模式,所谓共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的,而独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock。变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。 2.pre和next,分别指向当前Node结点的前驱结点和后继结点; 3.thread变量存储的请求锁的线程; 4.nextWaiter,与Condition相关,代表等待队列中的后继结点;

  • 线程的阻塞和唤醒 在JDK1.5之前,除了内置的监视器机制外,没有其它方法可以安全且便捷得阻塞和唤醒当前线程。JDK1.5以后,java.util.concurrent.locks包提供了LockSupport类来作为线程阻塞和唤醒的工具,如使用park() 和 unpark() 分别是阻塞线程和解除阻塞线程。

以上就是Android开发中.JUC闭锁与AQS技术解析;更多Android高级进阶开发技术,可参考传送直达↓↓↓ :link.juejin.cn/?target=htt…里面记录了30几个板块技术,上千个技术可帮助大家学习。