(转)Java并发包基石-AQS详解
背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方。
Java并发包基石-AQS详解
Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock、Semaphore,它们的实现都用到了一个共同的基类--AbstractQueuedSynchronizer,简称AQS。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
基本实现原理
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过procted类型的getState,setState,compareAndSetState进行操作
AQS支持两种同步方式:
1.独占式
2.共享式
这样方便使用者实现不同类型的同步组件,独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如ReentrantReadWriteLock。总之,AQS为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。
同步器的设计是基于模板方法模式的,一般的使用方式是这样:
1.使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
2.将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这其实是模板方法模式的一个很经典的应用。
我们来看看AQS定义的这些可重写的方法:
protected boolean tryAcquire(int arg) : 独占式获取同步状态,试着获取,成功返回true,反之为false
protected boolean tryRelease(int arg) :独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;
protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;
protected boolean tryReleaseShared(int arg) :共享式释放同步状态,成功为true,失败为false
protected boolean isHeldExclusively() : 是否在独占模式下被线程占用。
关于AQS的使用,我们来简单总结一下:
如何使用
首先,我们需要去继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;最后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。
设计思想
对于使用者来讲,我们无需关心获取资源失败,线程排队,线程阻塞/唤醒等一系列复杂的实现,这些都在AQS中为我们处理好了。我们只需要负责好自己的那个环节就好,也就是获取/释放共享资源state的姿势T_T。很经典的模板方法设计模式的应用,AQS为我们定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现即可。
AQS的核心思想是基于volatile int state这样的一个属性同时配合Unsafe工具对其原子性的操作来实现对当前锁的状态进行修改。
AQS的源码的主要点在于node节点、共享式 独占式 阻塞队列,cas
源码分析
我们先来简单描述下AQS的基本实现,前面我们提到过,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。(这个内置的同步队列称为"CLH"队列)。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。
其实就是个双端双向链表。
当线程获取资源失败(比如tryAcquire时试图设置state状态失败),会被构造成一个结点加入CLH队列中,同时当前线程会被阻塞在队列中(通过LockSupport.park实现,其实是等待态)。当持有同步状态的线程释放同步状态时,会唤醒后继结点,然后此结点线程继续加入到对同步状态的争夺中。
ps:此处可以参考
(转)分布式系统互斥性与幂等性问题的分析与解决
Node结点
Node结点是AbstractQueuedSynchronizer中的一个静态内部类,我们捡Node的几个重要属性来说一下
static final class Node {
/** waitStatus值,表示线程已被取消(等待超时或者被中断)*/
static final int CANCELLED = 1;
/** waitStatus值,表示后继线程需要被唤醒(unpaking)*/
static final int SIGNAL = -1;
/**waitStatus值,表示结点线程等待在condition上,当被signal后,会从等待队列转移到同步到队列中 */
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/** waitStatus值,表示下一次共享式同步状态会被无条件地传播下去
static final int PROPAGATE = -3;
/** 等待状态,初始为0 */
volatile int waitStatus;
/**当前结点的前驱结点 */
volatile Node prev;
/** 当前结点的后继结点 */
volatile Node next;
/** 与当前结点关联的排队中的线程 */
volatile Thread thread;
/** ...... */
}
ps:static final 和volatile变量
独占式
获取同步状态--acquire()
至此,关于acquire的方法源码已经分析完毕,我们来简单总结下
a.首先tryAcquire获取同步状态,成功则直接返回;否则,进入下一环节;
b.线程获取同步状态失败,就构造一个结点,加入同步队列中,这个过程要保证线程安全;
c.加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。
ps:多个线程都通过【CAS+死循环】这个free-lock黄金搭档来对队列进行修改,每次能够保证只有一个成功,如果失败下次重试,如果是N个线程,那么每个线程最多loop N次,最终都能够成功。
释放同步状态--release()
当前线程执行完自己的逻辑之后,需要释放同步状态,来看看release方法的逻辑
release的同步状态相对简单,需要找到头结点的后继结点进行唤醒,若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程。
共享式
共享式:共享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire返回值为boolean,这很容易理解;对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。
ps:共享式和独占市的区别。共享式tryAcquire返回boolean,tryAcquireShared返回int,分一下集中
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;
2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;
3.当返回值小于0时,表示获取同步状态失败。
获取同步状态--acquireShared
代码逻辑比较容易理解,需要注意的是,共享模式,释放同步状态也是多线程的,此处采用了CAS自旋来保证。
总结
关于AQS的介绍及源码分析到此为止了。
AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。
AQS为我们定义好了顶层的处理实现逻辑,我们在使用AQS构建符合我们需求的同步组件时,只需重写tryAcquire,tryAcquireShared,tryRelease,tryReleaseShared几个方法,来决定同步状态的释放和获取即可,至于背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了,这也是非常典型的模板方法的应用。AQS定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现。
(转)Java并发包基石-AQS详解的更多相关文章
- Java并发包基石-AQS详解
目录 1 基本实现原理 1.1 如何使用 1.2 设计思想 2 自定义同步器 2.1 同步器代码实现 2.2 同步器代码测试 3 源码分析 3.1 Node结点 3.2 独占式 3.3 共享式 4 总 ...
- java对象池commons-pool-1.6详解(一)
自己的项目中用到了 对象池 commons-pool: package com.sankuai.qcs.regulation.protocol.client; import com.dianping. ...
- Java网络编程和NIO详解开篇:Java网络编程基础
Java网络编程和NIO详解开篇:Java网络编程基础 计算机网络编程基础 转自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA 我们是幸运的,因为 ...
- Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO
Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO Java 非阻塞 IO 和异步 IO 转自https://www.javadoop.com/post/nio-and-aio 本系 ...
- java中的io系统详解 - ilibaba的专栏 - 博客频道 - CSDN.NET
java中的io系统详解 - ilibaba的专栏 - 博客频道 - CSDN.NET 亲,“社区之星”已经一周岁了! 社区福利快来领取免费参加MDCC大会机会哦 Tag功能介绍—我们 ...
- Java开发利器Myeclipse全面详解
Java开发利器Myeclipse全面详解: Ctrl+1:修改代码错误 Alt+Shift+S:Source命令 Ctrl+7:单行注释 Ctrl+Shift+/ :多行注释 Ctrl+I :缩进( ...
- Java中的main()方法详解
在Java中,main()方法是Java应用程序的入口方法,也就是说,程序在运行的时候,第一个执行的方法就是main()方法,这个方法和其他的方法有很大的不同,比如方法的名字必须是main,方法必须是 ...
- [译]Java Thread join示例与详解
Java Thread join示例与详解 Java Thread join方法用来暂停当前线程直到join操作上的线程结束.java中有三个重载的join方法: public final void ...
- Java虚拟机之垃圾回收详解一
Java虚拟机之垃圾回收详解一 Java技术和JVM(Java虚拟机) 一.Java技术概述: Java是一门编程语言,是一种计算平台,是SUN公司于1995年首次发布.它是Java程序的技术基础,这 ...
随机推荐
- axios+Vue上传文件显示进度
一,前言 最近在用Vue,然后上传文件时需要显示进度,于是网上搜了一下,经过自己实测终于也弄明白了 二,效果 三,代码 HTML代码 <div id="app"> &l ...
- 阿里巴巴矢量图标库(Iconfont)-利于UI和前端的搭配
前端时间,做一个小网站的时候,需要用到很多小图标,UI设计好之后不知道如何使用,如果使用图片那会很麻烦,相信一些前端更喜欢iconfont这样的标签直接调用,这样包括颜色和大小以及使用都更方便快捷,于 ...
- CSS 三条横线等分
.header_qian { width: 1.5rem; height: 1.5rem; background: rgba(250, 250, 250,0.7); border-radius: 50 ...
- zookeeper安装以及遇到的一些坑
最近项目中用到了storm,然后storm中用到了zookeeper,然后今天抽空整理一下zookeeper的安装使用,原来后期再慢慢学习. 本篇文档,操作部分是摘自其他博客,里边的问题分析是自己在实 ...
- 项目实战工具类(二):ZipUtils(压缩/解压缩文件相关)
import android.content.Context; import android.util.Log; import java.io.File; import java.io.FileInp ...
- SQLServer之数据库行锁
行锁使用注意事项 1.ROWLOCK行级锁确保在用户取得被更新的行,到该行进行更新,这段时间内不被其它用户所修改.因而行级锁即可保证数据的一致性,又能提高数据操作的并发性. 2.ROWLOCK告诉SQ ...
- Anaconda在Windows上安装不上原因
倒腾了一下午安装Anaconda,[所有程序]中只有一个Anaconda的目录,目录中只有一个anaconda prompt,最后发现是因为环境中原先就安装了Java环境,将之前安装的java环境卸载 ...
- L2TP连接尝试失败,因为安全层在初始化与远程计算机的协商时遇到了一个处理错误(转)
L2TP连接尝试失败,因为安全层在初始化与远程计算机的协商时遇到了一个处理错误 错误描述:“ L2TP连接尝试失败,因为安全层在初始化与远程计算机的协商时遇到了一个处理错误” 只有这个没有错误码. ...
- Docker:pipeline编写基本技巧- jenkins配置通过免交互方式拉取git源码管理仓库的代码
工作中,从git仓库拉取代码有2种方式:交互式和非交互式 什么是交互式?就是拉取需要权限才能访问的代码时,需要输入密码 免交互式呢? 是通过密钥,私钥的方式,让服务端信任客户端,产生信任后,任何一次客 ...
- 移动端键盘密码输入框插件(jquery用于支付密码)
最后生成样子: 配置值: * back {function} 回调函数 * msghtml {html} 自定义的html * title {string|object} 标题 * {txt:标题,b ...