The j.u.c Synchronizer Framework翻译(一)背景与需求

原文链接 作者:Doug Lea 译者:欧振聪 校对:丁一

摘要

在J2SE 1.5的java.util.concurrent包(下称j.u.c包)中,大部分的同步器(例如锁,屏障等等)都是基于AbstractQueuedSynchronizer(下称AQS类)这个简单的框架来构建的。这个框架为同步状态的原子性管理、线程的阻塞和解除阻塞以及排队提供了一种通用机制。这篇论文主要描述了这个框架基本原理、设计、实现、用法以及性能。

1. 背景介绍

通过JCP的JSR166规范,Java的1.5版本引入了j.u.c包,这个包提供了一系列支持中等程度并发的类。这些组件是一系列的同步器(抽象数据类型(ADT))。这些同步器主要维护着以下几个功能:内部同步状态的管理(例如:表示一个锁的状态是获取还是释放),同步状态的更新和检查操作,且至少有一个方法会导致调用线程在同步状态被获取时阻塞,以及在其他线程改变这个同步状态时解除线程的阻塞。上述的这些的实际例子包括:互斥排它锁的不同形式、读写锁、信号量、屏障、Future、事件指示器以及传送队列等。

几乎任一同步器都可以用来实现其他形式的同步器。例如,可以用可重入锁实现信号量或者用信号量实现可重入锁。但是,这样做带来的复杂性,开销,不灵活使其至多只能是个二流工程。且缺乏吸引力。如果任何这样的构造方式不能在本质上比其他形式更简洁,那么开发者就不应该随意地选择其中的某个来构建另一个同步器。取而代之,JSR166建立了一个小框架,AQS类。这个框架为构造同步器提供一种通用的机制,并且被j.u.c包中大部分类使用,同时很多用户也用它来定义自己的同步器。

在这篇论文的下面部分会讨论这个框架的需求、设计与实现背后的主要思路、示例用法,以及性能指标的一些测量。

2 需求

2.1 功能

同步器一般包含两种方法,一种是acquire,另一种是release。acquire操作阻塞调用的线程,直到或除非同步状态允许其继续执行。而release操作则是通过某种方式改变同步状态,使得一或多个被acquire阻塞的线程继续执行。

j.u.c包中并没有对同步器的API做一个统一的定义。因此,有一些类定义了通用的接口(如Lock),而另外一些则定义了其专有的版本。因此在不同的类中,acquire和release操作的名字和形式会各有不同。例如:Lock.lock,Semaphore.acquire,CountDownLatch.await和FutureTask.get,在这个框架里,这些方法都是acquire操作。但是,J.U.C为支持一系列常见的使用选项,在类间都有个一致约定。在有意义的情况下,每一个同步器都支持下面的操作:

  • 阻塞和非阻塞(例如tryLock)同步。
  • 可选的超时设置,让调用者可以放弃等待
  • 通过中断实现的任务取消,通常是分为两个版本,一个acquire可取消,而另一个不可以。

同步器的实现根据其状态是否独占而有所不同。独占状态的同步器,在同一时间只有一个线程可以通过阻塞点,而共享状态的同步器可以同时有多个线程在执行。一般锁的实现类往往只维护独占状态,但是,例如计数信号量在数量许可的情况下,允许多个线程同时执行。为了使框架能得到广泛应用,这两种模式都要支持。

j.u.c包里还定义了Condition接口,用于支持管程形式的await/signal操作,这些操作与独占模式的Lock类有关,且Condition的实现天生就和与其关联的Lock类紧密相关。

2.2 性能目标

Java内置锁(使用synchronized的方法或代码块)的性能问题一直以来都在被人们关注,并且已经有一系列的文章描述其构造(例如引文[1],[3])。然而,大部分的研究主要关注的是在单核处理器上大部分时候使用于单线程上下文环境中时,如何尽量降低其空间(因为任何的Java对象都可以当成是锁)和时间的开销。对于同步器来说这些都不是特别重要:程序员仅在需要的时候才会使用同步器,因此并不需要压缩空间来避免浪费,并且同步器几乎是专门用在多线程设计中(特别是在多核处理器上),在这种环境下,偶尔的竞争是在意料之中的。因此,常规的JVM锁优化策略主要是针对零竞争的场景,而其它场景则使用缺乏可预见性的“慢速路径(slow paths)” ,所以常规的JVM锁优化策略并不适用于严重依赖于J.U.C包的典型多线程服务端应用。

这里主要的性能目标是可伸缩性,即在大部分情况下,即使,或特别在同步器有竞争的情况下,稳定地保证其效率。在理想的情况下,不管有多少线程正试图通过同步点,通过同步点的开销都应该是个常量。在某一线程被允许通过同步点但还没有通过的情况下,使其耗费的总时间最少,这是主要目标之一。然而,这也必须考虑平衡各种资源,包括总CPU时间的需求,内存负载以及线程调度的开销。例如:获取自旋锁通常比阻塞锁所需的时间更短,但是通常也会浪费CPU时钟周期,并且造成内存竞争,所以使用的并不频繁。

实现同步器的这些目标包含了两种不同的使用类型。大部分应用程序是最大化其总的吞吐量,容错性,并且最好保证尽量减少饥饿的情况。然而,对于那些控制资源分配的程序来说,更重要是去维持多线程读取的公平性,可以接受较差的总吞吐量。没有任何框架可以代表用户去决定应该选择哪一个方式,因此,应该提供不同的公平策略。

无论同步器的内部实现是多么的精雕细琢,它还是会在某些应用中产生性能瓶颈。因此,框架必须提供相应的监视工具让用户发现和缓和这些瓶颈。至少需要提供一种方式来确定有多少线程被阻塞了。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: The j.u.c Synchronizer Framework翻译(一)背景与需求

  • Trackback 关闭
  • 评论 (2)
  1. 第一篇和第二篇怎么都链接到同一篇文章了?

    • icngor
    • 2018/01/09 10:58上午

    “这个包提供了一系列支持中等程度并发的类”什么叫中等程度

return top