`

JAVA.util.concurrent 同步框架(翻译二)

阅读更多

 

接上一篇:http://caoyaojun1988-163-com.iteye.com/admin/blogs/1279097

 

3.3 队列

     框架的核心是维护阻塞线程的队列,队列的策略是先进先出(FIFO),因此,该框架不支持基于优先级的同步

      一直以来,对于同步队列的最合适的选择是无阻塞的数据结构有不小的争议,这种数据结构本身并不需要使用较低级别的锁来构造。其中有两个主要的观点:Mellor-Crummey与Scott的变体锁(MCS) [9]和 Craig, Landin, 和Hagersten的变体锁 (CLH)[5][8][10].从历史上看,CLH锁仅仅被用于自旋锁,然而,在同步框架中使用它们比MCS似乎更适合,因为他们更容易处理取消和超时,因此我们选择它作为基础。虽然最终的设计远远偏离了CLH的设计结构;

     CLH队列与经典的队列不同,其入队和出队操作与使用锁是紧密联系在一起的。它是一个链表,通过两个可原子更新的节点头部和尾部访问队列,双方最初都指向一个虚拟节点;


     一个新的节点入队列的时候,使用下面的原子操作:

 

 

do { 
    pred = tail;
} while(!tail.compareAndSet(pred, node));

 

 

     每个节点的释放状态域保存其前一个节点的地址。所以,一个自旋锁(spinlock)的“自旋”的样子如下:

 

 

while (pred.status != RELEASED) ; // spin

 

 

     当自旋简单的设置了head域指向node时,出队列(dequeue)操作如下:

 

 

head = node;

     CLH锁的优势是入队和出队速度快,无锁,并畅通无阻(即使在竞争的情况下,插入操的线程永远获得执行的权限),并且在检测是否有线程在排队是很快(只需要检测头尾是否相同)、释放状态是分散的,可以减少内存冲突;

 

     CLH锁的原始版本并不是双向连接。自旋锁里,PRED变量作为一个域来保存。然而, Scott 和Scherer[10]展示了,通过明确地维护节点里的pre域,CLH锁是可以处理超时和其他形式的取消的。如果一个节点的前一个节点被取消,该节点可以滑动使用前一个节点的状态域;

     要使用CLH队列构建阻塞同步器,主要的修改点是:一个节点必须提供一种有效的方式找到下一个节点。自旋锁的时候,由于仅仅在它被下一个节点通知的时候在改变它的状态;所以找个下一个节点的链接是不必要的;但在阻塞同步器中,一个节点需要明确地唤醒(unpark)它的下一个节点。

     AbstractQueuedSynchronizer队列节点包含指向下一个节点的域。但因为没有合适的技术可以对双向链表节点实现无锁的原子插入,即便使用compareAndSet。所以这个连接的操作并不包含在原子插入的操作里面;在插入操作后,它被简单地赋值,操作如下:

 

pred.next = node;

找到下一节点的路径只是作为一个优化的路径。如果一个节点的下一个节点通过next域判断不存在(或者取消),总是从列表的尾部开始使用PRED域做反向遍历准确检查是否真的不存在。

 

     第二个修改点是使用每个节点的状态域来控制阻塞而不是自旋。在同步框架中,一个队列中的线程,仅仅在它通过了一个定义在具体子类中tryAcquire方法,获取到锁才返回;这样仅仅一个“释放”位就不够了。同时仍然需要控制,以确保活动线程只有当它在队列的头部是才能调用tryAcquire;虽然在这种情况下,它任然可能会获取失败,并(重新)阻塞。这个可以通过检查当前节点的前一个节点是头节点来确认,而不需要查看每个节点的状态标志;与自旋锁不同的是,读头节点操作没有很多内存冲突。当然,取消状态仍然必须保存在状态域里面。

     队列节点的状态域也用于避免不必要的park和unpark调用。虽然他们可以避免在Java、JVM和操作系统之间交互开销,比阻塞原语快。一个线程调用park之前,先设置一个“信号”位,然后调用park前再次重新检查同步和节点状态。一个释放线程清除状态。这样可以减少无谓地尝试,这些block操作的开销是不值得的,特别是浪费时间去与其他竞争线程竞争获取锁,同时这也避免了释放线程确定其继任执行者的时间,因为继任者已经设置的信号位;如果信号没有一起被取消,就避免了必须遍历多个节点,直到next域为null的开销。

     也许用于同步框架的CLH变种锁和其他场景之间的主要区别是,它的垃圾回收机制通过管理节点的实现,从而避免了复杂性和开销。然而,当他们永远不会再用到的时候GC还负责讲链接到的域赋值为null,通过简单的出队列操作即可实现,否则,未使用的节点仍然可以访问,致使他们无法收回。

     当然还有一些优化调整,包括延迟初始化:CLH队列在第一次竞争发生时才初始话所需的虚拟节点,这些详细描述在J2SE1.5版本的源代码的文档里。

    忽略这些细节,实现一个具有基本操作(互斥,不可中断,超时)的一般形式是:

 

if (!tryAcquire(arg)) {
    node = create and enqueue new node;//创建一个新的节点,入队列
    pred = node's effective predecessor;//pred 执行当前节点的前一个有效节点
    while (pred is not head node || !tryAcquire(arg)) {//当前一个节点不是头节点或者获取锁失败
        if (pred's signal bit is set)//前一个节点的信号位被设置
            park();//阻塞自己
        else
            compareAndSet pred's signal bit to true;//讲前一个节点的信号量设置位true
       pred = node's effective predecessor;//执行当前节点的前一个有效节点
    }
   head = node;
}

 

 

一个释放操作如下:

 

if (tryRelease(arg) && head node's signal bit is set) {
compareAndSet head's signal bit to false;//head节点信号位设置位false
unpark head's successor, if one exists//唤醒head节点的下个节点
}

    循环迭代的次数,取决于tryAcquire性质,因此,在没有取消的情况下,获取和释放的时间都是O(1),跨线程的开销,和任何花费在基于park的OS的线程调度都可以忽略;

 

     对取消操作的支持,主要是需要在每次循环中park操作返回之后检查中断或超时,由于超时或中断被取消的线程负责设置节点的状态,并且unparks它的后继者,所以它可能会重置链接;取消操作,可能会确定的前一个节点和后一个节点,并且重置状态,遍历的时间复杂度是O(N)(其中n是队列长度)。因为一个线程永远不会为取消而被block,所以链接和状态往往很快重新稳定。

 

3.4 条件队列

 

     同步框架提供了一个维护互斥同步的同步器和供Lock接口使用的ConditionObject的类。许多condition对象可能会关联到一个锁对象,提供经典的基于监视器(monitor)风格的等待(await),、唤醒(signal)和唤醒全部(signalAll)的操作,包括超时,以及一些检查和监测方法,

     通过操纵一些设计决策,ConditionObject类可以有效地与其他同步操作相结合。这个类仅仅支持java风格的监视器访问规则,既是仅仅当当前线程的锁的条件满足时操作才是允许的(请参阅[4]替代方案的讨论);因此,一个ConditionObject关联到ReentrantLock的方式与内置的监视器一样(通过Object.wait等),唯一的不同是方法名称,额外的功能,而事实上,用户可以在每个锁上申明多个条件。

     一个ConditionObject与同步器使用相同的内部队列节点,但它们维护在一个单独的条件队列中;唤醒操作通过从条件队列转移到锁队列来实现;一个线程已经重新获取了锁,不需要唤醒已经唤醒的线程;

     基本等待(await)操作是:

 

create and add new node to condition queue;//新建一个node,添加到条件队列中
release lock;//释放锁
block until node is on lock queue; //阻塞直到node转移到锁队列上
re-acquire lock; //重新获取锁

    唤醒的操作是:

 

 

将条件队列的第一个节点转移到锁队列中;

     因为这些操作只有持有锁的时候才允许,因此可以使用有序链表队列(使用节点的nextWaiter域)维护条件队列;转移操作简单取出条件队列中的第一个节点,然后使用CLH的插入操作添加到锁队列;

 

     实现这些操作最复杂的地方在于处理因为await操作超时或Thread.interrupt而引起的取消操作。取消操作和唤醒几乎在同时发生,产生一个竞争,其结果符合内置监视器的规范;在修订JSR133的时候,要求如果一个中断信号在唤醒之前发生,那么等待的方法必须重新获取锁后,抛出InterruptedException。但是如果一个中断信号在唤醒之后,则该方法必须返回,不抛出异常,同时线程设置为中断状态。

     为了维持正常的顺序,队列中的节点有一个状态域记录节点是否已经转移(或者已经在执行中)。唤醒和取消都使用compareAndSet来尝试更新状态。如果一个唤醒操作竞争失败,如果存在下一个节点,将转移队列中的下一个节点。如果取消操作竞争失败,就必须中止转移,然后等待重新获取锁。后一种情况,可能引入了一个无限自旋。直到一个节点已成功转移到锁队列中,否则一个被取消的等待不能重新开始获取锁,所以必须自旋等待被唤醒的线程在CLH队列上执行compareAndSet类型的插入成功。在这里的自旋是罕见的,通过Thread.yield暗示一个理想唤醒的线程调度执行。虽然这里可以为取消插入节点实现一个帮助策略,但是这种场景是非常罕见,并且意味着增加开销。在其他所有的场景下,所有的基本机制,都不会使用自旋或yields,来保持合理的单处理器的性能。

 

原文见第一篇的附件

 

下一篇:http://caoyaojun1988-163-com.iteye.com/admin/blogs/1302936

 

本站支持 pay for your wishes

  • 大小: 10.2 KB
0
0
分享到:
评论
2 楼 wpf0310 2013-12-30  
确实有点吃力
1 楼 wpf0310 2013-12-30  
[color=orange][color=red][color=red][color=olive][/color][/color];lllll[/color][/color]

相关推荐

    JAVA_API1.6文档(中文)

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 ...

    juconcurrent:java.util.concurrent

    Java JUC的使用1.volatile关键字-内存可见性2.原子变量-CAS算法3.ConcurrentHashMap锁分段机制4.CountDownLatch闭锁5.实现Callable接口6.Lock同步锁7.Condition控制线程通信8.线程按序交替9.ReadWriteLock读写锁10....

    Java基础知识点总结.docx

    无论是工作学习,不断的总结是必不可少的。只有不断的总结,发现问题,弥补不足,才能长久的...java.util.concurrent.locks包下常用的类 326 NIO(New IO) 327 volatile详解 337 Java 8新特性 347 Java 性能优化 362

    java jdk-api-1.6 中文 chmd

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 ...

    JavaAPI中文chm文档 part2

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 ...

    Java 1.6 API 中文 New

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 java...

    java 多线程同步

    不客气地说,创建 java.util.concurrent 的目的就是要实现 Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性...

    java api最新7.0

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 java...

    [Java参考文档].JDK_API 1.6

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 java...

    JavaAPI1.6中文chm文档 part1

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 ...

    [Java参考文档]

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 ...

    J.U.C-AQS框架同步组件之闭锁CountDownLatch介绍

    CountDownLatch是在java1.5被引入的,跟它一起被引入的并发工具类还有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue,它们都存在于java.util.concurrent包下。CountDownLatch这个类能够使一个线程...

    Java中LockSupport的使用.docx

    LockSupport是JDK1.6中在java.util.concurrent中的子包locks中引入的一个比较底层的工具类,用来创建锁和其他同步工具类的基本线程阻塞原语。java锁和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通过...

    JDK_1_6 API

    java.util.concurrent.locks 为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。 java.util.jar 提供读写 JAR (Java ARchive) 文件格式的类,该格式基于具有可选清单文件的标准 ZIP 文件格式。 ...

Global site tag (gtag.js) - Google Analytics