最近在使用memcache客户端的时候,发现一个可能是多线程的问题,客户端的实现是NIO+JUC,由于出现频率很低,场景没有办法复原,一直没有找到问题的真正原因,通过代码走查也没有发现任何问题,于是决定回顾一下JUC的东西,看看是不是可以受到启发,于是决定先看一下大牛Doug Lea的论文,顺便翻译一下。由于英文水平很挫,又是第一次,希望不要误导了大家。废话不表。
JAVA.util.concurrent 同步框架
Doug Lea SUNY Oswego Oswego NY 13126 dl@cs.oswego.edu
摘要
J2SE1.5的java.util.concurrent包中大多数同步器(锁,壁垒(barrier)等)都是基于一个轻量级框架建立起来的,这个框架的基础类就是AbstractQueuedSynchronizer类。
这个框架为同步状态的原子性操作、阻塞和唤醒线程和排队提供了通用的机制。本文将介绍这个框架的基本原理、设计、实现、使用和性能。
分类和主题
D.1.3 [编程技术]:并发编程 – 并行编程
通用术语
算法、测量(measurement)、性能、设计。
keywords
同步、Java
1、引言
通过Java社区进程(JCP)的Java规范请求(JSR)166,java 1.5引入了java.util.concurrent包,它提供了一系列中等水平的并发支持类,这些组件是一系列的同步器,即一个维护了内部同步状态的抽象数据类型(ADT)(例如:表示一个锁是锁定还是解锁)。可以对这个状态的更新和检查,并且必须至少提供一个方法,调用它可以阻塞线程。并且当其它线程改变这个状态的时候,允许该线程唤醒。例如:各种形式的互斥锁、读写锁、信号量(semaphores)、 壁垒(barrier)、futures、 事件信号(event indicators)、 和交替队列(handoff queues)。
众所周知(见[2])几乎任何同步器都可用于实现其他的同步器,例如:可以通过可重入锁(reentrant locks)构建信号量(semaphores ),反之亦然;然而,这样的实现往往都非常复杂,过度的设计和僵化的实现,充其量只能是备选方案;此外在概念上也不具有吸引力,如果这些结构与其他没有本质上的区别,开发人员不应该被强迫选择任意其中之一作为建设其他的基础;相反,JSR166建立一个以类AbstractQueuedSynchronizer为中心的轻量级框架,它提供了一个公共的机制,包里面提供的同步器都是基于它,当然用户也可以定义自己的类;
本文的其余部分讨论了这个框架的需求,设计、实现和用法示例,以及一些其性能特点;
2、需求
2.1、功能需求
同步器具有两种类型的方法[7]:至少有一个获取锁操作(acquire ),它阻塞线程直到同步状态允许继续执行,另外需要一个释放操作(release),它改变同步的状态,以允许一个或多个阻塞的线程解锁。java.util.concurrent包中没有为同步器定义一个统一的API。一些定义是通过通用的接口(如锁),另外一些只是包含在特定的版本中;因此,获取和释放操作,在不同的类中的名称和形式不一样。例如,Lock.lock Semaphore.acquire,CountDownLatch.await和FutureTask.get方法都是获取操作。然而,不同的类也保持一致的约定,以支持一系列常见的用法。在有意义的时候,每个同步支持:
1、非阻塞同步(例如的tryLock)以及阻断版本;
2、可选的超时,这样应用程序可以放弃等待;
3、通过中断(interruption)实现可取消,通常提供一个可中断的和不可中断的获取操作(acquire )
根据他们是否只维护一个互斥量(exclusive),同步器可能会有所不同;互斥量(exclusive)表示在这个可能的阻塞点一次只有一个线程可以执行;对应还有的阻塞点可以允许一次至少一个线程允许。他们叫共享量(shared);通常的锁都是独占的(拥有一个互斥量),但是像计数器。可以允许多个允许计数的线程同时获取;要想广泛使用,该框架必须支持两种操作模式。
java.util.concurrent包中还定义了Condition接口,提供监视器风格的阻塞(await)和唤醒(signal)操作,可以在独占类型的锁里使用,它的实现本质上是依赖于他关联的锁;
2.2 性能需求
Java内置锁(使用synchronized方法或者synchronized块)开发者长期以来一直担心它的性能,关于他的研究文献也相当可观([1], [3]),然而,这些工作的主要重点是在单处理器单线程的上下文中使用时最大限度地减少空间上的开销(因为任何Java对象可以作为一个锁)和最大限度地减少时间开销;但是这些都不是同步器应该关心的重点:1、程序员只在需要时构建同步器,所以没有必要压缩空间;2、 可以预料同步器几乎全部用在多线程设计(多处理器的场景也越来越多)。通常JVM的优化,也只是针对零竞争的场景,其他的场景是很难预见和处理的; "slow paths" [12] is not the right tactic for typical multithreaded server applications that rely heavily on java.util.concurrent.(暂时没想到好的翻译)
相反,这里的主要目标是可扩展性:特别是当使用同步器是有争议的时候可以预测维护效率。理想的情况下,不管多少线程同步,在一个同步点上的开销应该是恒定的。其中的主要目标是,以尽量减少总时间,在此期间,一个线程允许通过一个同步点,但其他将会阻塞。当然,这必须兼顾对其他资源的考虑,比如:总CPU时间,内存开销,和线程调度的开销。举例来说,自旋锁(spinlocks)通常比阻塞锁提供更短的获取时间,但是通常因为空循环和内存争用而不经常使用。
这些目标覆盖了两种使用风格,大多数应用程序追求最大化的总吞吐量,容忍饥饿的出现,然而另外一些应用,如资源控制,对他们来说更为重要的是保持线程的公平性,允许低的总吞吐量。 框架不可以代替用户决定这些相互冲突的目标,而是必须实现不同的公平策略。
不管如何精心设计的,对于某些应用,同步器将出现性能瓶颈;因此,框架必须提供可监测和检查的基本操作,让用户及时发现和缓解瓶颈;最基本的功能(也是最有用)需要提供一种方法来确定有多少线程被阻塞;
3、设计和实现
一个同步器背后的基本理念是非常简单。一个获取操作(acquire)的处理如下:
while (同步状态不能获取(acquire)) { 当前线程如没有排队等待,则排队 可能阻塞当前线程; } 如果当前线程排队,则出队列;
一个释放(release)操作的处理流程如下:
更新同步器状态 if(同步状态允许一个阻塞的线程获取(acquire)) 唤醒一个或者多个线程
支持这些操作需要下面三个基本组成部分的协调:
同步状态的原子管理
线程阻塞和解除阻塞
维护一个队列
虽然可以建立一个框架,使这三件独立变化;然而,这既不是非常有效的,也是不可用的。例如,存放在队列节点的信息必须与需要释放的线程关联,并且暴露的方法签名必须依赖于同步状态的特性。同步框架的核心设计决策是为这三个组成部分选择一个具体实现,同时仍保持一个灵活的扩展;虽然限制了适用范围,但提供了高效率的支持,几乎没有任何理由不使用它,而去从头开始构建同步器。
3.1 同步状态
类AbstractQueuedSynchronizer使用一个(32位)的整数维护同步状态,并且提供getState,setState和compareAndSetState方法访问和更新状态;反过来,
java.util.concurrent.atomic的这些方法支持JSR133(Java内存模型)定义的读取和写入操作的可见性(volatile)语义;并且通过一个native的
compare-and-swap 和 loadlinked/ store-conditional 来实现 compare- AndSetState方法;只有当它拥有的值和给定的预期相同,才会更新为一个新的值,整个操作保持原子性。
以一个32位的int维护同步状态,是一个务实的决定。虽然JSR166还提供了64位长的原子操作,但是这些仍然必须在适当的平台上使用,用来模拟内部锁。否则同步器可能不会工作的很好,在将来,很可能添加第二个基类提供一个专门的64位状态(即 long 型);但是,现在没有一个令人信服的理由,将它包含在包里;目前,32位满足大多数应用,
java.util.concurrent中只有一个CyclicBarrier同步类,维护更多的位来保持状态,代替使用锁(它像大多数更高级别的工具包)。
基于类AbstractQueuedSynchronizer具体实现必须定义tryAcquire和tryRelease方法,用这些对外暴露的方法控制获取和释放操作。如果tryAcquire方法获取(acquire)成功必须返回true,如果新的同步状态允许新的线程获取,tryRelease方法必须返回true,这些方法只接受一个int参数,用于各自状态的沟通,例如, 可重入锁( reentrant lock),当等待条件返回后重新获得锁需要重新建立递归计数。许多同步器并不需要这个参数,可以直接忽略它。
3.2 阻塞:
JSR166之前,没有Java API可以阻塞和唤醒线程,用于构建一个不基于内在的监视器(monitors.)的同步机制。唯一可以使用的是Thread.suspend 和Thread.resume,
但是由于他们有一个无法解决的竞争问题,也难以使用,即:如果一个解除阻塞的线程在阻塞线程被暂停(suspend)之前执行恢复操作(resume),恢复操作(resume)不会有任何效果。java.util.concurrent.locks包里的LockSupport类提供了解决这个问题的方案。方法LockSupport.park阻塞当前线程,直到LockSupport.unpark被调用,(假唤醒也是允许的)unpark方法的调用不会计数,所以在一个park方法前多个unpark,也只唤醒一个park方法阻塞的线程。此外,这适用于每个线程,而不是每同步。这意味着一个线程在一个新的同步器上调用了park方法,可能会立即返回,因为有之前剩余的unpark,然而,在一个unpark的情况下,它的下一次调用将被阻塞。虽然有可能显式地清除这种状态,这是不值得这样做。这使得当需要多次调用park的时候更有效。
同样的机制也一定程度上被Solaris9线程库[11]、 win32的“事件消费机制”、和Linux的NPTL线程库等使用。每一个对应到在最常见的Java平台上运行也是有效的。
(目前Sun HotSpot JVM的实现参考了Solaris和Linux上实际使用的一个pthread condvar机制来兼容现有的设计。)park方法同样支持一个可选项,比如一个相对或则绝对的超时时间(timeout),并且集成了JVM 中Thread.interrupt支持 - 线程unparks的时候可以中断它。
原文见附件
下一篇:http://caoyaojun1988-163-com.iteye.com/admin/blogs/1290759
本站支持 pay for your wishes
相关推荐
多线程编程是 Java 语言中的一大特点,java.util.concurrent 多线程框架则是该特点的体现。该框架提供了多种机制,包括线程池、并发集合、同步器、lock 等,以便开发者更方便地编写高效、可靠的多线程程序。 在 ...
Java.util.concurrent是Java 5.0引入的一个重要包,它为多线程编程提供了一组高级并发工具。这个包的设计者是Doug Lea,它的出现是JSR-166的一部分,也被称作Tiger更新。Java.util.concurrent的引入是为了解决传统...
`java.util.concurrent`包中的`AbstractQueuedSynchronizer`框架为Java开发者提供了一个强大的工具箱,使得他们能够高效地实现各种同步器。通过对同步状态的有效管理、阻塞和唤醒机制的优化以及灵活的扩展性设计,...
文档标题“java.util.concurrent同步器框架”和描述“Doug Lea的java.util.concurrent同步器框架”表明本文将探讨由Doug Lea所撰写的关于Java并发编程中同步器框架的内容。文档中提到了AbstractQueuedSynchronizer类...
AQS(AbstractQueuedSynchronizer)是Java.util.concurrent包中同步器的基础框架,它的核心设计思想与实现方法在Doug Lea先生的这篇论文中有详细的介绍。论文详细阐述了AQS框架的原理、设计、实现、应用以及性能等...
在Java并发编程中,`java.util.concurrent`(简称JUC)提供了丰富的类和接口,如Executor框架、线程池、并发集合、同步工具类等。这些工具使得程序员能够更方便地管理线程,避免了传统的锁和同步机制带来的复杂性和...
Java.util.concurrent(JUC)是Java平台中的一个核心包,专门用于处理多线程并发问题。这个包包含了大量的工具类和接口,极大地简化了并发编程的复杂性,提高了程序的性能和可伸缩性。本测试源文件主要是针对JUC并发...
总之,`java.util.concurrent` 提供的工具使得并发编程变得更加容易和高效,是 Java 并发编程的基石,无论是对于初学者还是经验丰富的开发者,理解和掌握这个包都是非常重要的。通过熟练运用这些工具,开发者可以...
在Java编程语言中,`java.util.Vector`是一个重要的集合类,它是`ArrayList`的早期版本,提供了线程安全的动态数组实现。这篇文章将对`Vector`类进行详细的总结,包括其特点、用法以及与`ArrayList`的区别。 1. **...
在实际项目中,结合`java.util.concurrent`包与其他工具,比如Spring框架的ThreadPoolTaskExecutor,可以构建出高效、可扩展的并发解决方案。同时,持续学习和实践是提升并发编程能力的关键,不断探索新的并发模式和...
8. **Fork/Join框架**: `java.util.concurrent.ForkJoinPool` 和 `RecursiveTask` 或 `RecursiveAction` 用于并行执行可拆分任务,适合大量计算工作。 9. **ScheduledExecutorService**: 提供定时及周期性任务的...
23. **`java.util.concurrent.locks.Lock`** 和 **`java.util.concurrent.locks.ReentrantLock`**: 锁机制,用于线程同步。 24. **`java.util.ArrayList`**: 用于创建堆栈、队列和双端队列的实现,如`ArrayDeque`。...
`java.util.Queue`接口及其实现如ArrayDeque、LinkedList(作为Queue的实现),以及`java.util.concurrent`包下的并发队列如LinkedBlockingQueue、ConcurrentLinkedQueue等,提供高效的数据同步和处理机制。...
因此,在更复杂的场景下,我们通常会使用`ScheduledExecutorService`,它是`java.util.concurrent`包的一部分。下面是如何使用`ScheduledExecutorService`的例子: ```java import java.util.concurrent.Executors;...
`Thread`类允许创建和管理独立执行的线程,而`java.util.concurrent`包包含了许多高级并发工具,如线程池、未来结果(Future)和同步器(Semaphore)。 4. **网络编程**:`java.net`包提供了网络通信的接口和类,如...
3. **同步容器**: `java.util.concurrent`包中的`BlockingQueue`接口及其实现(如`ArrayBlockingQueue`, `LinkedBlockingQueue`)是线程安全的数据结构,适用于生产者-消费者模型。此外,还有`ConcurrentHashMap`,...
java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 ...
- `java.util.concurrent` 包:包含线程池、并发容器、同步工具类,如`ExecutorService`、`CountDownLatch`、`CyclicBarrier`等。 7. **反射工具类**: - `java.lang.reflect` 包:提供反射API,可以在运行时动态...