1.线程安全
线程安全的代码,本质上就是管理对状态的访问。(指共享的、可变的状态)(一个对象的状态:就是指它的数据)
线程的安全真正要做的:在不可控制的并发访问中保护数据。
线程安全取决于程序中如何使用对象,而不是对象完成了什么。
- 无论如何,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。
1.1线程安全性
“线程安全性” 关键在于你程序的 “正确性” 。(在并发环境中的隐患不会多于单线程环境下的隐患)
正确性意味着一个类与它的规约保持一致。
良好的规约定义了用于强制对象状态的不变约束,以及描述操作影响的后验条件。
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方法代码时不必作其它的协调,这个类的行为仍然是正确了,那么称这个类是线程安全的。
对于线程安全类的实例进行顺序或并发的一系列操作,多不会导致实例处于无效状态。
1.2原子性
在多线程中,进行离散操作,会造成数据的计算错误。
如果用于生成序列或者对象唯一标识符,可能会导致严重的数据完整性的问题。
出现错误结果的可能性对于并发程序而言非常重要。(竞争条件)
数据结果的状态衍生自它先前的状态。
1.2.1竞争条件
当计算的正确性依赖于运行时中相关的时序或者多线程的交替时,会产生竞争条件。
最常见的竞争条件:“检查再运行”。(指使用潜在的过期观察值来作决策或执行计算)
例:你观察到一些事情为真(文件x不存在),然后基于你的观察去执行一些动作(创建文件x);但但事实上,从观察到执行操作的这段时间内,观察的结果可能已经无效了(有人在此期间创建了文件x),从而引发错误。
1.2.2原子操作和复合操作
假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作:该操作对于所有的操作,包括它自己,都满足前述状态。
将“检查再运行”操作(如懒汉式初始化)和 读-改-写(如自增) 操作的全部执行过程看作是复合操作。
当只向无状态类中加入唯一的状态元素,而这个状态完全被线程安全的对象所管理,那么新的类仍然是线程安全的。
1.3锁
为什么需要锁?
线程安全性的定义要求无论是多线程中的时序或交替操作,倒要保证不破坏那些不变约束。当以恶不变约束涉及多个变量时,变量间不是彼此对的,某个变量的值会制约其它几个变量的值。因此,更新一个变量时,要在同一原子操作中更新其它几个。
为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。
1.3.1内部锁
Java提供了强制原子性的内置锁机制:synchronized 块。synchronized 块有两部分:锁对象的引用,锁保护的代码块。synchronized 方法的锁,就是该方法所在的对象本身。每个Java对象都可以隐式地扮演一个用于同步的锁的角色,这些内置的锁被称为内部锁或监视器锁。内部锁再Java中扮演了互斥锁的角色,意味着至多只有一个线程可以拥有锁。
1.3.2重进入
重进入:当线程在试图获得它自己占有的锁时,请求会成功。
为什么需要重进入?
//如果没有重进入,下面这段代码将会死锁。//在持有了子类锁后,依旧在申请子类锁对象public class Widget {public synchronized void doSomething() {...}}public class LoggingWidget extends Widget {public synchronized void doSomething() {System.out.println(toString() + ": calling doSomething");super.doSomething();}}
内部锁是可重进入的。
重进入意味着锁的请求是基于“每线程”,而不是基于“每调用”的。
1.4用锁来保护状态
操作**共享状态**的**复合操作**必须是**原子的**。复合操作会再完成的运行期间占有锁。
对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们成这个变量是由这个锁保护的。
获得了与对象关联的锁**不能**阻止其它线程访问这个对象,只能阻止其它线程再获得相同的锁。一种常见的锁规则是**在对象内部封装所有的可变状态**,通过对象的内部锁来同步任何访问可变状态的代码路径,保护它再并发访问中的安全。(例如:Vector和collection类)。
对于每一个涉及多个变量的不变约束,需要同一个锁来保护其所有的变量。(这样才可以在一个单一的原子操作中访问或更改它们,从而保证了不变约束)
1.5活跃度和性能
如果不停的用内部锁去保护每一个变量,这种简单粗糙的方法虽然使我们重获安全性,但是代价高昂。
我们需要尽可能的去平衡简单性与并发性。请求和释放锁的操作也需要开销,所以不能将synchronized 块分解的过小,可不能过大。有时候简单性与性能会彼此冲突,我们需要找到一个合理的平衡。
当使用锁的时候,应该要清楚块中的代码的功能,以及它的执行过程是否会很耗时,无论是做运算密集型的操作,还是执行一个可能存在潜在阻塞的操作,**如果线程长时间地占有锁,就会引起活跃度与性能风险的问题**。(例如:网络和控制台I/O)
