要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态进行访问。 对象的状态是指存储在状态变量(实例或静态域)重的数据,对象的状态可能包括其他依赖对象的域。 「共享」意味着变量可以由多个线程同时访问,「可变」意味着变量的值在其生命周期内可以发生变化。<font color=#ff0000>数据安全性更侧重于如何防止在数据上发生不可控的并发访问</font>。 即使某个程序中省略了必要的同步机制并且通过了测试并能在随后几年内都能正确执行,但程序仍可能在某个时刻发生错误。 防止错误的三种办法:
- 不在线程见共享该状态变量
- 将状态变量修改为不可变的
- 在使用状态变量时使用同步
当设计线程安全的类时,良好的面相对象技术、不可修改性,以及明确的不变形规范都能起到一定的作用
完全由线程安全类构成的程序并不一定是线程安全的,而在线程安全程序中也可以包含非线程安全的类。
什么是线程安全性
线程安全性的定义中最重要的就是「正确性」,正确性的含义是,某个类的行为与其规范完全一致。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替之行,并且在主调代码中不需要额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
原子性
原子操作是指对于访问一个状态的所有操作,这个操作是一个以原子方式执行的操作。
竞态条件
当某个计算的正确性取决于多个线程交替执行的时序时就会发生竞态条件,从而使结果变的不可靠。 竞态条件并不总是发生错误,还需要某种不恰当的执行时序。
复合操作
为了保证线程安全性,“先检查后执行”和“读取-修改-写入”等操作必须是原子的。“先检查后执行”和“读取-修改-写入”等操作统称为符合操作。
加锁机制
要保持状态的一致性,就需要在单个原子操作中更新所有相关的变量。
Java 提供了内置锁 Synchronized 和 重入锁。这些锁的原理在后面的文章中再详细分析。
要判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,包括安全性、简单性和性能。