《java并发编程实战》读书笔记11--构建自定义的同步工具,条件队列,Condition,AQS
第14章 构建自定义的同步工具
本章将介绍实现状态依赖性的各种选择,以及在使用平台提供的状态依赖机制时需要遵守的各项规则。
14.1 状态依赖性的管理
对于并发对象上依赖状态的方法,虽然有时候在前提条件不满足的情况下不会失败,但通常有一种更好的选择,即等待前提条件为真。依赖状态的操作可以阻塞知道可以继续执行。内置的条件队列可以使线程一直阻塞。
首先介绍如何通过轮询与休眠等方式来(勉强地)解决状态依赖性问题。可阻塞的状态依赖操作的形式如程序14-1所示。

构成前提条件的状态变量必须由对象锁来保护,从而使它们在测试前提条件的同时保持不变。如果前提条件尚未满足,就必须释放锁,以便其他线程可以修改对象的状态,否则,前提条件就永远无法变成真。在再次测试前提条件之前,必须重新获得锁。
在生产者-消费者的设计中经常会使用想ArrayBlockingQueue这样的有界缓存。在有界缓存提供的put和take中都包含一个前提条件:不能从空缓存中取出元素,也不能将元素放入已满的缓存中。当前提条件为满足时,依赖状态的操作可以抛出一个异常或返回一个错误状态,也可以保持阻塞直到对象进入正确的状态。
接下来介绍有界缓存的几种实现,其中采用不同的方法来处理前提条件失败的问题。


14.1.1 示例:将前提条件的失败传递给调用者

程序清单14-4给出了对take的调用——并不是很漂亮,尤其是当程序中有许多地方都调用put和take方法时。(因为调用者要不停的捕获处理异常,并且在每次缓存操作时都需要重试)


如果将状态依赖性管理交给调用者管理,那么将导致一些功能无法实现,例如维持FIFO顺序,由于迫使调用者重试,因此失去了“谁先到达”的信息。

14.1.2 示例:通过轮询与休眠来实现简单的阻塞



这种通过轮询与休眠方式来实现阻塞操作的过程需要付出大量的努力。如果存在某种挂起线程的方法,并且这种方法能够确保当某个条件成真时线程立即醒来,那么将极大地简化实现工作。这正是条件队列实现的功能。
14.1.3 条件队列
使得一组线程(称之为等待线程)能够通过某种方式来等待特定的条件变成真。正如每个java对象都可以作为一个锁,每个对象同样可以作为一个条件队列。

Object.wait会自动释放锁,并请求操作系统挂起当前线程,从而使其他线程能够获得这个锁并修改对象的状态。当被挂起的线程醒来时,它将在返回之前重新获得锁。


14.2 使用条件队列
条件队列使构建高效以及高响应的状态依赖类变得更容易,但同时也很容易被不正确的使用。
14.2.1 条件谓词
想要正确的使用条件队列,关键是找出对象在哪个条件谓词上等待。条件谓词是使某个操作成为状态依赖的前提条件。在有界缓存中,只有当缓存不为空时,take方法才能执行,否则必须等待。对take方法说,它的条件谓词就是“缓存不为空”。

14.2.2 过早唤醒
wait方法的返回不一定意味着线程正在等待的条件谓词已经变成真了。

当执行控制重新进入调用wait代码时,它已经重新获取了与条件队列相关联的锁。但现在条件谓词未必为真,或许,在发出notifyAll时,条件谓词可能已经变成真,但在重新获取锁时将再次变为假。所以,每当线程从wait中唤醒时,都必须再次测试条件谓词。


14.2.3 丢失的信号


14.2.4 通知
在缓存变为非空时,为了使take解除阻塞,必须确保在每条缓存变为非空的代码路径中都发出一个通知。在BoundedBuffer中,只有一条代码路径,即在put方法之后。因此,put在成功地将一个元素添加到缓存后,将调用notifyAll。同样,take在移除一个元素后也将调用notifyAll,向任何正在等待“不为满”条件的线程发出通知:缓存已经不满了。在条件队列API中发出通知的方法,即notify和notifyAll。无论调用哪一个,都必须持有与条件队列对象相关联的锁。在调用notify时,JVM会从这个条件队列上等待的多个线程选择一个来唤醒(wait挂起的是线程,不是条件队列对象),而notifyAll则会唤醒所有在这个条件队列上等待的线程。由于多个线程可以基于不同的条件谓词在同一个条件队列(对象)上等待,因此如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号丢失的问题。

只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll:
i.所有等待线程的类型都相同 ii.单进单出(在条件变量上的每次通知,最多只能唤醒一个线程来执行)

14.2.5 示例:阀门类

在await中使用的条件谓词比测试isOpen复杂的多。因为如果当阀门打开时有N个线程正在等待它,那么这些线程都应该被允许执行。然而,如果阀门在打开后又非常快速的关闭了,并且await方法只检查isOpen,那么所有线程都可能无法释放:当所有线程收到通知时,将重新请求锁并退出wait,而此时的阀门可能已经再次关闭了。
14.2.6 子类的安全问题
14.2.7 封装条件队列
通常,我们应该把条件队列封装起来,因而除了使用条件队列的类,就不能在其他地方访问它。
14.2.8 入口协议与出口协议
14.3 显示的Condition对象

之所以要使用显示的Condition对象是因为内置条件队列的一些缺陷:

一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁关联一样。要创建一个Condition,可以在相关联的Lock上调用Lock.newCondition方法。正如Lock比内置锁提供了更加丰富的功能,Condition同样比内置条件队列提供了更丰富的功能:在每个锁上可存在多个等待、条件等待可以是可中断的或不可中断的、基于时限的等待,以及公平的或非公平的队列操作。

注意:在Condition对象中,与wait、notify和notifyAll方法对应的分别是await、signal和signalAll。但是,Condition对Object进行了扩展,因而它也包含wait和notify方法。一定要确保使用正确的版本。
下面来通过Condition实现有界缓存:


14.4 Synchronized剖析


ReentrantLock和Semaphore在实现时都使用了一个共同的基类,即AbstractQueuedSynchronized(AQS),这个类也是许多其他同步类的基类。AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。
14.5 AbstractQueueSynchronized
在基于AQS构建的同步容器中,最基本的操作包括各种形式的获取操作和释放操作。获取操作是一种依赖状态操作,并且通常会阻塞。当使用锁或信号量时,“获取”操作的含义就很直观,即获取的是锁或者许可,并且调用者可能会一直等待直到同步容器类处于可被获取的状态。在使用CountDownLatch时,“获取”操作意味着“等待并知道闭锁到达结束状态”,而在使用FutureTask时,则意味着“等待并直到任务已经完成”。“释放”并不是一个可阻塞的操作,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行。AQS负责管理同步容器类中的状态,它管理了一个整数信息,可以通过getState、setState以及compareAndSetState等protected类型方法来进行操作。这个整数可以用于表示任意状态。例如,ReentrantLock用它来表示所有者线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态。在同步容器类中还可以自行管理一些额外的状态变量,例如,ReentrantLock保存了锁的当前所有者信息,这样就能区分某个获取操作是重入的还是竞争的。

如果某个同步容器支持独占的获取操作,那么需要实现一些保护方法,包括tryAcquire、tryRelease和isHeldExclusively等,而对于支持共享获取的同步器(如Semaphore),则应该实现tryAcquireShared和tryReleaseShared等方法。AQS中的acquire、acquireShared、release和releaseShared等方法都将调用这些方法在子类中带有前缀try的版本来判断某个操作是否能执行。

一个简单的闭锁
程序清单14-14中的OneShotLatch是一个使用AQS实现的二元闭锁。它包含两个共有方法:await和signal,分别对应获取操作和释放操作。

在OneShotLatch中,AQS状态用来表示闭锁状态——关闭(0)或者打开(1)。await方法调用AQS的acquireSharedInterruptibly,然后接着调用OneShotLatch中的tryAcquireShared方法。在tryAcquireShared的实现中必须返回一个值来表示该操作能否执行。acquireSharedInterruptibly方法处理失败的方式是把这个线程放入等待线程队列中。
14.6 java.util.concurrent同步器类中的AQS
14.6.1 ReentrantLock
ReentrantLock只支持独占方式的获取操作,因此它实现了tryAcquire、tryRelease和isHeldExeclusively,程序14-15给出了非公平版本的tryAcquire


ReentrantLock将同步状态用于保存锁获取操作的次数,并且还维护一个owner变量来保存当前所有者线程的标识符,只有在当前线程刚刚获取到锁,或者正要释放锁的时候,才会修改这个变量。在tryRelease中检查owner域,从而确保当前线程在执行unlock操作之前已经获取了锁:在tryAcquire中将使用这个域来区分获取操作是重入的还是竞争的。由于状态可能在检查后被立即修改,因此tryAcquire使用compareAndSetState来原子地更新状态。
14.6.2 Semaphore与CountDownLatch

14.6.3 FutureTask
14.6.4 ReentrantReadWriteLock

小结:

《java并发编程实战》读书笔记11--构建自定义的同步工具,条件队列,Condition,AQS的更多相关文章
- Java并发编程实战 第14章 构建自定义的同步工具
状态依赖性 定义:只有满足特定的状态才能继续执行某些操作(这些操作依赖于固定的状态,这些状态需要等待别的线程来满足). FutureTask,Semaphroe,BlockingQueue等,都是状态 ...
- Java并发编程实战 读书笔记(一)
最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一 线程与进程 进程与线程的解释 个人觉 ...
- Java并发编程实战 读书笔记(二)
关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...
- 《java并发编程实战》笔记
<java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为: Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...
- Java多线程编程实战读书笔记(一)
多线程的基础概念本人在学习多线程的时候发现一本书——java多线程编程实战指南.整理了一下书中的概念制作成了思维导图的形式.按照书中的章节整理,并添加一些个人的理解.
- Java并发编程实战 第5章 构建基础模块
同步容器类 Vector和HashTable和Collections.synchronizedXXX 都是使用监视器模式实现的. 暂且不考虑性能问题,使用同步容器类要注意: 只能保证单个操作的同步. ...
- Java并发编程实践读书笔记(2)多线程基础组件
同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...
- Java并发编程实践读书笔记(5) 线程池的使用
Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到 ...
- Java并发编程艺术读书笔记
1.多线程在CPU切换过程中,由于需要保存线程之前状态和加载新线程状态,成为上下文切换,上下文切换会造成消耗系统内存.所以,可合理控制线程数量. 如何控制: (1)使用ps -ef|grep appn ...
随机推荐
- 20165218 2017-2018-1《Java程序设计》第二周学习总结
20165218 2017-2018-1 <Java程序设计>第2周学习总结 教材学习内容总结 Ch2 基本数据类型与数组 Unicode字符集之中所有都叫做"字母", ...
- 项目管理---git----快速使用git笔记(一)------git的简单介绍
最近svn代码管理服务器崩溃了,切换到git来运作. 经过几天的使用,感觉很不错. 尤其是代码合并到正式版本之前 可以对代码进行 code review. 这样能很好的保证团队的代码质量和一些重复代码 ...
- 如何区别java中的public,protected,default,private
================Public====================== 1>首先我们介绍public关键字,从字面意义上出发,public意为公共的,可见它的访问权限是很宽松的 ...
- Codeforces Round #169 (Div. 2) A水 B C区间更新 D 思路
A. Lunch Rush time limit per test 2 seconds memory limit per test 256 megabytes input standard input ...
- [freemarker篇]06.超级强大的自定义指令
Freemarker的自定义指令是很强大的,非常强大,在之后的教程中我会简单的做一个示例,让大家对其有所了解!如果做Freemarker编程,请好好看看API手册,可以说里面的内容很多!也是一门独立的 ...
- [LeetCode] 14. Longest Common Prefix ☆
Write a function to find the longest common prefix string amongst an array of strings. 解法: 广度优先搜索:先比 ...
- JavaScript字符串、数组操作总结一
1.将数组转换成字符串 例子: var arr=[1,2,3,4,5,6]; var str=arr.join('|'); str输出为 “1|2|3|4|5|6” 2.数组indexOf()方法 ...
- Java 中 给静态方法 添加泛型 (static <T>)
今天在用到static方法的时候.想要用泛型.结果不能通过编译. 上网查了一下.其具体写法如下:
- .net core Fundamentals
• Application Startup 應用程序啟動 • Middleware 中間件 • Working with Static Files 靜態文件 • Routing 路由 • URL Re ...
- 32岁白发菜鸟拿2.6万年薪苦熬10年 NBA首秀便惊艳世人 科比书豪纷纷为他点赞
这是一场普通的常规赛——斯台普斯球馆,湖人的赛季第81场.比赛的结果也没什么意外:客场作战的火箭106-99带走胜利.然而,这一场的斯台普斯却成了欢乐的海洋,现场甚至喊出了MVP的呼声,这份赞誉,送给 ...