java.util.concurrent解析——AbstractQueuedSynchronizer综述

尽管JVM在并发上已经做了很多优化工作,如偏向锁、轻量级锁、自旋锁等等。但是基于Synchronized wait notify实现的同步机制还是无法满足日常开发中。原生同步机制在时间和空间上的开销也一直备受诟病。为了提升Java程序在并发场景下的性能、扩展性和健壮性,java.util.concurrent的使用必不可少。java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。通过使用java.util.concurrent,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。

java.util.concurrent的功能很强大,要想完整了解其全部细节也是很不容易的,需要多年的学习和实践经验。不过,通过深入其核心部分,可以快速了解其骨架和底层实现机制。那么谁才是java.util.concurrent的核心组件呢?稍微看过一点java.util.concurrent源码的同学知道,concurrent包下很多组件如:ReentrantLock Semaphore CountDownLatch在其内部都有一个sync类,而这个sync有继承自java.util.concurrent.locks.AbstractQueuedSynchronizer,而这个AbstractQueuedSynchronizer就是concurrent包的核心。尽管AbstractQueuedSynchronizer只是一个类,但其实质上却提供了一个框架,通过提供基于FIFO的队列管理机制、线程阻塞机制和状态同步机制,用户可以快速基于AbstractQueuedSynchonizer完成一系列复杂的进程同步操作。如果第一次接触到AbstractQueuedSynchronizer,建议读一下其作者的论文:The java.util.concurrent Synchronizer Framework

1 概述

AbstractQueuedSynchronizer(以下简称AQS)从字面理解是一个抽象的基于队列的同步器,所以AQS至少要完成以下几部分工作:

  • 同步状态的原子性管理
  • 等待线程队列的维护
  • 线程的阻塞和唤醒
  • 仅定义核心操作,留出足够的扩展性给子类

AQS定义了两个核心操作:acquire release及其变种。前者用于进入同步块前获取同步块执行权,后者用于释放对于同步块的占有权。

acquire核心逻辑如下:

// 循环里不断尝试,典型的失败后重试
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; 

release核心逻辑如下:

update synchronization state;    //  更新同步状态
if (state may permit a blocked thread to acquire) // 检查状态是否允许一个阻塞线程获取
      unblock one or more queued threads;     // 允许,则唤醒后继的一个或多个阻塞线程。

而要实现上述两个核心接口,就必须实现前文提到的AQS主要工作的前三项:

  • 同步状态的原子性管理
  • 阻塞线程队列的维护
  • 线程的阻塞和唤醒

实际使用中,AQS提供了以下5个模板方法:

tryAcquire(int)      // 试图在独占模式下获取对象状态。此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。
tryRelease(int)       // 试图设置状态来反映独占模式下的一个释放。
tryAcquireShared(int)       // 试图在共享模式下获取对象状态。此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它。
tryReleaseShared(int)       // 试图设置状态来反映共享模式下的一个释放。
isHeldExclusively()      // 如果对于当前(正调用的)线程,同步是以独占方式进行的,则返回 true。此方法是在每次调用非等待 AbstractQueuedSynchronizer.ConditionObject 方法时调用的。(等待方法则调用 release(int)。)

2 实现

2.1 同步状态的原子性管理

AQS内部维护一个32bit字段state用于描述当前状态,state字段有volatile修饰,保证了其可见性。同时AQS还提供了getState,setState, compareAndSetState等方法用于状态的读取和更新:

  • getState:提供一个基于内存语义(memory semantics)的volatile变量(state)读取
  • setState:提供一个基于内存予以(memory semantics)的volatile变量(state)更新
  • compareAndSetState:提供一个基于CAS(compare and swap)的原子性状态更新操作

通过简单的原子读写就可以达到内存可视性,减少了同步的需求。子类可以获取和设置状态的值,通过定义状态的值来表示 AQS 对象是否被获取或被释放。

2.2 线程的阻塞与唤醒

AQS基于java.util.concurrent.locks.LockSupport 支持创建锁和其他同步类需要的基本线程阻塞、解除阻塞原语。

这个类最主要的功能有两个:

  • park:把线程阻塞
  • unpark:让线程恢复执行

其实除了LockSupport,Java之初就有Object对象的wait和notify方法可以实现线程的阻塞和唤醒。那么它们的区别是什么呢?

主要的区别应该说是它们面向的对象不同。阻塞和唤醒是对于线程来说的,LockSupport的park/unpark更符合这个语义,以“线程”作为方法的参数, 语义更清晰,使用起来也更方便。而wait/notify的实现使得“线程”的阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程、什么时候阻塞/唤醒很困难, 要不随机唤醒一个线程(notify)要不唤醒所有的(notifyAll)。

LockSupport并不需要获取对象的监视器。LockSupport机制是每次unpark给线程1个“许可”——最多只能是1,而park则相反,如果当前 线程有许可,那么park方法会消耗1个并返回,否则会阻塞线程直到线程重新获得许可,在线程启动之前调用park/unpark方法没有任何效果。

// 1次unpark给线程1个许可
LockSupport.unpark(Thread.currentThread());
// 如果线程非阻塞重复调用没有任何效果
LockSupport.unpark(Thread.currentThread());
// 消耗1个许可
LockSupport.park(Thread.currentThread());
// 阻塞
LockSupport.park(Thread.currentThread());

因为它们本身的实现机制不一样,所以它们之间没有交集,也就是说LockSupport阻塞的线程,notify/notifyAll没法唤醒。

2.3 队列维护

队列管理是AQS的核心部分,作者采用了基于CLH锁队列来实现内部队列。CLH锁(可参考:CLH锁)通常用于自旋锁,我们反而用于阻塞同步器,但使用相同的基本策略:在(线程)它自己结点持有关于线程的一些控制信息。每个结点的 “status” 字段跟踪一个线程是否应该阻塞。一个结点在它的前驱释放时被通知。队列的每个结点作为一个特定通知风格(specific-notification-style)的监视器服务,持有单一等待线程。”status” 字段不控制线程是否授予。一个线程可能尝试去获取如果它是第一个进入队列,但成为第一个不保证就成功;它只是获得权利去竞争,所以当前释放的竞争者线程可能需要再次等待(注:这是公平性的问题,子类的实现可以进行控制)。

为了进入CLH锁队列,你只需要原子地把它作为一个新的尾结点拼接;为了出队列,你只需要设置 “head” 字段。

      +------+  prev +-----+       +-----+
 head |      | <---- |     | <---- |     |  tail
      +------+         +-----+       +-----+

队列部分比较复杂,详细的介绍请参考下一篇博客。

3 总结

本文只在于提纲挈领式地指出AQS的大致框架以及主要作用,读者需要了解作为一个维护内部竞争队列的同步器,AQS需要完成三部分工作:

  • 共享状态的原子性维护
  • 线程的阻塞与唤醒
  • 竞争队列的维护
时间: 2024-11-02 12:50:01

java.util.concurrent解析——AbstractQueuedSynchronizer综述的相关文章

java.util.concurrent解析——AbstractQueuedSynchronizer队列管理

上一篇博客中,我们提到AQS的队列管理是基于CLH锁队列实现的,所以首先我们来看下CLH锁队列. 1 CLH锁队列 CLH锁队列本质上是一个基于链表的FIFO自旋锁队列,队列中的每一个节点实质上是一个自旋锁:在阻塞时不断循环读取状态变量,当前驱节点释放同步对象使用权后,跳出循环,执行同步代码.其基本结构如下: 队列中每一个节点有两个成员: 节点状态变量 前驱指针:pred head,tail并不是实际节点,只是为了表示队列的首尾,被称为dumb node. 在如此结构之下,其enqueue操作逻

java.util.concurrent解析——ThreadPoolExecutor源码解析

任何一种语言.框架,线程都是非常重要的一部分.要想实现异步就需要通过异步线程,但是频繁地创建销毁线程会带来较大的性能开销,而线程池就是为解决这一问题而出现的.简单来说线程池有以下几大优势: 降低资源开销:通过复用已经创建的线程,降低线程频繁创建.销毁带来的资源开销和性能损耗 快速启动任务:通过复用已有线程,快速启动任务 易于管理:线程池可以统一管理.分配.调优和监控 Java中的线程池是基于ThreadPoolExecutor实现的,我们使用的ExecutorService的各种线程池策略都是基

java.util.concurrent解析——FutureTask源码解析

1. Runnable.Callable.Future.FutureTask的区别与联系 和Java异步打交道就不能回避掉Runnable,Callable,Future,FutureTask等类,首先来介绍下这几个类的区别. 1.1 Runnable Runnable接口是我们最熟悉的,它只有一个run函数.然后使用某个线程去执行该runnable即可实现多线程,Thread类在调用start()函数后就是执行的是Runnable的run()函数.Runnable最大的缺点在于run函数没有返

剖析java.util.concurrent锁

1. 导言 程序的性能分析是应用程序开发过程中的一个重要方面.这个工作一般是由一些专业人员来完成的,他们的目标是在一个特定的平台上,提高代码的性能.当程序是运行在多核平台的多线程或者并行程序的时候,提高性能这个问题就变得更加困难了.因为在这样的情况下,不仅需要考虑代码的性能,还需要考虑代码的可伸缩性. 随着Java 5中引入了java.util.concurrent (JUC)包,在Java语言中出现了一种新的锁.JUC包使用得越来越普遍,因为更多的应用程序需要为了多核系统而开发或仔细地调优.虽

【JAVA秒会技术之多线程】多线程java.util.concurrent详解

一.多线程 1.操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己的独立地址空间,包含程序内容和数据:不同进程的地址空间是互相隔离的:进程拥有各种资源和状态信息,包括打开的文件.子进程和信号处理. 线程:表示程序的执行流程,是CPU调度执行的基本单位:线程有自己的程序计数器.寄存器.堆栈和帧.同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源. 2.Java标准库提供了进程和线程相关的API,进程主要包括表示进程的jav

java.util.concurrent包源码阅读(二) java.util.concurrent.atomic包

Aomic数据类型有四种类型:AomicBoolean, AomicInteger, AomicLong, 和AomicReferrence(针对Object的)以及它们的数组类型, 还有一个特殊的AomicStampedReferrence,它不是AomicReferrence的子类,而是利用AomicReferrence实现的一个储存引用和Integer组的扩展类 首先,所有原子操作都是依赖于sun.misc.Unsafe这个类,这个类底层是由C++实现的,利用指针来实现数据操作 关于CAS

java.util.concurrent包源码阅读(一) 源码包的结构

准备花点时间阅读一下java.util.concurrent这个包里面的java代码,大致看了一下,这个包我个人觉得大致可以分为五个部分(如有不对之处,还望指正) 第一部分 Aomic数据类型 这部分都被放在java.util.concurrent.atomic这个包里面,实现了原子化操作的数据类型,包括 Boolean, Integer, Long, 和Referrence这四种类型以及这四种类型的数组类型. 第二部分 锁 这部分都被放在java.util.concurrent.lock这个包

关于java.util.concurrent您不知道的5件事,第2部分

并发 Collections 提供了线程安全.经过良好调优的数据结构,简化了并发编程.然而, 在一些情形下,开发人员需要更进一步,思考如何调节和/或限制线程执行.由于 java.util.concurrent 的总体目标是简化多线程编程,您可能希望该包包含同步实用程序,而 它确实包含. 本文是 第 1 部分 的延续,将介绍几个比核心语言原语(监视器)更高级的同步结构,但 它们还未包含在 Collection 类中.一旦您了解了这些锁和门的用途,使用它们将非常直观. 1. Semaphore 在一

关于java.util.concurrent您不知道的 5 件事,第1部分

通过并发 Collections 进行多线程编程 Concurrent Collections 是 Java 5 的巨大附加产品,但是在关于注释和泛型的争 执中很多 Java 开发人员忽视了它们.此外(或者更老实地说),许多开发人员避免使用这个 数据包,因为他们认为它一定很复杂,就像它所要解决的问题一样. 事实上,java.util.concurrent 包含许多类,能够有效解决普通的并发问题,无需复杂工 序.阅读本文,了解 java.util.concurrent 类,比如 CopyOnWri