Java同步框架AQS原文分析

0、引言

自J2SE1.5开始,java中的同步类(Lock,Semphore等等)都基于AbstractQueuedSynchronizer(后文简称AQS)。AQS提供了一种原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。

本文主要是分析此框架的实现者Doug Lea写的一篇介绍AQS的论文(→猛戳这里拿原文←),并没有完全翻译原文,所以想看原文的在上面拿原文。

1、基本功能

同步器至少要有以下两种类型的方法acquire和release

  • acquire:至少要有一个操作能实现对调用线程的阻塞,直到同步器允许它进行操作。
  • release:至少要有一个操作能用一种方式解锁一个或者更多个已经阻塞的线程改变同步状态。

同时,同步器还需要支持以下几种功能:

  • 非阻塞式的同步过程尝试(tryLock)
  • 可选的超时机制,可以允许程序放弃等待
  • 可以通过中断执行取消

而为了适应不同的同步器,同步器要支持两种模式

  • 独占式exclusive。要保证一次只有一个线程可以经过阻塞点
  • 共享式shared。可以允许多个线程阻塞点

2、性能要求

AQS对性能改进的关注点不是主要在于减小空间的开销和时间的开销。
原因是:对于开发者来说,只在需要的时候构建同步器,实在没有必要为了这部分空间的消耗去压缩空间。与此同时,同步器大部分情况是用在多线程的情况下,产生一些竞争也是可以想象到的。

AQS的性能重点在于可扩展性

AQS的几个改进目标

  1. 可预见性的保证同步器的效率,甚至在其发生竞争的情形下。
  2. 减少那些已经被允许通过阻塞点的线程但是没有通过的消耗时间。

对比自旋锁来说,自旋锁的响应速度很快,但是在线程竞争特别激烈的情况下,由于大量的内存读取,会降低其响应的速度。

AQS的框架必须能够提供一些监视和检查的基本操作,以便用户发现和缓解瓶颈。例如:提供一个方式,决定多少个线程会被阻塞。

3、设计与实现

acquire和release操作的伪代码可以很容易的写出来如下:

acquire{
    while (同步状态不允许获取) {
        若当前线程没有入队,那么就将其入队;
        可能会阻塞当前线程;
    }
    如果它已经入队了,就将其出队
}

release {
    更新同步状态
    if(状态表示允许一个阻塞的线程去acquire)
       释放一个或多个队列中的线程
}

3.1、同步状态State

AQS采用了int(4个字节)的变量来持有同步状态。使用getStatesetStatecompareAndSetState方法来进行状态的获取和更新。

以上的方法,它们依赖于volatile这个机制来进行读写,compare-and-swap机制去实现compareAndSetState方法。(CAS操作如果不清楚的可以自行搜索相关的内容)

AQS是一个抽象类,它的tryAcquire和tryRelease方法都需要子类去实现。两个方法都支持传入一个int类型的参数。这个参数主要用来实现不同子类功能的。【例如】:reentrant lock,当在返回一个条件等待后重新去获取锁权限是,它会重新建立一个递归计数。

3.2、阻塞

AQS没有采用Thread.suspendThread.resume这两种方式,以上两种方式都有严重的安全问题,容易造成死锁等。

AQS采用了java.util.concurrent.locks包下的LockSupport类。该类可以响应中断操作,可以设置超时时间等。此机制与Win32内的“消费事件”机制,Linux NPTL线程库的方式类似。

3.3、核心队列

AQS框架的核心是阻塞线程的队列。也就是一个FIFO(先进先出)队列。AQS不支持基于优先级的同步器。

AQS的锁策略采用的CLH而不是MCS,原因是CLH要比MCS更适合处理取消和超时。

CLH队列的入队和出队操作是与它的锁操作息息相关。它有两个原子操作更新域,head和taiil。初始化时,将指向一个虚假的节点。


队列格式.png

每个节点的release状态都保存在它的前驱节点内,while (pred.status != RELEASED);后就可以开始自旋。若持有前驱节点的域,CLH锁可以处理超时和其他形式的取消操作。

AQS对CLH机制有两点修改

3.3.1、修改点1:增加后继节点访问域

AQS增加了节点node访问其后继节点的next域。由于AQS队列是双向队列,所以CAS操作也没有很好的方式对两个方向都做到完全的原子性更新。后继结点的更新就采用了
pred.next = node; 这种方式。
当后继结点可能出现退出的情形,AQS会通过pred域往前遍历确定是否是真的退出了。

3.3.2、修改点2:采用状态域控制阻塞

CLH采用自旋进行线程的阻塞,AQS没有采用这种方式,而采用之前介绍过的State字段进行阻塞。

AQS需要控制在头节点调用tryAcquire方法适合才允许通过,其他情况acquire和block都会失败。每次只需检查当前节点的前驱节点是不是head,这一点减少了CLH对内存的读取竞争,同时还能避免不必要的阻塞和唤醒操作。

【原因】:在调用park方法前,线程会设置一个“SIGNAL”信号,然后重新检查同步状态,再确定是否需要再次调用park方法。

3.3.3、修改点3:利用垃圾回收机制进行节点存储管理

AQS主要使用在出队的时候置null方式回收节点内存,这可以有效的避免复杂的处理和瓶颈。


这里我们可以给出更加具体的acquire方法

acquire {
    if (!tryAcquire(arg)) {
        node = 创建队列并且新入队节点;
        pred = 节点的有效前驱节点;
        while (pred 不是头节点 || !tryAcquire(arg)) {
            if (pred的状态位是Signal信号)
                park();
            else
                CAS操作设置pred的Signal信号;
            pred = node节点的有效前驱节点;
        }
        head = node;
    }
}

而release方法也可以得到如下:

release {
    if (tryRelease(arg) && 头节点的状态是Signal) {
        将头节点的状态设置为不是Signal;
        如果头节点的后继结点存在,则将其唤醒。
    }
}

【时间复杂度】acquire的循环次数由tryAcquire方法的性质决定。在不考虑线程等消耗,以及取消操作的情况下,它的时间复杂度是O(1)。在取消操作的情况下,确认前驱和后继结点后重置同步状态需要O(n)次遍历(n为队列的长度)。

3.4、条件队列

AQS中的ConditionObject提供一个能让同步器使用的类。它既符合Lock接口,又能持有互斥的同步机制。

ConditionObject类提供了类似awaitsignalsignalAll操作的API。这些方法的作用与Object.wait方法是一样的。ConditionObject使用与同步器同样的内部队列,不过与同步器存储在分开的条件队列中。

3.4.1、基本操作流程

基本的await操作如下

  1. 创建并添加新的节点到条件队列中;
  2. 释放锁;
  3. 阻塞直到节点在锁队列中
  4. 重新获取锁。

基本的signal操作如下

  1. 传递条件队列的第一个节点到锁队列中

3.4.2、取消流程

上述基本操作的实现并不困难,而处理取消,超时和线程的中断是实现的难点。

  • 中断发生在await操作之前,此方法一定要抛出一个InterruptedException
  • 中断发生在await操作之后,此方法不抛出异常,而是系统的中断状态集

条件队列需要一个状态位,当出现Signal信号失败,就将信号传递到队列的下一个节点内。而如果出现Cancel信号失败,就取消传递操作,唤醒锁的重新获取操作。

4、用法

4.1、控制公平性

AQS并不保证同步器一定是公平的。tryAcquire方法是在入队操作前的一个检验,因此完全可以在入队前,“偷取”获取的权限。

  • 非公平的FIFO策略(获取到锁的顺序不一定是队列中的顺序),将tryAcquire方法中每次进入都会进行竞争,无论当前线程是否是队列的头节点。只要进入的线程速度更快,那么队列中的节点即使解除了阻塞,依然会重新阻塞回去。

  • 公平的FIFO策略,只需要将tryAcquire方法在当前线程不是队列的头节点时放回失败就行。次之的方式,只需要判断队列是否为空,空队列就可以放回tryAcquire成功。

同步器的公平性设置主要是在多处理器情况下,才能发挥出其水平。多处理器往往会有更多的竞争,也就更有可能发生一个线程发现锁现在被其他线程需要的情形。

4.2、同步器

  • ReentrantLock
  • ReentrantReadWriteLock
  • Semaphore
  • CountDownLatch
  • FutureTask(1.5以后不再使用AQS)
  • SynchronousQueue

自我总结

这里开始就不是论文中的内容了。AQS的基本理解就在上述的文章中,后面我们再深入到AQS的源码中看它具体的实现方式。

AQS的核心内容就在于它处理阻塞和非阻塞的方式,如何用队列实现不同功能的同步器,如何控制同步器的性能。

如果你对上述问题还不了解,可以再深入看看原文。

参考文章

  1. Doug Lea. The java.util.concurrent Synchronizer Framework.

如有翻译不周到的地方,欢迎批评指正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容