解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式。所谓不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。
快速实现具备不可变性的类
将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。更严格的做法是这个类本身也是 final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。
经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性。
利用享元模式避免创建重复对象
享元模式本质上其实就是一个对象池,利用享元模式创建对象的逻辑也很简单:创建之前,首先去对象池里看看是不是存在;如果已经存在,就利用对象池里的对象;如果不存在,就会新创建一个对象,并且把这个新创建出来的对象放进对象池里。
使用 Immutability 模式的注意事项
1. 对象的所有属性都是 final 的,并不能保证不可变性;
2. 不可变对象也需要正确发布。
例如下面的代码中,Bar 的属性 foo虽然是 final 的,依然可以通过 setAge() 方法来设置 foo 的属性 age。所以,在使用Immutability 模式的时候一定要确认保持不变性的边界在哪里,是否要求属性对象也具备不可变性。
class Foo{
int age = 0;
int name = "abc";
}
final class Bar {
final Foo foo;
void setAge(int a){
foo.age = a;
}
}
下面我们再看看如何正确地发布不可变对象。不可变对象虽然是线程安全的,但是并不意味着引用这些不可变对象的对象就是线程安全的。例如在下面的代码中,Foo 具备不可变性,线程安全,但是类 Bar 并不是线程安全的,类 Bar 中持有对 Foo 的引用 foo,对 foo这个引用的修改在多线程中并不能保证可见性和原子性。
//Foo 线程安全
final class Foo{
final int age = 0;
final int name = "abc";
}
//Bar 线程不安全
class Bar {
Foo foo;
void setFoo(Foo f){
this.foo = f;
}
}
如果你的程序仅仅需要 foo 保持可见性,无需保证原子性,那么可以将 foo 声明为volatile 变量,这样就能保证可见性。如果你的程序需要保证原子性,那么可以通过原子类来实现。
public class SafeWM {
class WMRange{
final int upper;
final int lower;
WMRange(int upper,int lower){
// 省略构造函数实现
}
}
final AtomicReference<WMRange>rf = new AtomicReference<>(new WMRange(0,0));
// 设置库存上限
public void setUpper(int v){
while(true){
WMRange or = rf.get();
// 检查参数合法性
if(v < or.lower){
throw new IllegalArgumentException();
}
WMRange nr = new WMRange(v, or.lower);
if(rf.compareAndSet(or, nr)){
return;
}
}
}
}