Java并发编程实战
第一章 简介
第二章 线程安全性
无状态类总是线程安全的。
当改变状态的行为是可再拆分时候,(比如:改变一个变量:读取、改变、写入,包含了三个操作),在多线程环境下,该状态就有可能失去同步。
当一个类中存在多个状态变量时,特别是一个变量的改变依赖于另一个变量的状态时,尤其需要小心,同上面提及的情形类似,这里的关联变量在改变他们时请保持同步。
Java提供synchronized 关键字对代码块进行同步处理,但是值得注意的是,在非显示指定的情况下,该操作使用的锁是该对象本身(this)。
互斥行为只发生在同一个锁上,如果是不同的锁,则不会发生互斥,只是约定上通常使用对象锁来保证行为同步,如果各个行为使用不同的锁则不能保证同步。
持有锁的时间过长会带来活跃性或性能问题。例如:如果执行巨大耗费的操作或者I/O,则过程中一定不要使用锁,会严重影响性能。
同步的场景不仅包含互斥行为,而且包含可见变量。
第三章 对象的共享
加锁的意义不仅仅在于原子操作的互斥作用,而且可以使得对象的有效值对多个并发访问保持可见性。
早期的java虚拟机允许将64位操作分解为两个32位操作,所以请注意64位数据对象的互斥和可见性问题。
volatile 仅能保证数据的可见性,并不能保证操作的原子性。该关键字建议编译器放弃效率更高的直接从寄存器获取值,而直接从内存获取,以保证数据的最新。
发布(publish)表示数据对象已经完全准备好在构造区域之外的地方使用,该对象已经完成构造。
逸出(escape)表示数据对象在并未完全构造好的情况下被外部访问。这种情况通常会或多或少带来一些问题,请谨慎发布数据对象,避免逸出。
建议
不要在构造函数中使this 逸出,因为对象尚未构造完全。可以在构造函数中定义线程对象,但最好不要启动它,应当在随后的过程中定义start/init延迟启动该线程。
线程封闭:不共享数据,仅在单线程中访问数据,这时就不需要同步,这种方法称之为线程封闭。
方法
- ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担。(单线程的简便性胜过了该方式的脆弱性,所以仍值得推荐)
- 栈封闭:也被称为线程内部使用或者线程局部使用。
- ThreadLocal
不可变对象(满足如下三个条件)
- 对象创建以后状态不能修改
- 所有域都是final类型的
- 对象正确创建,构建期间没有this 逸出。
发布
不可变对象可以任意机制发布。
事实不可变对象需要通过安全方式发布。
可变对象不仅需要通过安全方式发布,而且必须是线程安全的,或者由某个锁保护起来。
方式
线程封闭: 对象只在某个线程内部访问。
只读共享: 对象从构造完成便不能再更改。
线程安全共享:线程安全的对象在内部实现同步,线程可以通过该对象的公有接口来访问。
保护对象: 必须持有特定的锁才能访问某对象。
第四章 对象的组合
注意依赖状态的操作,保持同步。
注意状态变量的有效性约束条件,这可能需要你添加额外的同步操作。
注意状态的所有权,分散所有权将会使状态同步和可见性变得复杂。
将非线程安全的类转化为线程安全的类,实例封闭:将非线程安全的类封装在另一个类内部,并采取同步措施。
线程封闭的一种方式:java监视器模式,将所有修改或获取类内部状态的方式封装,不对外发表状态本身,而发布访问接口,对访问接口同步。
可以使用委托:如果一个类由多个独立且线程安全的内部状态变量组成,并且所有的操作都不包含无效状态转换,则可以将类的线程安全委托给内部的状态变量。
为现有的线程安全类添加原子操作:需了解所使用的线程安全类其所使用的锁,特别是作为原有的线程安全类的客户端时,使用使用同样的锁才能保证原子性和同步。
组合是一个良好的方式: 添加原子操作,也可以将一些操作安全的委托给内部的组合线程安全对象。
将你的线程安全策略文档化,并且当某些库的文档没有明确指明某些类是否是线程安全时,不要做这样的假设。
第五章 基础构建模块
同步容器类
对非线程安全容器实例使用 toString 将会隐式调用迭代器。相同的还有hashCode、 equals、 containsAll、 removeAll、 retainAll等方法,以及把容器作为操作的构造函数,这些都会对容器进行迭代,所有这些操作都可能抛出ConcurrentModificationException。
通过并发容器来代替同步容器可以极大地提高伸缩性并降低风险:
- ConcurrentHashMap
- CopyOnWriteArrayList
阻塞队列与生产者/消费者模式
这称之为串行线程封闭, 另外还有双端队列(Deque,具体实现有:ArrayDeque和LinkedBlockingDeque)和工作密取(work stealing,当自己需要处理的队列内容为空时,可以从临近的其他对象的任务双端队列中获取任务),这些方式都能降低线程竞争,并且提高效率。
同步工具类
CountDownLatch闭锁:举例, 老师等待三个学生到来(await),学生到达时调用countDown, 等到老师等待的所有学生到达后,老师开始活动。这个过程中,老师等待多少同学是在构造闭锁(CountDownLatch)的时候确定的,并且在这个过程中,没有任何办法能改变这个值。
FutureTask: 继承自runable 接口,这也意味着可以定义有返回值的行为作为,再用这个FutrueTask构建线程,当你需要某个过程的结果时,可以预先运行这个过程,并在之后需要的时候调用FutureTask的get方法得到这个过程的返回值,如果运行已经完成,则立即返回结果,如果没有,get方法将会阻塞,直到结果返回。
Semaphore信号量: 举例,老板说我这里有三间更衣室,你们有五个人,但是你们同时只能进来三个(acquire,过程后release),其他的,排队。
CyclicBarrier栅栏:举例,我有五个兄弟,我过河了,但是我得等他们一起到之后我才能继续行动(线程使用await)。
其他
1 | ## 概述 |