JUC同步框架详解

前言

最近几个周末都在学习JUC有关的东西,在学习AQS时在网上发现了《The java.util.concurrent Synchronizer Framework》这篇论文的中文翻译,因此分享下。

英文版地址: http://gee.cs.oswego.edu/dl/papers/aqs.pdf

摘要

在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时钟周期,并且造成内存竞争,所以使用的并不频繁。

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

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

3 设计与实现

同步器背后的基本思想非常简单。acquire操作如下:

1

2

3

4

5

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操作如下:

1

2

3

update synchronization state;

if (state may permit a blocked thread to acquire)

unblock one or more queued threads;

为了实现上述操作,需要下面三个基本组件的相互协作:

  • 同步状态的原子性管理;
  • 线程的阻塞与解除阻塞;
  • 队列的管理;

创建一个框架分别实现这三个组件是有可能的。但是,这会让整个框架既难用又没效率。例如:存储在队列节点的信息必须与解除阻塞所需要的信息一致,而暴露出的方法的签名必须依赖于同步状态的特性。

同步器框架的核心决策是为这三个组件选择一个具体实现,同时在使用方式上又有大量选项可用。这里有意地限制了其适用范围,但是提供了足够的效率,使得实际上没有理由在合适的情况下不用这个框架而去重新建造一个。

3.1 同步状态

AQS类使用单个 int (32位)来保存同步状态,并暴露出 getState 、 setState 以及 compareAndSet 操作来读取和更新这个状态。这些方法都依赖于j.u.c.atomic包的支持,这个包提供了兼容JSR133中 volatile 在读和写上的语义,并且通过使用本地的compare-and-swap或load-linked/store-conditional指令来实现 compareAndSetState ,使得仅当同步状态拥有一个期望值的时候,才会被原子地设置成新值。

将同步状态限制为一个32位的整形是出于实践上的考量。虽然JSR166也提供了64位 long 字段的原子性操作,但这些操作在很多平台上还是使用内部锁的方式来模拟实现的,这会使同步器的性能可能不会很理想。当然,将来可能会有一个类是专门使用64位的状态的。然而现在就引入这么一个类到这个包里并不是一个很好的决定( 译者注:JDK1.6中已经包含 java.util.concurrent.locks.AbstractQueuedLongSynchronizer 类,即使用 long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本 )。目前来说,32位的状态对大多数应用程序都是足够的。在j.u.c包中,只有一个同步器类可能需要多于32位来维持状态,那就是 CyclicBarrier 类,所以,它用了锁(该包中大多数更高层次的工具亦是如此)。

基于AQS的具体实现类必须根据暴露出的状态相关的方法定义 tryAcquire 和 tryRelease 方法,以控制acquire和release操作。当同步状态满足时, tryAcquire 方法必须返回 true ,而当新的同步状态允许后续acquire时, tryRelease 方法也必须返回 true 。这些方法都接受一个 int 类型的参数用于传递想要的状态。例如:可重入锁中,当某个线程从条件等待中返回,然后重新获取锁时,为了重新建立循环计数的场景。很多同步器并不需要这样一个参数,因此忽略它即可。

3.2 阻塞

在JSR166之前,阻塞线程和解除线程阻塞都是基于Java内置监视器,没有基于Java API可以用来创建同步器。唯一可以选择的是 Thread.suspend 和 Thread.resume ,但是它们都有无法解决的竞态问题,所以也没法用࿱

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值