第二章,线程安全性
“共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。一个对象是否需要是线程安全的,取决于它是否被多个线程访问。
1) 不在线程之间共享该状态变量
2) 将状态变量修改为不可变的变量
3) 在访问状态变量时使用同步
在任何情况中,只有当类中仅包含自己的状态时,线程安全类才是有意义的。
有时候,面向对象中的抽象和封装会降低程序的性能(尽管很少有开发人员相信),但在编写并发应用程序时,一种正确的编程方法就是:首先例代码正确运行,然后再提高代码的速度。以测量结果来指导优化。
2.2.1 竞态条件(Race Condition)
在并发编程中,这种由于不恰当的执行时序而出现的不正确的结果是一种非常重要的情况。
2.2.2示例:延迟初始化中的竞态条件
2.2.3复合操作
Java确保原子性的内置机制:加锁。
使用线程安全类:
2.3.1内置锁(synchronized)
相当于互斥锁。
2.3.2重入
如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重人的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获计数值置为1。
2.4用锁来保护状态
对象锁与其状态之间没有内在的关联。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,只能阻止其他线程获得同一个锁。
之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象。(你需要自行构造加锁协议或者同步策略来实现对共享状态的安全访问)
第三章 对象的共享
3.1可见性
3.1.1失效数据
当读线程read值时,取的是未同步的数据。
3.1.2非原子性64位操作
JVM允许将64位读、写操作分为两个32位的操作。当读取非volatile的long变量时,如果读写在不同的线程,在不考虑失效数据时,多线程使用共享可变变量Long时不安全的。除非用volatile声明,或用锁保护。
3.1.4 volatile
确保变量的更新操作通知到其他线程。Volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量总返回最新写入的值。
3.2发布与逸出
发布:例对象能够在当前作用域之外的代码中使用。
逸出:当某个不应该发布的对象被发布时称为逸出。
States超出了作用域
内部类的实例中包含了对ThisEscape实例的隐含引用。
3.3线程封闭(Confinement)
仅在单线程中访问数据。
应用:Swing JDBC。
JDBC:在Connection对象返回之前,连接池不会再将它分配给其他线程。这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中。
程序员要负责确保封闭在线程中的对象不会从线程中逸出。
3.3.1 ad-hoc线程封闭
指:维护线程封闭性的职责完全由程序实现来承担。
脆弱,不建议使用。应该使用更强的线程封闭技术(如:栈封闭或ThreadLocal类)
3.3.2栈封闭
只能通过局部变量才能访问对象。
3.3.3 ThreadLocal类(规范方法)
使线程中的某个值与保存值的对象关联起来。ThreadLocal对象通常用于防止对可变的单实例变量或僵尸变量进行共享。
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,避免每次执行重新分配临时对象。
3.4不变性
使用不可变对象(ImmutableObject).
在不可变对象内部仍可以使用可变对象来管理它们的状态。
3.4.1 Final域
如果final域引用的对象是可变的,该对象是可以修改的。
3.4.2 示例:使用volatile类型发布不可变对象
对于访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中消除。如果是可变对象,使用锁。
3.5.3安全对象的正确发布
1)在静态初始化函数中初始化一个对象引用。
2)将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
3)将对象的引用保存到某正解构造对象的final类型域中。
4)将对象的引用保存到由锁保护的域中。
3.5.4事实不可变对象(Effectively Immutable Object)
在没有额外的同步情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
3.5.6安全地共享对象
在并发程序中使用和共享对象时,可以使用一些实用的策略。包括:
线程封闭
只读共享(不可变对象和事实不可变对象)
线程安全共享:多个线程可以通过对象的公有接口进行访问而不需要进一步同步。
保护对象:只能通过持有特定的锁来访问。(包括封闭在其他线程安全对象中的对象)
第四章 对象的组合(P60)
4.2实例封闭
将数据封闭在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
4.2.1 Java监视器模式
从线程封闭原则及其逻辑推论可以得出Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。Eg: Vector和Hashtable。
Java监视器模式仅仅是一种编写代码的约定:对于任何一种锁对象,只要自始至终都使用该锁对象,都可以用来保护对象的状态。
使用私有的锁对象而不是对象的内置锁。
4.4在现有的线程安全类中添加功能
“扩展”方法比直接将代码添加到类中更加脆弱。
4.4.1 客户端加载机制
ListHelper的锁和 list的锁不一致。
4.4.2组合
客户代码不会再直接使用这个对象,而只能通过ImprovedList来访问它。
4.5将同步策略文档化
维护人员查阅文档来理解其中实现策略,避免在维护过程中破坏安全性。然而,通常人们从文档中获取的信息却少之又少
SimpleDateFormate不是线程安全的!但JDK1.4前未提到。
第五章基础构建模块
5.1同步容器类
包括Vector和Hashtable。及Collections.synchronizedXXX.
每个方法首先获得数组大小,然后通过结果获取或删除最后一个元素。
使两个方法成为原子操作。
如果不希望在迭代期间对容器加锁,那么一种替代方法就是“克隆”容器,并在副本上进行迭代。(克隆过程需要加锁)
5.2并发容器
BlockingQueue扩展了Queue增加了可阻塞的插入和获取等操作,如果队列为空,那么获取元素的操作一直阻塞,直到队列中出现一个可用元素,如果已满,那插入一直阻塞,直到有可用空间。适用于“生产者—消费者”设计模式中。
5.2.1 ConcurrentHashMap
它并将每个方法都在同一个锁上同步并使每次只能有一个线程访问容器。
使用粒度更细的加锁机制——分段锁(Lock Striping)。
它提供的迭代器不会抛出ConcurrentModificationException。
只有需要独占访问时使用HashMap而放弃使用它。
5.2.3 CopyOnWriteArrayList
替代同步Set。线程安全性在于,只要正确发布一个事实不可变对象,那么在访问该对象时就不再需要进一步同步。每次修改都创建并重新发布一个新的容器副本,从而实现可变性。
5.3 阻塞队列和产生者—消费者模式
阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。
在构建高可靠应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。
虽然生产者和消费者代码彼此解耦,但行为通过共享工作队列间接耦合在一起。通常不会为工作队列大小设置边界。但这将导致在之后需要重新设计系统架构。
尽早通过阻塞队列在设计中构建资源管理机制——越早就越容易。
BlockingQueue的一个实现是SynchronousQueue。实际它不是一个真正的队列,因为它不会为队列中的元素维护存储空间,它维护一组线程,这些线程在等待着把元素加入或移出队列。
以洗盘子为喻:相当于没有盘架,直接将洗好的盘子直接放入下一个空闲的烘干机中,从而降低了将数据从生产者移动到消费者的延迟。所以put 和take会一直阻塞,直到有另一个线程已准备好参与到交付过程中。
5.3.3 双端队列与工作密取
Deque与BolckingDeque。双端队列。实现:ArrayDeque和LinkedBlockingDeque。
每个消费者都有各自的双端队列,如果消费者完成了双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。例如在垃圾回收阶段对堆进行标记。
5.4阻塞方法与中断方法
阻塞:等待I/O操作结束、等待获得一个锁。
中断:一种协作机制。注意:一个线程不能强制其他线程停止正在执行的操作而去执行其他的操作。当A中断B,仅仅是要求B在执行到某个可以暂停的地方停止。
InterruptedException:两种选择:把它传递给方法的调用者;恢复中断。
5.5 同步工具类
根据其自身的状态来协调线程的控制流。
包括:阻塞队列、信号量(Semaphonre)、栅栏(Barrier)、闭锁(Latch)。
5.5.1闭锁
可以延迟线程的进度直到其到达终止状态。用来确某些活动直到其他活动都完成后继续执行:
确保需要的资源都被初始化后缕缕执行。
确保依赖的服务启动之后才启动
等待其他参与者。
实现:CountDownLatch,灵活的闭锁实现。
5.5.2 FutureTask
闭锁的一种。可生成结果的Runnable。处于三种状态:Waitingto Run 、Running 、Complated。
FutureTask在Executor框架中表示异步任务,还可以表示一些时间较长的计算。
5.5.3信号量
计数信号量(CountingSemaphone):控制同时访问某个特定资源的操作数量。或者同时执行某个指定操作的数量。还可用来实现资源池、容器边界。
Semaphonre中管理一组虚拟许可permit。执行操作先获得许可,执行完毕释放许可。
二级信号量:初始值为1的Semaphore,用作互斥体。具备不可重入的加锁语义。
5.5.4 栅栏
Barrier类似闭锁,阻塞一组线程直到某事件发生。
Exchanger,Two-PartyBarrier。例如当一个线程向缓冲区写数据,另一个读数据。可以使用Exchanger来汇合,并将满的缓冲区与空的缓冲区交换。使用Exchanger即把这两个对象安全地发布给另一方。
5.6构建高效且可伸缩的结果缓存
简单的缓存可能会将性能瓶颈转变为可伸缩性瓶颈。
以下程序首先检查某个相应的计算已经开始,如果没启动,创建一个FutureTask,并注册到Map中,然后启动计算;如果已经启动,那么等待现有计算结果。
改进:将cache.put(arg,ft)改来catche.putifAbsent(arg,ft);
当缓存的是Futured而不是值时,将导致缓存污染(Cache Pollution)
第一部份小结
可变状态是至关重要的。
尽量将域声明为final类型,除非需要可变的。
不可变对象一定是线程安全的。
封闭有助于管理复杂性。
用锁来保护每个可变变量。
当保护同一个不变性条件中的所有变量时,要使用同一个锁。
在执行复合操作期间,要持有锁。
不要故作聪明地推断出不需要使用同步。
在设计过程中考虑线程安全。
同步策略文档化。