在写Java程序的时候,何时需要进行并发控制,关键在于判断这段程序或这个类是否是线程安全的。
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步,这个类的行为仍然是正确的,那么称这个类是线程安全的。我们设计类就是要在有潜在并发问题存在情况下,设计线程安全的类。线程安全的类可以通过以下手段来满足:
- 不跨线程共享变量
- 使状态变量为不可变的
- 在任何访问状态变量的时候使用同步。
- 每个共享的可变变量都需要由唯一一个确定的锁保护。
满足线程安全的一些思路
1)从源头避免并发问题
很多开发者一想到有并发的可能就通过底层技术来解决问题,其实往往可以通过上层的架构设计和业务分析来避免并发场景。比如我们需要用多线程或分布式集群来计算一堆客户的相关统计值,由于客户的统计值是共享数据,因此会有并发潜在可能。但从业务上我们可以分析出客户与客户之间数据是不共享的,因此可以设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。这种规则很容易设计。当你从源头就避免了并发问题的可能,下面的工作就完全可以不用担心线程安全问题。
2)无状态就是线程安全
多线程编程或者分布式编程最忌讳有状态,一有状态就不但限制了其横向扩展能力,也是产生并发问题的起源。当你设计的类是无状态的,那么它永远都是线程安全的。因此在设计阶段需要考虑如何用无状态的类来满足你的业务需求
3)分清原子性操作和复合操作
所谓原子性,是说一个操作不会被其他线程打断,能保证其从开始到结束独享资源连续执行完这一操作。如果所有程序块都是原子性的,那么就不存在任何并发问题。而很多看上去像是原子性的操作正式并发问题高灾区。比如所熟知的计数器(count++)和check-then-act,这些都是很容易被忽视的,例如大家所常用的惰性初始化模式,以下代码就不是线程安全的:
- @NotThreadSafe
- public class LazyInitRace {
- private ExpensiveObject instance = null;
- public ExpensiveObject getInstance() {
- if (instance == null)
- instance = new ExpensiveObject();
- return instance;
- }
- }
这段代码具体问题在于没有认识到if(instance==null)和instance = new ExpensiveObject();是两条语句,放在一起就不是原子性的,就有可能当一个线程执行完if(instance==null)后会被中断,另一个线程也去执行if(instance==null),这次两个线程都会执行后面的instance = new ExpensiveObject();这也是这个程序所不希望发生的。
虽然check-then-act从表面上看很简单,但却普遍存在与我们日常的开发中,特别是在数据库存取这一块。比如我们需要在数据库里存一个客户的统计值,当统计值不存在时初始化,当存在时就去更新。如果不把这组逻辑设计为原子性的就很有可能产生出两条这个客户的统计值。
在单机环境下处理这个问题还算容易,通过锁或者同步来把这组复合操作变为原子操作,但在分布式环境下就不适用了。一般情况下是通过在数据库端做文章,比如通过唯一性索引或者悲观锁来保障其数据一致性。当然任何方案都是有代价的,这就需要具体情况下来权衡。
另外,java1.5以后提供了一套提供原子性操作的类,有兴趣的可以研究一下它是如何在软件层面保证原子性的。
4)锁的合理使用
大家都知道可以用锁来解决并发问题,但在具体使用上还有很多讲究,比如:
- 每个共享的可变变量都需要由一个个确定的锁保护。
- 一旦使用了锁,就意味着这段代码的执行就丧失了操作系统多道程序的特性,会在一定程度上影响性能
- 锁不能解决在分布式环境共享变量的并发问题