Java并发框架——AQS之如何使用AQS构建同步器
AQS的设计思想是通过继承的方式提供一个模板让大家可以很容易根据不同场景实现一个富有个性化的同步器。同步器的核心是要管理一个共享状态,通过对状态的控制即可以实现不同的锁机制。AQS的设计必须考虑把复杂重复且容易出错的队列管理工作统一抽象出来管理,并且要统一控制好流程,而暴露给子类调用的方法主要就是操作共享状态的方法,以此提供对状态的原子性操作。一般子类的同步器中使用AQS提供的getState、setState、compareAndSetState三个方法,前两个为普通的get和set方法,要使用这两个方法必须要保证不存在数据竞争,compareAndSetState方法提供了CAS方式的硬件级别的原子更新。对于独占模式而言,锁获取与释放的流程的定义则交给acquire和release两个方法,它们定义了锁获取与释放的逻辑,同时也是提供给子类获取和释放锁的接口。它的执行逻辑可以参考前面的“锁的获取与释放”,它提供了一个怎样强大的模板?由下面的伪代码可以清晰展示出来,请注意tryAcquire和tryRelease这两个方法,它就是留给子类实现个性化的方法,通过这两个方法对共享状态的管理可以自定义多种多样的同步器,而队列的管理及流程的控制则不是你需要考虑的问题。
① 锁获取模板
if(tryAcquire(arg)) {
创建node
使用CAS方式把node插入到队列尾部
while(true){
if(tryAcquire(arg) 并且 node的前驱节点为头节点){
把当前节点设置为头节点
跳出循环
}else{
使用CAS方式修改node前驱节点的waitStatus标识为signal
if(修改成功)
挂起当前线程
}
}
② 锁释放模板
if(tryRelease(arg)){
唤醒后续节点包含的线程
}
我们可以认为同步器可实现任何不同锁的语义,一般提供给使用者的锁是用AQS框架封装实现的更高层次的实现,提供一种更加形象的API让使用者使用起来更加方便简洁,而不是让使用者直接接触AQS框架,例如,ReentrantLock、Semphore、CountDownLatch等等,这些不同的形象的锁让你使用起来更好理解更加得心应手,而且不容易混淆。然而这些锁都是由AQS实现,AQS同步器面向的是线程和状态的控制,定义了线程获取状态的机制及线程排队等操作,很好地隔离了两者的关注点,高层关注的是场景的使用,而AQS同步器则关心的是并发的控制。假如你要实现一个自定义同步装置,官方推荐的做法是将集成AQS同步器的子类作为同步装置的内部类,而同步装置中相关的操作只需代理成子类中对应的方法即可。往下用一个简单的例子看看如何实现自己的锁,由于同步器被分为两种模式,独占模式和共享模式,所以例子也对应给出。
① 独占模式,独占模式采取的例子是银行服务窗口,假如某个银行网点只有一个服务窗口,那么此银行服务窗口只能同时服务一个人,其他人必须排队等待,所以这种银行窗口同步装置是一个独占模型。第一个类是银行窗口同步装置类,它按照推荐的做法使用一个继承AQS同步器的子类实现,并作为子类出现。第二个类是测试类,形象一点地说,有三位良民到银行去办理业务,分别是tom、jim和jay,我们使用BankServiceWindow就可以约束他们排队,一个一个轮着办理业务而避免陷入混乱的局面。
public class BankServiceWindow {
private final Sync sync;
public BankServiceWindow() {
sync = new Sync();
}
private static class Sync extends AbstractQueuedSynchronizer {
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int releases) {
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
public void handle() {
sync.acquire(1);
}
public void unhandle() {
sync.release(1);
}
}
public class BankServiceWindowTest {
public static void main(String[] args){
final BankServiceWindow bankServiceWindow=new BankServiceWindow();
Thread tom=new Thread(){
public void run(){
bankServiceWindow.handle();
System.out.println("tom开始办理业务");
try {
this.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("tom结束办理业务");
bankServiceWindow.unhandle();
}
};
Thread jim=new Thread(){
public void run(){
bankServiceWindow.handle();
System.out.println("jim开始办理业务");
try {
this.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("jim结束办理业务");
bankServiceWindow.unhandle();
}
};
Thread jay=new Thread(){
public void run(){
bankServiceWindow.handle();
System.out.println("jay开始办理业务");
try {
this.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("jay结束办理业务");
bankServiceWindow.unhandle();
}
};
tom.start();
jim.start();
jay.start();
}
}
输出结果如下:
tom开始办理业务
tom结束办理业务
jim开始办理业务
jim结束办理业务
jay开始办理业务
jay结束办理业务
明显tom、jim、jay仨是排队完成的,但是无法保证三者的顺序,可能是tom、jim、jay,也可能是tom、jay、jim,因为在入列以前的执行先后是无法确定的,它的语义是保证一个接一个办理。如果没有同步器限制的情况,输出结果将不可预测,可能为输出如下:
jim开始办理业务
jay开始办理业务
tom开始办理业务
jay结束办理业务
jim结束办理业务
tom结束办理业务
② 共享模式,共享模式采取的例子同样是银行服务窗口,随着此网点的发展,办理业务的人越来越多,一个服务窗口已经无法满足需求,于是又分配了一位员工开了另外一个服务窗口,这时就可以同时服务两个人了,但两个窗口都有人占用时同样也必须排队等待,这种服务窗口同步器装置就是一个共享型。第一个类是共享模式的同步装置类,跟独占模式不同的是它的状态的初始值可以自由定义,获取与释放就是对状态递减和累加操作。第二个类是测试类,tom、jim和jay再次来到银行,一个有两个窗口甚是高兴,他们可以两个人同时办理了,时间缩减了不少。
public class BankServiceWindows {
private final Sync sync;
public BankServiceWindows(int count) {
sync = new Sync(count);
}
private static class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
public int tryAcquireShared(int interval) {
for (;;) {
int current = getState();
int newCount = current - 1;
if (newCount < 0 || compareAndSetState(current, newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int interval) {
for (;;) {
int current = getState();
int newCount = current + 1;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
}
public void handle() {
sync.acquireShared(1);
}
public void unhandle() {
sync.releaseShared(1);
}
}
public class BankServiceWindowsTest {
public static void main(String[] args){
final BankServiceWindows bankServiceWindows=new BankServiceWindows(2);
Thread tom=new Thread(){
public void run(){
bankServiceWindows.handle();
System.out.println("tom开始办理业务");
try {
this.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("tom结束办理业务");
bankServiceWindows.unhandle();
}
};
Thread jim=new Thread(){
public void run(){
bankServiceWindows.handle();
System.out.println("jim开始办理业务");
try {
this.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("jim结束办理业务");
bankServiceWindows.unhandle();
}
};
Thread jay=new Thread(){
public void run(){
bankServiceWindows.handle();
System.out.println("jay开始办理业务");
try {
this.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("jay结束办理业务");
bankServiceWindows.unhandle();
}
};
tom.start();
jim.start();
jay.start();
}
}
可能的输出结果为:
tom开始办理业务
jay开始办理业务
jay结束办理业务
tom结束办理业务
jim开始办理业务
jim结束办理业务
tom和jay几乎同时开始办理业务,而jay结束后有空闲的服务窗口jim才过来。
这节主要讲的是如何使用AQS构建自己的同步器,并且剖析了锁获取与释放的模板的逻辑,让你更好理解AQS的实现,最后分别给出了独占模式和共享模式的同步器实现的例子,相信你们搞清楚这两种方式的实现后,要构建更加复杂的同步器就知道力往哪里使了。
喜欢研究java的同学可以交个朋友,下面是本人的微信号:
Java并发框架——AQS之如何使用AQS构建同步器的更多相关文章
- Java并发框架AbstractQueuedSynchronizer(AQS)
1.前言 本文介绍一下Java并发框架AQS,这是大神Doug Lea在JDK5的时候设计的一个抽象类,主要用于并发方面,功能强大.在新增的并发包中,很多工具类都能看到这个的影子,比如:CountDo ...
- 深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.概述 1.1.前言 重剑无锋,大巧不工 读j.u.c包下的源码,永远无法绕开的经典 ...
- 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...
- 深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock) 深入 ...
- java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock
原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...
- java并发编程笔记(六)——AQS
java并发编程笔记(六)--AQS 使用了Node实现FIFO(first in first out)队列,可以用于构建锁或者其他同步装置的基础框架 利用了一个int类型表示状态 使用方法是继承 子 ...
- 【死磕Java并发】-----J.U.C之AQS:CLH同步队列
此篇博客全部源代码均来自JDK 1.8 在上篇博客[死磕Java并发]-–J.U.C之AQS:AQS简单介绍中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个F ...
- Java 并发系列之十:java 并发框架(2个)
1. Fork/Join框架 2. Executor框架 3. ThreadPoolExecutor 4. ScheduledThreadPoolExecutor 5. FutureTask 6. t ...
- 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock)
一.前言 优秀的源码就在那里 经过了前面两章的铺垫,终于要切入正题了,本章也是整个AQS的核心之一 从本章开始,我们要精读AQS源码,在欣赏它的同时也要学会质疑它.当然本文不会带着大家逐行过源码(会有 ...
- 【死磕Java并发】—–J.U.C之AQS(一篇就够了)
[隐藏目录] 1 独占式 1.1 独占式同步状态获取 1.2 独占式获取响应中断 1.3 独占式超时获取 1.4 独占式同步状态释放 2 共享式 2.1 共享式同步状态获取 2.2 共享式同步状态释放 ...
随机推荐
- 前端性能监控系统 & 前端数据分析系统
前端监控系统 目前已经上线,欢迎使用! 背景:应工作要求,需要整理出前端项目的报错信息,尝试过很多统计工具,如: 腾讯bugly.听云.OneApm.还有一个忘记名字的工具. 因为各种原因,如: 统计 ...
- [SDOI 2016]征途
Description 题库链接 将一个长度为 \(n\) 的正整数序列分为 \(m\) 段,问你这 \(m\) 段最小的方差 \(v\) 为多少.输出 \(v\times m^2\) . \(1\l ...
- [Vijos 2024]无向图最短路径
Description 无向图最短路径问题,是图论中最经典也是最基础的问题之一.本题我们考虑一个有 $n$ 个结点的无向图 $G$.$G$ 是简单完全图,也就是说 $G$ 中没有自环,也没有重边,但任 ...
- [HNOI2012]与非
题目描述 NAND(与非)是一种二元逻辑运算,其运算结果为真当且仅当两个输入的布尔值不全为真.NAND运算的真值表如下(1表示真,0表示假): 两个非负整数的NAND是指将它们表示成二进制数,再在对应 ...
- Go学习——go+channel实战(转)
转载:http://studygolang.com/articles/2423 背景 在最近开发的项目中,后端需要编写许多提供HTTP接口的API,另外技术选型相对宽松,因此选择Golang + Be ...
- 51 nod 1188 最大公约数之和 V2
1188 最大公约数之和 V2 题目来源: UVA 基准时间限制:2 秒 空间限制:262144 KB 分值: 160 难度:6级算法题 给出一个数N,输出小于等于N的所有数,两两之间的最大公约数 ...
- [UOJ UNR#2 黎明前的巧克力]
来自FallDream的博客,未经允许,请勿转载,谢谢. 传送门 很奇妙的一道题 首先不难发现一个暴力做法,就是f[i]表示异或和为i的答案数,每次FWT上一个F数组,其中F[0]=1,F[ai]=2 ...
- 伸展树Splay【非指针版】
·伸展树有以下基本操作(基于一道强大模板题:codevs维护队列): a[]读入的数组;id[]表示当前数组中的元素在树中节点的临时标号;fa[]当前节点的父节点的编号;c[][]类似于Trie,就是 ...
- [Codeforces]871D Paths
失踪OJ回归. 毕竟这样的数论没做过几道,碰上一些具体的应用还是无所适从啊.小C还是借助这题大致摸索一下莫比乌斯函数吧. Description 有n个点,标号为1~n,为这n个点建一张无向图.两个点 ...
- VB.NET 泛型类型的应用经验
VB.NET编程语言中的数据类型种类繁多,初学者要想全部掌握这些类型的应用是一个比较困难的步骤.今天我们先让大家了解一下VB.NET泛型类型这一高阶技术的应用,以便让大家对这一语言进行深入的解读. 定 ...