要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的和可变的状态的访问。
对象的状态是指存储在状态变量(例如实例和静态域)中的数据。对象的状态可能包括其依赖对象的域。例如牟特HashMap的状态不仅存储在HashMap对象本身,还存储在许多Map.Entry对象中。在对象的状态中包含了任何可能影响其外部可见行为的数据。“共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。
一个对象是否需要是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。可以采用同步机制来协同对对象可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果。
当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。JAVA中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显式锁(Lock)以及原子变量。
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:
1. 不在线程之间共享该状态变量
2. 将状态变量修改为不可变的变量
3. 在访问状态变量时使用同步
如果在设计类的时候没有考虑并发访问的情况,那么在采用上述方法时可能需要对设计进行重大修改,因此要修复这个问题可谓是知易难行。如果从一开始就设计一个线程安全的类,那么比在以后再将这个类修改为线程安全的类要容易得多。
在一些大型程序中,要找出多个线程在哪些位置上将访问同一个变量是非常复杂的。幸运的是,面向对象这种技术不仅有助于编写出结构优雅、可维护性高的类,还有助于编写出线程安全的类。访问某个变量的代码越少,就越容易确保对变量的所有访问都实现正确同步,同时也更容易找出变量在哪些条件下被访问。JAVA语言并没有强制要求将状态都封装在类中,开发人员完全可以将状态保存在某个公开的域(甚至公开的静态域)中,或者提供一个对内部对象的公开引用。然而,程序状态的封装性越好,就越容易实现程序的线程安全性,并且代码的维护人员也越容易保持这种方式。
如何定义线程安全性?最核心的概念就是正确性。正确性的含义是,某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性来约束对象的状态,以及定义各种后验条件来描述对象操作的结果。由于我们通常不会为类编写详细的规范,那么如何知道这些类是否是正确的呢?我们无法知道,但这并不妨碍我们在确信“类的代码能工作”后使用它们。这个“代码可信性”非常接近我们对正确性的理解,因此我们可以将单线程的正确性近似定义为“所见即所知”。在对“正确性”给出了一个较为清晰的定义后,就可以定义线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。进一步解释,当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的。
由于单线程程序也可以看成是一个多线程程序,如果某个类在单线程环境中都不是正确的,那么它肯定不会是线程安全的。如果正确地实现了某个对象,那么在任何操作中(包括调用对象的公有方法或者对公有域进行读/写操作)都不会违背不变性条件或后验条件。在线程安全类的对象实例上执行的任何串行或并行操作都不会使对象处于无效状态。注意,线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施。
例子:一个无状态的Servlet
与大多数Servlet相同,StatelessFactorizer是无状态的:它既不包含任何域,也不包含任何对其他类中域的引用。计算过程中的临时状态仅存在线程栈上的局部变量中,并且只能由正在执行的线程访问。访问StatelessFactorizer的线程不会影响另一个访问同一个StatelessFactorizer的线程的计算结果,因为这两个线程并没有共享状态,就好像它们都在访问不同的示例。由于线程访问无状态对象的行为并不会影响其他线程中操作的正确性,因此无状态对象一定是线程安全的。
大多数的Servlet都是无状态的,从而极大地降低了在实现Servlet线程安全性时的复杂性。只有当Servlet在处理请求时需要保存一些信息,线程安全才会成为一个问题。