在理解J.U.C原理以及锁机制之前,我们来介绍J.U.C框架最核心也是最复杂的一个基础类:java.util.concurrent.locks.AbstractQueuedSynchronizer。

AQS

AbstractQueuedSynchronizer,简称AQS,是J.U.C最复杂的一个类,导致绝大多数讲解并发原理或者实战的时候都不会提到此类。但是虚心的作者愿意借助自己有限的能力和精力来探讨一二(参考资源中也有一些作者做了部分的分析。)。

首先从理论知识开始,在了解了相关原理后会针对源码进行一些分析,最后加上一些实战来描述。

上面的继承体系中,AbstractQueuedSynchronizer是CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基础,因此AbstractQueuedSynchronizer是Lock/Executor实现的前提。公平锁、不公平锁、Condition、CountDownLatch、Semaphore等放到后面的篇幅中说明。

完整的设计原理可以参考Doug Lea的论文 The java.util.concurrent Synchronizer Framework ,这里做一些简要的分析。

基本的思想是表现为一个同步器支持下面两个操作:

获取锁:首先判断当前状态是否允许获取锁,如果是就获取锁,否则就阻塞操作或者获取失败,也就是说如果是独占锁就可能阻塞,如果是共享锁就可能失败。另外如果是阻塞线程,那么线程就需要进入阻塞队列。当状态位允许获取锁时就修改状态,并且如果进了队列就从队列中移除。

while(synchronization state does not allow acquire){

enqueue current thread if not already queued;

possibly block current thread;

}

dequeue current thread if it was queued;

释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。

update synchronization state;

if(state may permit a blocked thread to acquire)

unlock one or more queued threads;

要支持上面两个操作就必须有下面的条件:

  • 原子性操作同步器的状态位
  • 阻塞和唤醒线程
  • 一个有序的队列

目标明确,要解决的问题也清晰了,那么剩下的就是解决上面三个问题。

状态位的原子操作

这里使用一个32位的整数来描述状态位,前面章节的原子操作的理论知识整好派上用场,在这里依然使用CAS操作来解决这个问题。事实上这里还有一个64位版本的同步器(AbstractQueuedLongSynchronizer),这里暂且不谈。

阻塞和唤醒线程

标准的JAVA API里面是无法挂起(阻塞)一个线程,然后在将来某个时刻再唤醒它的。JDK 1.0的API里面有Thread.suspend和Thread.resume,并且一直延续了下来。但是这些都是过时的API,而且也是不推荐的做法。

在JDK 5.0以后利用JNI在LockSupport类中实现了此特性。

LockSupport.park()
LockSupport.park(Object)
LockSupport.parkNanos(Object, long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(Object, long)
LockSupport.parkUntil(long)
LockSupport.unpark(Thread)

上面的API中park()是在当前线程中调用,导致线程阻塞,带参数的Object是挂起的对象,这样监视的时候就能够知道此线程是因为什么资源而阻塞的。由于park()立即返回,所以通常情况下需要在循环中去检测竞争资源来决定是否进行下一次阻塞。park()返回的原因有三:

  • 其他某个线程调用将当前线程作为目标调用 unpark
  • 其他某个线程中断当前线程;
  • 该调用不合逻辑地(即毫无理由地)返回。

其实第三条就决定了需要循环检测了,类似于通常写的while(checkCondition()){Thread.sleep(time);}类似的功能。

有序队列

在AQS中采用CHL列表来解决有序的队列的问题。

AQS采用的CHL模型采用下面的算法完成FIFO的入队列和出队列过程。

对于入队列(enqueue):采用CAS操作,每次比较尾结点是否一致,然后插入的到尾结点中。

do {

pred = tail;

}while ( !compareAndSet(pred,tail,node) );

对于出队列(dequeue):由于每一个节点也缓存了一个状态,决定是否出队列,因此当不满足条件时就需要自旋等待,一旦满足条件就将头结点设置为下一个节点。

while (pred.status != RELEASED) ;

head  = node;

实际上这里自旋等待也是使用LockSupport.park()来实现的。

AQS里面有三个核心字段:

private volatile int state;

private transient volatile Node head;

private transient volatile Node tail;

其中state描述的有多少个线程取得了锁,对于互斥锁来说state<=1。head/tail加上CAS操作就构成了一个CHL的FIFO队列。下面是Node节点的属性。

volatile int waitStatus; 节点的等待状态,一个节点可能位于以下几种状态:

  • CANCELLED = 1: 节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。
  • SIGNAL = -1: 节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。
  • CONDITION = -2:表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
  • 0: 正常状态,新生的非CONDITION节点都是此状态。
  • 非负值标识节点不需要被通知(唤醒)。

volatile Node prev;此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。

volatile Node next;此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。

volatile Thread thread;节点绑定的线程。

Node nextWaiter;下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。

AQS 在J.U.C里面是一个非常核心的工具,而且也非常复杂,里面考虑到了非常多的逻辑实现,所以在后面的章节中总是不断的尝试介绍AQS的特性和实现。

这一个小节主要介绍了一些理论背景和相关的数据结构,在下一个小节中将根据以上知识来了解Lock.lock/unlock是如何实现的。

参考资料:

(1)ReentrantLock代码剖析之ReentrantLock.lock ReentrantLock代码剖析之ReentrantLock.unlock ReentrantLock代码剖析之ReentrantLock.lockInterruptibly

(2)java多线程--java.util.concurrent.locks.AbstractQueuedSynchronizer解析(只包含多线程同步示例)

(3)处理 InterruptedException

(4)AbstractQueuedSynchronizer源码解析之ReentrantLock(一)  AbstractQueuedSynchronizer源码解析之ReentrantLock(二)

(5)The java.util.concurrent Synchronizer Framework

深入浅出 Java Concurrency (7): 锁机制 part 2 AQS的更多相关文章

  1. 深入浅出 Java Concurrency (7): 锁机制 part 2 AQS[转]

    在理解J.U.C原理以及锁机制之前,我们来介绍J.U.C框架最核心也是最复杂的一个基础类:java.util.concurrent.locks.AbstractQueuedSynchronizer. ...

  2. 深入浅出 Java Concurrency (6): 锁机制 part 1 Lock与ReentrantLock

      前面的章节主要谈谈原子操作,至于与原子操作一些相关的问题或者说陷阱就放到最后的总结篇来整体说明.从这一章开始花少量的篇幅谈谈锁机制. 上一个章节中谈到了锁机制,并且针对于原子操作谈了一些相关的概念 ...

  3. 深入浅出 Java Concurrency (15): 锁机制 part 10 锁的一些其它问题

      主要谈谈锁的性能以及其它一些理论知识,内容主要的出处是<Java Concurrency in Practice>,结合自己的理解和实际应用对锁机制进行一个小小的总结. 首先需要强调的 ...

  4. 深入浅出 Java Concurrency (9): 锁机制 part 4 锁释放与条件变量 (Lock.unlock And Condition)

    本小节介绍锁释放Lock.unlock(). Release/TryRelease unlock操作实际上就调用了AQS的release操作,释放持有的锁. public final boolean ...

  5. 深入浅出 Java Concurrency (15): 锁机制 part 10 锁的一些其它问题[转]

    主要谈谈锁的性能以及其它一些理论知识,内容主要的出处是<Java Concurrency in Practice>,结合自己的理解和实际应用对锁机制进行一个小小的总结. 首先需要强调的一点 ...

  6. 深入浅出 Java Concurrency (9): 锁机制 part 4[转]

    本小节介绍锁释放Lock.unlock(). Release/TryRelease unlock操作实际上就调用了AQS的release操作,释放持有的锁. public final boolean ...

  7. 深入浅出 Java Concurrency (6): 锁机制 part 1[转]

    前面的章节主要谈谈原子操作,至于与原子操作一些相关的问题或者说陷阱就放到最后的总结篇来整体说明.从这一章开始花少量的篇幅谈谈锁机制. 上一个章节中谈到了锁机制,并且针对于原子操作谈了一些相关的概念和设 ...

  8. 深入浅出 Java Concurrency (12): 锁机制 part 7 信号量(Semaphore)

      Semaphore 是一个计数信号量.从概念上讲,信号量维护了一个许可集.如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可.每个 release() 添加一个许可,从而可能 ...

  9. 深入浅出 Java Concurrency (12): 锁机制 part 7 信号量(Semaphore)[转]

    Semaphore 是一个计数信号量.从概念上讲,信号量维护了一个许可集.如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可.每个 release() 添加一个许可,从而可能释放 ...

随机推荐

  1. [Github] 本地git push免用户名和密码的配置

    在终端通过git config --global命令进行配置 git config --global user.email "xxx@xxmail.com" git config ...

  2. 从客户端中检测到有潜在危险的 Request.Form 值的问题的解决方法。

    在controller控制器里面添加[ValidateInput(false)]         [ValidateInput(false)]        public ActionResult m ...

  3. 【剑指offer15】二进制中1的个数(位运算),C++实现

    原创博文,转载请注明出处! # 本文是牛客网<剑指offer>刷题笔记 1.题目 # 输入一个整数,输出该数二进制表示中1的个数.其中负数用补码表示.例如,把9表示成二进制是1001,有两 ...

  4. 【剑指offer】07重建二叉树,C++实现

    本博文是原创博文,转载请注明出处! # 本文为牛客网<剑指offer>刷题笔记 1.题目 # 输入某二叉树的前序遍历和中序遍历的结果,重建二叉树 2.思路(递归) # 前序遍历中,第一个数 ...

  5. HihoCoder 1055 : 刷油漆 树形DP第一题(对象 点)

    刷油漆 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上回说到,小Ho有着一棵灰常好玩的树玩具!这棵树玩具是由N个小球和N-1根木棍拼凑而成,这N个小球都被小Ho标上了 ...

  6. JSP学习(三)JSP中的九个内置对象

    JSP中的九个内置对象 NO. 内置对象 类型 1 pageContext javax.servlet.jsp.PageContext 2 request javax.servlet.http.Htt ...

  7. 从BZOJ2242看数论基础算法:快速幂,gcd,exgcd,BSGS

    LINK 其实就是三个板子 1.快速幂 快速幂,通过把指数转化成二进制位来优化幂运算,基础知识 2.gcd和exgcd gcd就是所谓的辗转相除法,在这里用取模的形式体现出来 \(gcd(a,b)\) ...

  8. hadoop 的job.setOutputKeyClass和job.setOutputValueClass的几个问题

    昨天写了一个mapreduce函数一直有错误,找不到错误,今天找了一天终于解决了,原来是hadoop 的job.setOutputKeyClass和job.setOutputValueClas设置输出 ...

  9. Windows10安装MySQL8.0

    1.到MySQL官网下载安装包:https://dev.mysql.com/downloads/mysql/:选择8.0版本: 2.将下载好的安装包(mysql-8.0.12-winx64 .zip) ...

  10. BZOJ5369 [Pkusc2018]最大前缀和

    题意 小C是一个算法竞赛爱好者,有一天小C遇到了一个非常难的问题:求一个序列的最大子段和. 但是小C并不会做这个题,于是小C决定把序列随机打乱,然后取序列的最大前缀和作为答案. 小C是一个非常有自知之 ...