设计线程安全的程序
在上一篇博文(线程安全性:num++操作为什么也会出问题?)中,举了一个简单的num++操作导致线程不安全的例子:
public class Tools { private static int num = 0; public static int plus() { num++; return num; } }
当多个线程同时访问Tool类的plus()方法时,它产生了预期之外的结果,num++看似是一步操作,但其实它包含了多个操作:读取num,将num加一,将计算结果写入num,当多个线程同时读取num进行处理时就会出现问题。
如果我们写了这样一个Tools工具类,没有考虑并发的情况,其他调用者可能就会在多线程调用plus()方法中产生问题,我们也不希望在多线程调用其他开发者编写的类时产生和单线程调用不一样的结果,我们希望无论单线程还是多线程调用一个类时,无须使用额外的同步,这个类即能表现出正确的行为,这样的类是线程安全的。
怎么样设计一个线程安全的类呢?
观察上面程序,我们在对num变量进行操作时出了问题,首先,num变量具有两个特点:共享的(Shared)和可变的(Mutable)。“共享”意味着变量可以由多个线程同时访问,而“可变”意味着变量的值在其生命周期内可以发生变化;其次,看一下我们对num的操作,读取num,将num加一,将计算结果写入num,这是一个“读取-修改-写入”的操作,并且其结果依赖于之前的状态。这是一种最常见的竞态条件——“先检查后执行(Check-Then-Act)”操作,首先观察某个条件为真(num为0),然后根据观察结果采取相应的动作(将num加1),但是,当我们采取相应动作的时候,系统的状态就可能发生变化,观察结果可能变得无效(另一个线程在这期间将num加1),这样的例子还有很多,比如观察某路径不存在文件夹X,线程A开始创建文件夹X,但是当线程A开始创建文件夹X的时候,它先前观察的结果就失效了,可能会有另一个线程B在这期间创建了文件夹X,这样问题就出现了。
因此,我们可以从两个方面来考虑设计线程安全的类:
一、状态变量方面:(对象的状态是指存储在实例变量与静态域成员中的数据,还可能包括其他依赖对象的域。例如,某HashMap的状态不仅存储在HashMap本身,还存储在许多Map.Entry对象中。)多线程访问同一个可变的状态变量没有使用合适的同步会出现问题,因此:
1.不在线程之间共享该状态变量(即每个线程都有独自的状态变量)
2.将状态变量修改为不可变的变量
3.在访问状态变量时使用同步
二、操作方面:在某个线程需要复合操作修改状态变量时,通过某种方式防止其它线程使用这个变量,从而确保其它线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。