第二章 线程安全
1. 一个类是线程安全的,是指被多个线程访问时,类可以支持进行正确的行为。
2. 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调用和交替执行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确 的,那么称这个类为线程安全的
3. 对于线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态;
4. 线程安全的类封装了任何必要的同步,因此客户不需要自己提供;
5. 因为线程访问无状态对象的行为,不会影响其他线程访问该对象时的正确性,所以无状态对象(永远)是线程安全的。
6. 假设操作A和B,如果从执行A的线程角度看,当其他线程执行B时,要么B完全执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作是指:改操作对于所有 的操作,包括它自己,都满足前面描述的状态。
7. 为了保护状态的一致性,要在单一的原子操作中更新相互联系的状态变量。
2.3.1 内部锁
Java提供了强制原子性的内置锁机制:synchronized块。锁对象的引用,以及这个锁保护的代码块。
这些内置锁被称为内部锁(intrinsiclocks)或监视器锁(monitor locks);
内部锁在Java中扮演了互斥锁(mutualexclusion lock)的角色,以为着只有一个线程可以拥有锁。
2.3.2重进入(Reentrancy)
当一个线程请求其他线程已经占用的锁时,请求线程将被阻塞。然后内部锁是可重进入的,因此线程在试图获得它自己占有的锁时,请求会成功。
重进入方便了锁行为的封装,因此简化了面向对象并发代码的开发。
可重入:当被多个线程调用的时候,不会引用任何共享数据。
线程不安全的根源:共享数据
2.4 用锁来保护状态
操作共享状态的复合操作必须是原子的,以避免竞争条件,比如递增命中计数器(读-改-写)或者惰性初始化(检查再运行).
对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。
用对象的内部锁来保护所有的于,然而这并不是必需的。即使获得了与对象关联的锁也不能阻止其他线程访问这个对象——获得多项的锁后,唯一可以做的事情是防止其他线程再获得相同的锁。你可以构造自己的锁协议或同步策略,使你可以安全地访问共享状态,并且光转程序都始终如一地使用它们
每个共享的可变变量都需要由唯一一个确定的锁保护。而维护者应该清楚这个锁。
并不是所有数据都需要锁的保护——只有那些被多个线程访问的可变数据。
锁保护的变量,意味着每一次访问变量时都要获得该锁,确保在同一时刻只有一个线程可以访问这个变量。且每个参与到不变约束的变量由同一个锁保护。
(对于每一个设计多个变量的不变约束,需要同一个锁保护器所有的变量)
问题:既然同步可以避免竞争条件,为什么不讲每个方法都声明为Synchronized呢?
如此武断地使用同步,可能导致程序中使用的同步过多或过少。
虽然同步方法确保了不可分割操作的原子性,但是把多个操作整合到一个复合操作时,还是需要额外的锁。同时每个方法还会导致活跃度(liveness)或性能(performance)的问题。
2.5活跃度与性能
弱并发:这些请求排队等待并依次被处理。我们把这种Web应用的运行方式成为弱并发(poor concurrency)的一种表现:限制并发调用的数量,并非可用的处理器资源,而恰恰是应用程序自身的结构。
Synchronized块之外的代买独享地操作本地(基于栈的)变量,这些变量不被跨线程地共享,因此不需要同步。
原子变量可以保证单一变量的操作是原子的,然而使用synchronized块构造了原子操作。使用两种不同的同步机制会引起混淆,而且性能与安全也不能从中得到额外的好处。
决定synchronized块的大小需要权衡各种设计要求,包括安全性(决不能妥协)、简单性和性能。简单性和性能彼此冲突。(简单性与性能是相互牵扯的。实现一个同步策略时,不要过早地为了性能而牺牲简单性)
有些好使的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作器件不要占有锁。