环境:
jdk1.8
摘要说明:
上一章节我们主要阐述了显示锁的使用详解
本章节主要阐述下AQS原理及其使用:
1.模版方法设计模式
2.AQS详解
3.AQS实战
步骤:
1.模版方法模式
模版方法设计模式本质上就是在父类中编排主流程,将步骤实现延迟到子类去实现。主类中需要定义各个步骤的方法但不具体实现,但主类中还需要规定框架方法来组合这些方法来确定主流程;
举例:
消息发送我们可以普遍认为需要有发送方,接收方,发送内容,发送时间,发送方式;其次我们可以定义框架方法来确定发送流程即确定发送方--》确定接收方--》发送内容编写--》确定发送时间--》确定发送方式;
父类代码如下:
package pers.cc.lockAndAQS;
import java.util.Date;
/**
* 使用模版方法模式写一个消息发送
*
* @author cc
*
*/
public abstract class MessageSendTmep {
/**
* 确定发送方
*/
public abstract void from();
/**
* 确定接收方
*/
public abstract void to();
/**
* 确定发送内容
*/
public abstract void content();
/**
* 确定发送时间
*/
public void date() {
System.out.println(new Date());
}
/**
* 确定发送方式
*/
public abstract void send();
/**
* 框架方法,即将子方法的实现交给子类去实现,定义出方法组合
*/
public void messageSend() {
from();
to();
content();
date();
send();
}
}
子类代码如下:
package pers.cc.lockAndAQS;
/**
* 实现发送短险
*
* @author cc
*
*/
public class SendSms extends MessageSendTmep {
@Override
public void from() {
System.out.println("18888888888");
}
@Override
public void to() {
System.out.println("16666666666");
}
@Override
public void content() {
System.out.println("hello world");
}
@Override
public void send() {
System.out.println("电信发送");
}
public static void main(String[] args) {
MessageSendTmep mt = new SendSms();
mt.messageSend();
}
}
运行结果:
18888888888
16666666666
hello world
Mon Mar 04 16:22:11 CST 2019
电信发送
总结:
模版方法模式其实是一个比较容易而且很好理解的模式,主要流程如下:
1、先将主流程框架逻辑设计完成
2、再实现各模块小步骤。
3、不能确实的步骤,作为虚拟方法,甩锅给子类实现。
4、子类只需要聚焦自己的小步骤逻辑。
需要注意以下几点:
1. 保护抽象类中定义算法顺序的方法不被子类修改。
2. 分离可变及不可变部分,让子类自己决定可变部分的实现。
3. 让算法的具体实现对子类开放,对其他类关闭。
2.AQS初体验
AQS全称AbstractQueuedSynchronizer,API上给的定义是提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。
首先我们可以看看有多少工具类的实现用到了抽象类AQS,我们经常使用的ReentrantLock、CountDownLatch等均是AQS的实现:
下面我们先解读CountDownLatch的源码来了解下AQS的具体应用,其中CountDownLatch的具体使用请参考之前的文章:
package java.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class CountDownLatch {
/**
* 定义一个共享信号量
*
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
/**
* 通过父类AQS的setState方法来编写构造方法
*
* @param count
*/
Sync(int count) {
setState(count);
}
/**
* 通过父类AQS的getState来获取信号量
*
* @return
*/
int getCount() {
return getState();
}
/**
* 实现父类AQS定义的tryAcquireShared方法,该方法为共享式获取信号量;此处实现除非信号量为0,否则获取失败,线程进入排队
*/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
/**
* 实现父类AQS的tryReleaseShared方法,该方法为共享式释放信号量方法
*/
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
/**
* 初始化时同时初始化共享信号量
*
* @param count
*/
public CountDownLatch(int count) {
if (count < 0)
throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
/**
* 调用AQS定义的acquireSharedInterruptibly获取信号量,
* 但上述重写了AQS的tryAcquireShared方法就导致调用此方法的线程全部堵塞
*
* @throws InterruptedException
*/
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
/**
* 调用AQS定义的acquireSharedInterruptibly获取信号量,
* 但上述重写了AQS的tryAcquireShared方法就导致调用此方法的线程全部堵塞,超时则返回
*
* @param timeout
* @param unit
* @return
* @throws InterruptedException
*/
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
/**
* 调用重新的releaseShared方法跟新信号量
*/
public void countDown() {
sync.releaseShared(1);
}
/**
* 获取信号量大小
*
* @return
*/
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
通过上述注释我们可以看到,CountDownLatch的实现原理就是利用AQS对信号量的控制:
1、定义了一个内部类实现了抽象类AQS初始化同步状态大小即信号量大小;
2、后续定义共享式释放方法提供修改信号量大小的方法,此方法使用了CAS具有原子性;
3、定义共享式获取方法达到多线程获取信号量时进行堵塞,除非信号量大小为0;
其中的关键点就是:
tryAcquireShared(共享获取方法):通过判断信号量大小是否为0来堵塞获取的线程,达到CountDownLatch的await()堵塞效果;
3.AQS设计思路:
AQS本质提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。
核心设计大多数都是依赖定义的同步状态值state(也被成为信号量),其本质是一个原子的int值;子类需要定义该同步状态值state改变方法,及state的获取和释放方法;且state值只能够使用getState()、setState(int)和compareAndSetState(int, int)方法操作的原子更新的int值以便实现队列阻塞机制;
当线程获取同步状态值state失败时,线程将进入阻塞队列,直到state值发生变更重新唤醒;
4.AQS堵塞队列数据模型:
同步器和同步队列:
增加节点到尾部:
首节点的变化即释放节点:
AQS保存堵塞线程的数据模型如上:
- 通过同步器每次添加线程到双向链表的尾部(tail);
- 且线程节点间上下(prev/next)双向关联;
- 添加尾部节点时使用的是cas原子操作
- 释放堵塞线程先从头部进行释放,且将该线程的next线程设置成头部节点
Node节点(保存线程节点):
节点的五种状态值:
- 0:初始状态
- CANCELLED(1):线程等待超时或者被中断了,需要从队列中移走
- SIGNAL(-1):后续的节点等待状态,当前节点,通知后面的节点去运行
- CONDITION(-2) :当前节点处于等待队列
- PROPAGATE(-3):共享,表示状态要往后面的节点传播
其他:
- Node SHARED表示共享模式下节点
- Node EXCLUSIVE表示独占模式下节点
- int waitStatus:标识节点状态即上述5中状态
- Node prev:上一线程节点
- Node next:下一线程节点
- Thread thread:节点线程
- Node nextWaiter:条件模式下的等待节点
- boolean isShared():判断节点是否是共享模式下
- Node predecessor():获取上一节点,可能返回null
5.AQS的两种模式(独占和共享):
独占模式就是表示同一时刻只能有一个线程获取同步状态state,其他状态获取同步状态则返回false即进入堵塞队列;子类需实现tryAcquire方法来决定如何独占;代表子类为ReentrantLock;
共享模式表示可以有多个线程获取同步状态state大于0即不进入堵塞,知道state减小到小于0才进入堵塞队列:子类需是实现tryAcquireShared来获取同步状态state;代表子类为CountDownLatch;
从这两种模式我们可以将AQS的方法结合模版方法模式区分为以下几种:
框架方法:
独占式获取:
- accquire
- acquireInterruptibly
- tryAcquireNanos
独占式释放锁:
- release
共享式获取:
- acquireShared
- acquireSharedInterruptibly
- tryAcquireSharedNanos
共享式释放锁:
- releaseShared
需要子类覆盖的方法:
流程方法:
- 独占式获取 tryAcquire
- 独占式释放 tryRelease
- 共享式获取 tryAcquireShared
- 共享式释放 tryReleaseShared
- 同步器是否处于独占模式 isHeldExclusively
同步状态state操作方法:
- getState:获取当前的同步状态
- setState:设置当前同步状态
- compareAndSetState 使用CAS设置状态,保证状态设置的原子性
其中同步状态流程如下:
流程说明:
- 调用如tryAcquireNanos框架方法获取同步状态state;
- 通过子类实现的如tryAcquire方法获取state值
- 判断state值是否大于0;大于则获取成功直接退出返回;小于则生成堵塞节点
- 后续state释放,判断堵塞队列头部线程是否中断,中断则变更头部节点,反之则获取同步状态state,同时加下一节点设置成头节点;
6.AQS的ConditionObject
前面我们介绍显示锁的等待唤醒靠lock的condition;他的实现其实就是依赖于AQS的ConditionObject;
一个Conditon包含一个等待队列:
同步队列和等待队列是同时存在的,一个同步器可以控制多个等待队列:
condition的await方法:本质上就是将线程从同步节点构造成等待节点移入等待队列;
condition的singal方法:本质上就是将线程从等待队列中构造成同步节点移到等待队列尾部;
7.实战
我们使用AQS实现一个简单的独占锁:
package pers.cc.lockAndAQS;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import pers.cc.tools.SleepTools;
/**
* 实现AQS构造一个独占锁
*
* @author cc
*
*/
public class SelfLock implements Lock {
/**
* 构建一个内部类来实现AQS
*
* @author cc
*
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 判断state()大小是否为1,为1则被占用
*/
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 获取信号量
*/
protected boolean tryAcquire(int arg) {
// 原子操作设置state值为1,成功返回true
if (compareAndSetState(0, 1)) {
// 设置独占线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 释放信号量。若当前信号量未被占用则抛出异常,反之清空占用线程,设置state为0
*/
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new UnsupportedOperationException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
Condition newCondition() {
return new ConditionObject();
}
}
private final Sync sycn = new Sync();
@Override
public void lock() {
// 调用框架方法获取1个信号量
sycn.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sycn.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sycn.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit)
throws InterruptedException {
return sycn.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sycn.release(1);
}
@Override
public Condition newCondition() {
return sycn.newCondition();
}
public static void main(String[] args) {
Lock lock = new SelfLock();
class Worker extends Thread {
public void run() {
while (true) {
lock.lock();
try {
SleepTools.second(1);
System.out.println(Thread.currentThread().getName());
SleepTools.second(1);
}
finally {
lock.unlock();
}
SleepTools.second(2);
}
}
}
// 启动10个子线程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.setDaemon(true);
w.start();
}
// 主线程每隔1秒换行
for (int i = 0; i < 20; i++) {
SleepTools.second(1);
System.out.println();
}
}
}
运行结果可以看到每次只有一个线程进行打印;
8.总结
AQS本身提供了一个框架,具体要看子类如何去实现;其框架方法主要都是通过同步器来控制,总的来说同步器中包含一个可表征同步状态的变量,可操作该变量的方法集,以及可阻塞或唤醒其他来修改该状态的线程的方法集。互斥锁,读写锁,信号量,屏障,事件指示器等等都是同步器。
学习AQS主要学习他的思想,进而了解其子类的实现;