前言
大家好,牧码心今天给大家推荐一篇并发编程系列(八)—初识JUC锁和AQS的文章,希望对你有所帮助。内容如下:
- 概要
- JUC锁框架
- AQS概要
- AQS方法
- AQS原理
概要
前面系列文章中我们已经讲过synchronized 同步锁机制,是依靠JVM对象的内置锁实现同步,是一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,如获取和释放锁的灵活性,不可中断等。为了弥补synchronized的局限性,在 JDK1.5 之后,Java引入的并发包java.util.concurrent中, 提供了不同类型的JUC锁,用来提供更多扩展的加锁功能。本文将介绍JUC锁的框架,每个组件的说明与AQS的联系。
JUC锁框架
相比同步锁,JUC包中的锁的功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁。JUC包的中锁包含:Lock接口,ReadWriteLock接口,LockSupport原语,Condition条件,AbstractOwnableSynchronizer/AbstractQueuedSynchronizer/AbstractQueuedLongSynchronizer三个抽象类,ReentrantLock独占锁,ReentrantReadWriteLock读写锁以及JDK1.8后引入的增加的读写锁StampedLock等,它们都是基于AQS实现。我们来看下JUC锁的整体框架图,如下所示:
从图中我们JUC锁框架中各组件的依赖,继承,实现等关系。下面是对每个组件的简单说明:
-
Lock接口
Lock接口定义了不同语义的锁规则,如公平锁,非公平锁,可重入锁等,也提供了lock()方法和unLock()方法对显式加锁和显式释放锁的支持。 -
ReadWriteLock接口
ReadWriteLock接口定义读写锁的规则,允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。读写锁内部维护了两个锁,一个是用于读操作的 ReadLock,一个是用于写操作的 WriteLock。 -
三个抽象类:AbstractQueuedLongSynchronizer/AbstractQueuedSynchronizer/AbstractOwnableSynchronizer
AbstractQueuedSynchronizer就是被称之为AQS的类,它是一个非常有用的超类,可用来定义锁以及依赖于排队阻塞线程的其他同步器;ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier和Semaphore等这些类都是基于AQS类实现的。AbstractQueuedLongSynchronizer 类提供相同的功能但扩展了对同步状态的 64 位的支持。两者都扩展了类 AbstractOwnableSynchronizer(一个帮助记录当前保持独占同步的线程的简单类)。 -
Condition 条件
Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。Condition需要和Lock联合使用,它的作用是代替Object监视器方法,可以通过await(),signal()来休眠/唤醒线程。 -
LockSupport 类
LockSupport提供“创建锁”和“其他同步类的基本线程阻塞原语”。其功能和"Thread中的Thread.suspend()和Thread.resume()有点类似",LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程。但是park()和unpark()不会遇到“Thread.suspend 和Thread.resume所可能引发的死锁”问题。 -
ReentrantLock 类
ReentrantLock是独占锁。所谓独占锁是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。ReentrantLock锁包括公平的锁"和"非公平的锁。公平的锁是指不同线程获取锁的机制是公平的,而非公平的锁则是指不同线程获取锁的机制是非公平的,ReentrantLock是可重入的锁。 -
ReentrantReadWriteLock类
ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括子类ReadLock和WriteLock。ReentrantLock是共享锁,而WriteLock是独占锁。 -
StampedLock类
StampedLock类在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。
AQS概要
AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),在JDK1.5时,Doug Lea引入了J.U.C包,该包中的大多数同步器都是基于AQS来构建的。AQS框架提供了一套通用的机制来管理同步状态(synchronization state)、阻塞/唤醒线程、管理等待队列。如ReentrantLock、CountDownLatch、CyclicBarrier等同步器都是基于AQS框架实现,同时AQS提供了了以下特性:
- 阻塞等待队列;
- 支持中断和超时;
- 支持独占和共享模式;
- 支持可重入;
- 支持公平和非公平锁;
AQS方法说明
- 模板方法,其中大多数方法都是final或是private的,我们把这类方法称为Skeleton Method,也就是说这些方法是AQS框架自身定义好的骨架,子类是不能覆写的。
方法名 | 描述 |
---|---|
tryAcquire | 独占方式。尝试获取资源,成功则返回true,失败则返回false |
tryRelease | 独占方式。尝试释放资源,成功则返回true,失败则返回false |
tryAcquireShared | 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源 |
tryReleaseShared | 共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false |
- CAS操作,即CompareAndSet,在Java中CAS操作的实现都委托给一个名为UnSafe类来保证字段的原子性。
方法名 | 描述 |
---|---|
compareAndSetState | CAS修改同步状态值 |
compareAndSetWaitStatus | CAS修改结点的等待状态 |
compareAndSetTail | CAS修改等待队列的尾指针 |
compareAndSetHead | CAS修改等待队列的头指针 |
- 等待队列操作
方法名 | 描述 |
---|---|
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)是一种基于双向链表的FIFO方式的等待队列,管理等待线程的同步工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。以下是同步等待队列的模型图:
说明: head和tail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。其中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。
2.变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。
3.pre和next,分别指向当前Node结点的前驱结点和后继结点;
4.thread变量存储的请求锁的线程;
5.nextWaiter,与Condition相关,代表等待队列中的后继结点;
- 线程的阻塞和唤醒
在JDK1.5之前,除了内置的监视器机制外,没有其它方法可以安全且便捷得阻塞和唤醒当前线程。JDK1.5以后,java.util.concurrent.locks包提供了LockSupport类来作为线程阻塞和唤醒的工具,如使用park() 和 unpark() 分别是阻塞线程和解除阻塞线程。
参考
- https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html