1. AQS简单介绍

AQS是Java并发类库的基础。其提供了一个基于FIFO队列,可以用于构建锁或者其它相关同步装置的基础框架。该同步器(下面简称同步器)利用了一个int来表示状态,期望它可以成为实现大部分同步需求的基础。使用的方法是继承。子类通过继承同步器并须要实现它的方法来管理其状态,管理的方式就是通过类似acquire和release的方式来操纵状态。

然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,须要使用这个同步器提供的下面三个方法对状态进行操作:

  • java.util.concurrent.locks.AbstractQueuedSynchronizer.getState()
  • java.util.concurrent.locks.AbstractQueuedSynchronizer.setState(int)
  • java.util.concurrent.locks.AbstractQueuedSynchronizer.compareAndSetState(int, int)

子类推荐被定义为自己定义同步装置的内部类。同步器自身没有实现不论什么同步接口。它不过定义了若干acquire之类的方法来供使用。

该同步器即能够作为排他模式也能够作为共享模式。当它被定义为一个排他模式时,其它线程对其的获取就被阻止,而共享模式对于多个线程获取都能够成功。

同步器是实现锁的关键。利用同步器将锁的语义实现,然后在锁的实现中聚合同步器。能够这样理解:锁的API是面向使用者的,它定义了与锁交互的公共行为,而每一个锁须要完毕特定的操作也是透过这些行为来完毕的(比方:能够同意两个线程进行加锁,排除两个以上的线程),可是实现是依托给同步器来完毕;同步器面向的是线程訪问和资源控制,它定义了线程对资源能否够获取以及线程的排队等操作。锁和同步器非常好的隔离了二者所须要关注的领域。严格意义上讲,同步器能够适用于除了锁以外的其它同步设施上(包含锁)。

2. CLH算法

锁的实现是以CLH算法为基础。

以下简介一下CLH算法:

CLH算法构建了隐式的链表,是一种非堵塞算法的实现。CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程须要获取锁,且不释放锁,为false表示线程释放了锁。

结点之间是通过隐形的链表相连,之所以叫隐形的链表是由于这些结点之间没有明显的next指针。而是通过myPred所指向的结点的变化情况来影响myNode的行为。

CLHLock上另一个尾指针,始终指向队列的最后一个结点。CLHLock的类图例如以下所看到的:



当一个线程须要获取锁时。会创建一个新的QNode。将当中的locked设置为true表示须要获取锁,然后线程对tail域调用getAndSet方法。使自己成为队列的尾部,同一时候获取一个指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转。直到前趋结点释放锁。

当一个线程须要释放锁时。将当前结点的locked域设置为false。同一时候回收前趋结点。例如以下图所看到的,线程A须要获取锁,其myNode域为true。些时tail指向线程A的结点。然后线程B也增加到线程A后面,tail指向线程B的结点。

然后线程A和B都在它的myPred域上旋转,一量它的myPred结点的locked字段变为false。它就能够获取锁扫行。明显线程A的myPred
locked域为false,此时线程A获取到了锁。


整个CLH的代码例如以下。当中用到了ThreadLocal类,将QNode绑定到每个线程上。同一时候用到了AtomicReference,对尾指针的改动正是调用它的getAndSet()操作来实现的,它可以保证以原子方式更新对象引用。
CLH算法的示意代码例如以下:
[java] view
plain
copy

  1. import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
  2. public class CLHLock {
  3. public static class CLHNode {
  4. private boolean isLocked = true; // 默认是在等待锁
  5. }
  6. @SuppressWarnings("unused" )
  7. private volatile CLHNode tail ;
  8. private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
  9. . newUpdater(CLHLock.class, CLHNode .class , "tail" );
  10. public void lock(CLHNode currentThread) {
  11. CLHNode preNode = UPDATER.getAndSet( this, currentThread);
  12. if(preNode != null) {//已有线程占用了锁,进入自旋
  13. while(preNode.isLocked ) {
  14. }
  15. }
  16. }
  17. public void unlock(CLHNode currentThread) {
  18. // 假设队列里仅仅有当前线程,则释放对当前线程的引用(for GC)。
  19. if (!UPDATER .compareAndSet(this, currentThread, null)) {
  20. // 还有兴许线程
  21. currentThread. isLocked = false ;// 改变状态,让兴许线程结束自旋
  22. }
  23. }
  24. }

至于AQS的实现,和CLH略有不同,同步器的開始提到了事实上现依赖于一个FIFO队列。那么队列中的元素Node就是保存着线程引用和线程状态的容器,每一个线程对同步器的訪问。都能够看做是队列中的一个节点。Node的主要包括下面成员变量:

[java] view
plain
copy

  1. Node {
  2. int waitStatus;
  3. Node prev;
  4. Node next;
  5. Node nextWaiter;
  6. Thread thread;
  7. }

成员变量主要负责保存该节点的线程引用,同步等待队列(下面简称sync队列)的前驱和后继节点。同一时候也包括了同步状态。

节点成为sync队列和condition队列构建的基础,在同步器中就包括了sync队列。同步器拥有三个成员变量:sync队列的头结点head、sync队列的尾节点tail和状态state。

对于锁的获取,请求形成节点,将其挂载在尾部。而锁资源的转移(释放再获取)是从头部開始向后进行。对于同步器维护的状态state,多个线程对其的获取将会产生一个链式的结构。


3.AQS实现分析

3.1 概述

同步器的设计包括获取和释放两个操作:

获取操作步骤例如以下:
if(尝试获取成功){
return;
}else{
增加等待队列;park自己
}

释放操作:

if(尝试释放成功){
unpark等待队列中第一个节点
}else{
return false
}

要满足以上两个操作。须要下面3点来支持:

1、原子操作同步状态;

2、堵塞或者唤醒一个线程;

3、内部应该维护一个队列。

AQS的实现採用了模板设计模式,在AbstractQueuedSynchronizer类中,定义了

[java] view
plain
copy

  1. protected boolean tryAcquire(int arg);
  2. protected int tryAcquireShared(int arg);
  3. protected boolean tryRelease(int arg);
  4. protected boolean tryReleaseShared(int arg)。

等未详细实现的方法。子类须要实现这些方法。来完毕不同的同步器实现。

3.2 获取、释放锁操作

3.2.1 获取操作

获取锁操作的代码例如以下:

[java] view
plain
copy

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) &&
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  4. selfInterrupt(); //假设获取锁的过程中有中断,则在获取操作完毕后,响应中断。
  5. }

上述逻辑主要包含:

1. 尝试获取(调用tryAcquire更改状态,须要保证原子性);

在tryAcquire方法中使用了同步器提供的对state操作的方法,利用compareAndSet保证仅仅有一个线程可以对状态进行成功改动,而没有成功改动的线程将进入sync队列排队(通过调用addWaiter方法)

addWaiter方法例如以下:

[java] view
plain
copy

  1. private Node addWaiter(Node mode) {
  2. Node node = new Node(Thread.currentThread(), mode);
  3. // Try the fast path of enq; backup to full enq on failure 首先在尾部高速加入。失败后再调用enq方法
  4. Node pred = tail;
  5. if (pred != null) {
  6. node.prev = pred;
  7. if (compareAndSetTail(pred, node)) {  //通过CAS操作,来进行入队操作
  8. pred.next = node;
  9. return node;
  10. }
  11. }
  12. enq(node);
  13. return node;
  14. }

2. 假设获取不到,将当前线程构造成节点Node并增加sync队列;

进入队列的每一个线程都是一个节点Node,从而形成了一个双向队列。类似CLH队列,这样做的目的是线程间的通信会被限制在较小规模(也就是两个节点左右)。

3. 再次尝试获取(调用acquireQueued方法),假设没有获取到那么将当前线程从线程调度器上摘下。进入等待状态。

acquireQueued代码例如以下:

[java] view
plain
copy

  1. final boolean acquireQueued(final Node node, int arg) {
  2. boolean failed = true;
  3. try {
  4. boolean interrupted = false;
  5. for (;;) {
  6. final Node p = node.predecessor();
  7. if (p == head && tryAcquire(arg)) { //假设为头结点。且获取锁成功,则退出, Note:head事实上保存的是已经获取锁的节点。是哑节点
  8. setHead(node);
  9. p.next = null; // help GC
  10. failed = false;
  11. return interrupted;
  12. }
  13. if (shouldParkAfterFailedAcquire(p, node) &&
  14. parkAndCheckInterrupt()) //<span style="font-family: Arial;">parkAndCheckInterrupt的实现会调用park方法。使当前线程进入等待状态</span>
  15. interrupted = true;
  16. }
  17. } finally {
  18. if (failed)
  19. cancelAcquire(node);
  20. }
  21. }

上述逻辑主要包含:

1. 获取当前节点的前驱节点;

须要获取当前节点的前驱节点,而头结点所相应的含义是当前站有锁且正在执行。

2. 当前驱节点是头结点而且可以获取状态。代表该当前节点占有锁。

假设满足上述条件,那么代表可以占有锁。依据节点对锁占有的含义,设置头结点为当前节点。

3. 否则进入等待状态。

假设没有轮到当前节点执行,那么将当前线程从线程调度器上摘下。也就是进入等待状态。

须要注意的是。acquire在运行过程中。并不能及时的对外界中断进行对应,必须等待运行完成之后,假设由外部中断。则进行中断响应。与acquire方法类似,acquireInterruptibly方法提供了获取状态能力,当然在无法获取状态的情况下会进入sync队列进行排队,这类似acquire。可是和acquire不同的地方在于它可以在外界对当前线程进行中断的时候提前结束获取状态的操作。换句话说,就是在类似synchronized获取锁时。外界可以对当前线程进行中断,而且获取锁的这个操作可以响应中断并提前返回。

一个线程处于synchronized块中或者进行同步I/O操作时。对该线程进行中断操作,这时该线程的中断标识位被设置为true,可是线程依然继续运行。

3.2.2 释放操作

释放操作代码例如以下:
[java] view
plain
copy

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);
  6. return true;
  7. }
  8. return false;
  9. }

上述逻辑主要包含:

1. 尝试释放状态;

tryRelease可以保证原子化的将状态设置回去,当然须要使用compareAndSet来保证。

假设释放状态成功过之后。将会进入后继节点的唤醒过程。

2. 唤醒当前节点的后继节点所包括的线程。

通过LockSupport的unpark方法将休眠中的线程唤醒,让其继续acquire状态。

4.锁--Synchronizer Framework Base Class—AbstractQueuedSynchronizer介绍的更多相关文章

  1. 《The java.util.concurrent Synchronizer Framework》 JUC同步器框架(AQS框架)原文翻译

    一.论文简介 闲来无事,看看源码,发现了一篇JDK作者的论文<The java.util.concurrent Synchronizer Framework>主要描述了作者对Abstrac ...

  2. The java.util.concurrent Synchronizer Framework笔记

    这篇笔记是关于 Doug Lea 的 The java.util.concurrent Synchronizer Framework . 原文地址:http://gee.cs.oswego.edu/d ...

  3. framework/base子目录

    framework/base下各子目录 ~/src/aosp_master/frameworks $ tree base/ -L 1 base/ ├── Android.bp ├── Android. ...

  4. RUF MVC5 Repositories Framework Generator代码生成工具介绍和使用

    RUF MVC5 Repositories Framework Generator代码生成工具介绍和使用 功能介绍 这个项目经过了大半年的持续更新到目前的阶段基本稳定 所有源代码都是开源的,在gith ...

  5. Entity Framework 6.1-Database First介绍

    原文:Entity Framework 6.1-Database First介绍 这种方式是比较传统的以数据库为核心的开发模式.比较适合有数据库DBA的团队.或者数据库已存在的情况. 优缺点: 1.优 ...

  6. Java显式锁学习总结之三:AbstractQueuedSynchronizer的实现原理

    概述 上一篇我们讲了AQS的使用,这一篇讲AQS的内部实现原理. 我们前面介绍了,AQS使用一个int变量state表示同步状态,使用一个隐式的FIFO同步队列(隐式队列就是并没有声明这样一个队列,只 ...

  7. AQS(AbstractQueuedSynchronizer)介绍-01

    1.概述 AQS( AbstractQueuedSynchronizer ) 是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来.如: ReentrantLock 和 ...

  8. mysql 开发进阶篇系列 6 锁问题(事务与隔离级别介绍)

    一.概述 在数据库中,数据是属于共享资源,为了保证并发访问的一致性,有效性,产生了锁.接下来重点讨论mysql锁机制的特点,常见的锁问题,以及解决mysql锁问题的一些方法或建议. 相比其他数据库,m ...

  9. iOS Framework: Introducing MKNetworkKit (MKNetworkKit介绍,入门,翻译)

    这片文章也有塞尔维亚-克罗地亚语(由Jovana Milutinovich翻译)和日语(由@noradaiko翻译) 如果有个一个网络库能够自动的为你处理cache该有多好啊. 如果有一个网络库能够在 ...

随机推荐

  1. Qt中文件操作遇到的(变量,容器,结构体)

    咳咳!总结了一下我在使用QT文件操作时所用到的,再接再厉!再接再厉!! 1.保存到文件: QFile file("test.txt"); if (!file.open(QIODev ...

  2. 【引用】Linux 内核驱动--多点触摸接口

    本文转载自James<Linux 内核驱动--多点触摸接口>   译自:linux-2.6.31.14\Documentation\input\multi-touch-protocol.t ...

  3. JAVA平台上的网络爬虫脚本语言 CrawlScript

    JAVA平台上的网络爬虫脚本语言 CrawlScript 网络爬虫即自动获取网页信息的一种程序,有很多JAVA.C++的网络爬虫类库,但是在这些类库的基础上开发十分繁琐,需要大量的代码才可以完成一 个 ...

  4. mongoose 数据库操作 - 分页

    使用mongoose 加入分页方法,临时还没发现什么更好的方法,我使用的方法是,直接在源代码中加入 找到 node_modules/mongoose/lib/model.js打开这个文件.里面加入这段 ...

  5. sass玩转颜色总结笔记

    变量: $color:#f00; 1.变浅和加深颜色,sass使用HSL标准来变浅或加深颜色 lighten($color,10%); darken($color,30%);             ...

  6. 数论F - Strange Way to Express Integers(不互素的的中国剩余定理)

    F - Strange Way to Express Integers Time Limit:1000MS     Memory Limit:131072KB     64bit IO Format: ...

  7. Domain(AD) 管理

    新建账户 文件夹权限管理

  8. SecureCRT使用Vim出现中文乱码问题的解决

    1. 首先保证securecrt本身显示中文是ok的.如果不是,就先解决这一个问题. 2. vi ~/.vimrc 添加set encoding=utf-8 fileencodings=ucs-bom ...

  9. 【转】增强 scite 编辑器的代码提示功能

    在 windows 下写 Lua, 我能找到的最好的编辑器就是 luaForWindows 项目里带的 scite. npp (即 notepad++ ) 也将就着能用, 不过只有代码高亮和简单的单词 ...

  10. ZOJ 3492 模拟循环链表线性查找

    WA了好几次最后找到错因是因为数组开小了! = = string whose length never exceeds 20 所以至少要开到21 = = ,我却一直开20 ╮(╯▽╰)╭ AC代码: ...