AQS是什么?

AQS介绍
AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。来看下同步组件对AQS的使用:

 

AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。

抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

它维护了一个volatile int state(代表共享资源)和一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列)

AQS原理简介

 

AQS的实现依赖内部的同步队列(FIFO双向队列),如果当前线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。

上面说的有点抽象,来具体看下,首先来看AQS最主要的三个成员变量:

    private transient volatile Node head;private transient volatile Node tail; private volatile int state;

上面提到的同步状态就是这个int型的变量state. head和tail分别是同步队列的头结点和尾结点。假设state=0表示同步状态可用(如果用于锁,则表示锁可用),state=1表示同步状态已被占用(锁被占用)。

下面举例说下获取和释放同步状态的过程:

获取同步状态

假设线程A要获取同步状态(这里想象成锁,方便理解),初始状态下state=0,所以线程A可以顺利获取锁,A获取锁后将state置为1。在A没有释放锁期间,线程B也来获取锁,此时因为state=1,表示锁被占用,所以将B的线程信息和等待状态等信息构成出一个Node节点对象,放入同步队列,head和tail分别指向队列的头部和尾部(此时队列中有一个空的Node节点作为头点,head指向这个空节点,空Node的后继节点是B对应的Node节点,tail指向它),同时阻塞线程B(这里的阻塞使用的是LockSupport.park()方法)。后续如果再有线程要获取锁,都会加入队列尾部并阻塞。

释放同步状态

当线程A释放锁时,即将state置为0,此时A会唤醒头节点的后继节点(所谓唤醒,其实是调用LockSupport.unpark(B)方法),即B线程从LockSupport.park()方法返回,此时B发现state已经为0,所以B线程可以顺利获取锁,B获取锁后B的Node节点随之出队。

上面只是简单介绍了AQS获取和释放的大致过程,下面结合AQS和ReentrantLock源码来具体看下JDK是如何实现的,特别要注意JDK是如何保证同步和并发操作的。

AQS源码分析

接下来以ReentrantLock的源码入手来深入理解下AQS的实现。
上面说过AQS一般是以继承的方式被使用,同步组件内部组合一个继承了AQS的子类。
在ReentrantLock类中,有一个Sync成员变量,即是继承了AQS的子类,源码如下:

 public class ReentrantLock implements Lock, java.io.Serializable { private static final long serialVersionUID = 7373984872572414699L; /** Synchronizer providing all implementation mechanics */ private final Sync sync; /** * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */ abstract static class Sync extends AbstractQueuedSynchronizer { ... } }

这里的Sync也是一个抽象类,其实现类为FairSync和NonfairSync,分别对应公平锁和非公平锁。ReentrantLock的提供一个入参为boolean值的构造方法,来确定使用公平锁还是非公平锁:

     public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }

获取锁

这里以NonfairSync类为例,看下它的Lock()的实现:

     final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }

lock方法先通过CAS尝试将同步状态(AQS的state属性)从0修改为1。若直接修改成功了,则将占用锁的线程设置为当前线程。看下compareAndSetState()和setExclusiveOwnerThread()实现:

     protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }

可以看到compareAndSetState底层其实是调用的unsafe的CAS系列方法。

     protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread; }

exclusiveOwnerThread属性是AQS从父类AbstractOwnableSynchronizer中继承的属性,用来保存当前占用同步状态的线程。

如果CAS操作未能成功,说明state已经不为0,此时继续acquire(1)操作,这个acquire()由AQS实现提供:

    public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

代码很短,不太好了理解,转换下写法(代码1):

    public final void acquire(int arg) { boolean hasAcquired = tryAcquire(arg); if (!hasAcquired) { Node currentThreadNode = addWaiter(Node.EXCLUSIVE); boolean interrupted = acquireQueued(currentThreadNode, arg); if (interrupted) { selfInterrupt(); } } }

简单解释下:
tryAcquire方法尝试获取锁,如果成功就返回,如果不成功,则把当前线程和等待状态信息构适成一个Node节点,并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。

首先看tryAcquire(arg)在NonfairSync中的实现(这里arg=1):

        protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

首先获取AQS的同步状态(state),在锁中就是锁的状态,如果状态为0,则尝试设置状态为arg(这里为1), 若设置成功则表示当前线程获取锁,返回true。这个操作外部方法lock()就做过一次,这里再做只是为了再尝试一次,尽量以最简单的方式获取锁。

如果状态不为0,再判断当前线程是否是锁的owner(即当前线程在之前已经获取锁,这里又来获取),如果是owner, 则尝试将状态值增加acquires,如果这个状态值越界,抛出异常;如果没有越界,则设置后返回true。这里可以看非公平锁的涵义,即获取锁并不会严格根据争用锁的先后顺序决定。这里的实现逻辑类似synchroized关键字的偏向锁的做法,即可重入而不用进一步进行锁的竞争,也解释了ReentrantLock中Reentrant的意义。

如果状态不为0,且当前线程不是owner,则返回false。
回到上面的代码1,tryAcquire返回false,接着执行addWaiter(Node.EXCLUSIVE),这个方法创建结点并入队,来看下源码:

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } 

首先创建一个Node对象,Node中包含了当前线程和Node模式(这时是排他模式)。tail是AQS的中表示同步队列队尾的属性,刚开始为null,所以进行enq(node)方法,从字面可以看出这是一个入队操作,来看下具体入队细节:

private Node enq(final Node node) {for (;;) {Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } 

方法体是一个死循环,本身没有锁,可以多个线程并发访问,假如某个线程进入方法,此时head, tail都为null, 进入if(t==null)区域,从方法名可以看出这里是用CAS的方式创建一个空的Node作为头结点,因为此时队列中只一个头结点,所以tail也指向它,第一次循环执行结束。注意这里使用CAS是防止多个线程并发执行到这儿时,只有一个线程能够执行成功,防止创建多个同步队列。

进行第二次循环时(或者是其他线程enq时),tail不为null,进入else区域。将当前线程的Node结点(简称CNode)的prev指向tail,然后使用CAS将tail指向CNode。看下这里的实现:

private final boolean compareAndSetTail(Node expect, Node update) {return unsafe.compareAndSwapObject(this, tailOffset, expect, update);}

expect为t, t此时指向tail,所以可以CAS成功,将tail重新指向CNode。此时t为更新前的tail的值,即指向空的头结点,t.next=node,就将头结点的后续结点指向CNode,返回头结点。经过上面的操作,头结点和CNode的关系如图:

 

其他线程再插入节点以此类推,都是在追加到链表尾部,并且通过CAS操作保证线程安全。

通过上面分析可知,AQS的写入是一种双向链表的插入操作,至此addWaiter分析完毕。

addWaiter返回了插入的节点,作为acquireQueued方法的入参,看下源码:

    final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

可以看到,acquireQueued方法也是一个死循环,直到进入 if (p == head && tryAcquire(arg))条件方法块。还是接着刚才的操作来分析。acquireQueued接收的参数是addWaiter方法的返回值,也就是刚才的CNode节点,arg=1。node.predecessor()返回CNode的前置节点,在这里也就是head节点,所以p==head成立,进而进行tryAcquire操作,即争用锁, 如果获取成功,则进入if方法体,看下接下来的操作:

1) 将CNode设置为头节点。
2) 将CNode的前置节点设置的next设置为null。

此时队列如图:

 

上面操作即完成了FIFO的出队操作。
从上面的分析可以看出,只有队列的第二个节点可以有机会争用锁,如果成功获取锁,则此节点晋升为头节点。对于第三个及以后的节点,if (p == head)条件不成立,首先进行shouldParkAfterFailedAcquire(p, node)操作(争用锁失败的第二个节点也如此), 来看下源码:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,是说明此节点已经将状态设置如果锁释放,则应当通知它,所以它可以安全的阻塞了,返回true。

如果前节点的状态大于0,即为CANCELLED状态时,则会从前节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。

前节点状态小于0的情况是对应ReentrantLock的Condition条件等待的,这里不进行展开。

如果shouldParkAfterFailedAcquire返回了true,则会执行:“parkAndCheckInterrupt()”方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。

释放锁

通过ReentrantLock的unlock方法来看下AQS的锁释放过程。来看下源码:

    public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 

unlock调用AQS的release()来完成, AQS的如果tryRelease方法由具体子类实现。tryRelease返回true,则会将head传入到unparkSuccessor(Node)方法中并返回true,否则返回false。首先来看看Sync中tryRelease(int)方法实现,如下所示:

protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free }

这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。
在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。

在方法unparkSuccessor(Node)中,就意味着真正要释放锁了,它传入的是head节点(head节点是占用锁的节点),看下源码:

    private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁。

 

转载于:https://www.cnblogs.com/fanBlog/p/9336126.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/426923.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实例19:python

#题目&#xff1a;一个数如果恰好等于它的因子之和&#xff0c;这个数就称为"完数"。 #例如61&#xff0b;2&#xff0b;3.编程找出1000以内的所有完数。 #!/usr/bin/python3 list2 [] for x in range(1, 1001): list1 [] for i in range(1, int(x / 2) 1): if x…

python将excel日期比大小_sql与excel、python比较(二)——日期和时间函数

1、CURDATE()或CURRENT_DATE()&#xff1a;返回当前的日期select curdate(),current_date();excel&#xff1a;TODAYpython&#xff1a;datetime和time库2、CURTIME()或CURRENT_TIME()&#xff1a;返回当前的时间select curtime(),current_time();excel&#xff1a;NOWpython&a…

c语言实验报告熟悉vc,C语言实验报告源代码

C语言实验报告源代码1.问题描述&#xff1a;计算三个整数12*34*56的积&#xff0c;得出正确的结果&#xff0c;并能通过此实验熟悉vc6.0的操作环境&#xff1b;源程序及注释&#xff1a;# include void main() /*求三个数的积*/{ int a,b,c,sum; /*这是声明部分&#xff0c;定义…

solr的一些查询语法

以下内容来自solr中国 1.1. 首先假设我的数据里fields有:name, tel, address 预设的搜寻是name这个字段, 如果要搜寻的数据刚好就是 name 这个字段,就不需要指定搜寻字段名称. 1.2. 查询规则: 如欲查询特定字段(非预设字段),请在查询词前加上该字段名称加 “:” (不包含”号) 符…

实例20:python

#题目&#xff1a;一球从100米高度自由落下&#xff0c;每次落地后反跳回原高度的一半&#xff1b;再落下 #&#xff0c;求它在第10次落地时&#xff0c;共经过多少米&#xff1f;第10次反弹多高&#xff1f; #!/usr/bin/python -- coding: UTF-8 -- tour [] height [] h…

圆弧齿轮啮合原理_图解八种齿轮的加工原理

齿形有多种形式&#xff0c;其中以渐开线齿形最为常见。渐开线齿形常用的加工方法有两大类&#xff0c;即成形法和展成法。1铣齿采用盘形模数铣刀或指状铣刀铣齿属于成形法加工&#xff0c;铣刀刀齿截面形状与齿轮齿间形状相对应。2成形磨齿也属于成形法加工&#xff0c;成形砂…

组个最小数C语言pta,PTA|C语言:组个最小数

7-58 组个最小数 (20 分)给定数字0-9各若干个。你可以以任意顺序排列这些数字&#xff0c;但必须全部使用。目标是使得最后得到的数尽可能小(注意0不能做首位)。例如&#xff1a;给定两个0&#xff0c;两个1&#xff0c;三个5&#xff0c;一个8&#xff0c;我们得到的最小的数就…

python中[-1]、[-1]、[-1]、[n-1]使用方法

import numpy as np anp.random.rand(5) print(a) [ 0.64061262 0.8451399 0.965673 0.89256687 0.48518743] print(a[-1]) ###取最后一个元素 [0.48518743] print(a[:-1]) ### 除了最后一个取全部 [ 0.64061262 0.8451399 0.965673 0.89256687] print(a[::-1]) ### 取从后向…

项目收集

配置中心 apollo &#xff1a; https://github.com/ctripcorp/apollo 参考资料 apollo 系列 &#xff1a; https://www.jianshu.com/nb/26825662 下载工具 proxyee-down https://github.com/proxyee-down-org/proxyee-down 书籍 books-collection 给程序员的开源、免费图书集合…

vue 获取请求url_vue 获取url里参数的两种方法小结

我就废话不多说了&#xff0c;大家还是直接看代码吧~第一种&#xff1a;const query Qs.parse(location.search.substring(1))let passport query.passport;第二种&#xff1a;var querythis.$route.query;let lat query.lat;补充知识&#xff1a;Vue通过query获取路由参数现…

单片机c语言1602怎么接线,lcd1602中文资料分享:lcd1602接线图_lcd1602与单片机连接图 - 全文...

lcd1602液晶屏在很懂工业产品上都有应用&#xff0c;LCD1602能够能够同时显示32个字符&#xff0c;价格便宜&#xff0c;编程简单而且稳定可靠。lcd1602液晶屏是一种图形点阵显示器&#xff0c;显示原理简单易懂&#xff0c;都是液晶屏内部的液晶材料变化而显示不同的字符&…

李代数笔记

下载&#xff1a;李代数笔记(20180906).pdf 犹记我曾经这篇博文中提到过Humphreys李代数是李代数的万恶之源。 这个笔记的证明都相对自然很多&#xff0c;不过不意味着这个笔记适合新手&#xff0c;因为这仅仅是用来复习的笔记&#xff0c;精简出最重要的内容而已&#xff0c;当…

access日期如何增加年数_如何为Access数据库表添加日期或时间戳

为了应用方便&#xff0c;您可能需要给数据库的每条记录都添加日期/时间戳&#xff0c;以便确定各个记录添加到数据库的时间。在Access数据库应用中&#xff0c;使用Now()函数能够轻松完成这个任务。本文将一步一步为您介绍整个添加过程&#xff0c;非常简单。本文所使用的Acce…

单片机 c语言 定义i o,【51单片机】普通I/O口模拟SPI口C语言程序

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼89C51系列单片机都不带SPI口&#xff0c;所在在这种情况下&#xff0c;我们可以模拟SPI口来现实我们要的功能&#xff0c;程序如下&#xff1a;//-----------------------函数声明&#xff0c;变量定义&#xff0d;&#xff0d;&am…

Python format 格式化函数

Python2.6 开始&#xff0c;新增了一种格式化字符串的函数 str.format()&#xff0c;它增强了字符串格式化的功能。 基本语法是通过 {} 和 : 来代替以前的 % 。 format 函数可以接受不限个参数&#xff0c;位置可以不按顺序。 实例 “{} {}”.format(“hello”, “world”) # 不…

潭州课堂25班:Ph201805201 爬虫基础 第九课 图像处理- PIL (课堂笔记)

Python图像处理-Pillow 简介 Python传统的图像处理库PIL(Python Imaging Library )&#xff0c;可以说基本上是Python处理图像的标准库&#xff0c;功能强大&#xff0c;使用简单。 但是由于PIL不支持Python3&#xff0c;而且更新缓慢。所以有志愿者在PIL的基础上创建了一个分支…

linux 安装qq,如何安装linux版本QQ?

layout: pagetitle: linuxQQ发布description: linuxQQ安装tags:Linuxqqcategories: linux前文时光匆匆&#xff0c;随着时间流逝&#xff0c;已经来到了2019年&#xff0c;在这一年的1024程序员节这天&#xff0c;腾讯终于又发布了linux版的qq&#xff0c;说起来也是喜大普奔&a…

Python中range()函数的用法

1、函数原型&#xff1a;range&#xff08;start&#xff0c; end&#xff0c; scan)&#xff1a; 参数含义&#xff1a; start:计数从start开始。默认是从0开始。例如range&#xff08;5&#xff09;等价于range&#xff08;0&#xff0c; 5&#xff09;; end:技术到end结束&a…

[SimplePlayer] 1. 从视频文件中提取图像

在开始之前&#xff0c;我们需要了解视频文件的格式。视频文件的格式众多&#xff0c;无法三言两语就能详细分析其结构&#xff0c;尽管如此&#xff0c;ffmpeg却很好地提取了各类视频文件的共同特性&#xff0c;并对其进行了抽象描述。 视频文件格式&#xff0c;统称为contain…

android glide 版本,Android Studio 第六十七期 - Android Glide3.7.0和3.8.0用法

一、前言&#xff1a;再优秀的开源库都有坑要填手上的项目使用的图片加载框架是&#xff1a;Universal-Image-Loader业务需要定制化的一些代码。Universal-Image-Loader 这个框架是一个非常经典好用的框架&#xff0c;唯一的问题是是作者很久之前就不再更新了。所以综合考虑下&…