线程安全性
要编写线程安全的代码,核心是对状态访问操作进行管理,尤其是那些可变的、共享的状态的访问,而对象的状态可以理解为存储在状态变量(实例或静态域)中的数据,当然对象的状态还可能会包含其他依赖对象的域。
什么是线程安全性
当多个线程访问一个类时,不管运行时采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要额外的同步和协同,这个类始终都能表现出正确的行为,那么这个类就称作是线程安全的。
无状态对象一定是线程安全的(不包含任何域也不包含任何引用对象的域),比如大多数的Servlet都是无状态的,只有当Servlet在处理一些请求需要保存数据时,才不是线程安全的。
从虚拟机的角度来看,无状态的对象的临时状态都是存储在线程栈中的局部变量中的,只允许当前线程访问。而不会受其他线程的影响。另外,类信息是存储在方法区中的,是共享的。类信息中包含了类的版本、字段、类名、接口、方法等信息。类的字段信息是可变的,线程的不安全性也正是因为对共享的、可变的状态的访问无法保证其正确性而引起的。所以,无状态的对象一定是线程安全的。
原子性
原子性的操作是不可被中断的,即使是多个线程同时执行,也互不干扰。
竞态条件
当某个结果的正确性取决于多个线程的交替时序的时候,那么就会发生竞态条件。最常见的竞态类型就是“先检查再运行”,即通过一个可能失效的结果来决定下一步的动作。这个失效的结果可能给你带来未预期的异常,数据被覆盖的。
“先检查后执行”的最常见的一种情况就是延迟初始化,所谓延迟初始化,就是将对象的初始化延迟到使用该对象时才进行,比如单例模式,如下代码:
1 | public class Singleton { |
如上的代码可能会出现多个Singleton的实例。比如线程A在进行条件判断,看到singleton为空,就会去创建实例,而此时线程B进来,也同样需要判断条件,而此时的singleton是否为null,则取决于不可预计的时序,以及线程的调度方式,所以,这两次调用getInstance( ),可能得到的实例并不是同一个实例。这就违法了单例模式了。
另外一种常见的竞态条件,发生在“读取-修改-写入”的操作中,例如递增计数器( count++ )。在使用递增计数器的时候,你必须要先获取先前的值,然后确保在执行更新过程中不会被其他线程修改和读取这个值。
复合操作
这是另外一个常见的竟态条件,包含了一组需要以原子方式执行的操作,也就是当前线程在修改某个变量时,必须通过某种方式来阻止其他线程来使用这个变量,从而确保其他线程只能在修改操作完成之前或之后来读取或修改这个变量。