如果我们给一个无状态的类添加一个状态,会发生什么情况?让我们加上一个计数器看看:
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() { return count; }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
UnsafeCountingFactorizer在单
线程环境下运行的很正常,但却不是
线程安全的。其中++count这句话并不是一个简单的操作,它不具备原子性。事实上java编译器会把它变成三步:获取当前值,+1,写入新的值。这是个典型的读-修改-写操作,这样会修改类的状态。
问题出在当两个线程同时调用这个方法时,如果非常凑巧的它们同时调用到了++count,那就麻烦了。假设当前count=9,那么可能两个线程同时得到了结果为10。这显然可能导致隐藏的bug。
也许你会认为在一个
web程序中有一些这样的小缺陷无关紧要,但是如果这个值是有实际
意义的那你就麻烦大了。比如你通过这段代码获取新纪录的
主键,相同的主键会导致插入失败,或者其他的数据不一致问题。这种形式的
错误有一个专有名词:竞争条件。
UnsafeCountingFactorizer这个
例子具有典型的竞争条件,这就可能导致不稳定。在多线程环境下,竞争条件存在隐患,导致错误发生。最标准的竞争条件就是:获取一个对象的状态然后根据该状态进行行动,就像是check-then-act。
其实我们在生活中经常遇到竞争条件的情况。比如我们约一位朋友中午12点在星巴克见面,而当地有两间星巴克。你在星巴克A没有见到你的朋友,然后你跑去星巴克B,而你的朋友也不在那里。究竟怎么回事?当你们因为手机没电等情况没法互相沟通的话,情况就复杂了。也许你朋友根本没来,或者当你在A的时候,你朋友在B。而你当你赶往B的时候,他正在去A的路上。问题出在从A地去B地是需要时间的(这代表该操作不具有原子性),当你出发后,可能B地的状态已经改变了(有状态的对象)。
另一个具有check-then-act类型的行为是延迟加载。延迟加载的目的是使一个对象在被调用时才进行初始化,而不是在声明时进行。下面的例子就是这样,当调用getInstance方法时对象才被建立并分配
内存空间。
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null)
instance = new ExpensiveObject();
return instance;
}
}
让我们看一下这个例子,在多线程环境下存在隐藏的bug。当两个线程同时访问getInstance()方法时,有可能
他们同时得到instance == null的状态,然后调用instance = new ExpensiveObject();生成了两个对象,而你本来只想要一个的。这种竞争条件可能导致不可知的结果(可能就意味着很难调试出来的bug)。
事实上这种错误并不容易发生,触发这种类型的bug需要很凑巧的时机。也许在某个实际系统跑了很多年也一直没有发生,但是一旦发生了就可能导致灾难性的后果(特别是对可靠性要求特别高的系统中如金融,电信领域)。
在上面所阐述的例子中,竞争条件的发生都是由于缺乏线程原子性方面的考虑。当我们在作改变对象状态行为的时候,我们需要做一些额外的工作,使其他线程不能在我们正在修改数据的时候获得数据(只能在修改前或修改后)。
我们这样定义原子性:
如果有线程正在进行操作A,那么其他所有线程都不能进行操作B(反之亦然),那么就称操作A和操作B互相具有原子性。如果一个操作和其他所有的操作都具有原子性,那么称这个操作自身具有原子性。
以后我们会讲述java采用锁机制实现操作的原子性,而现在我们来看一种更简单的方式:
@ThreadSafe
public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
java.util.concurrent.atomic包提供了一些线程的工具来实现原子性。当我们用AtomicLong来替代普通的long时,隐藏的bug就被我们解决了,而且得到了一个有状态的servlet并且是线程安全的。
在实际开发过程中,尽量使用现有的线程安全对象如AtomicLong等去管理对象的状态。这会使其他人更容易理解你的思路,从而提高代码的可维护性。