如何共享和发布对象?
可见性
可见性(Memory Visibility)就是指读操作的线程能看到其他线程写入的值。
要确保可见性,必须使用同步。
(在没有同步的条件下,编译器,处理器,运行时都可能对操作的执行顺序进行重新排序)
失效数据:
在缺少同步的程序中可能产生失效的数据。
非原子的64位操作:
Java内存模型要求变量的读或写必须是原子操作。但是对非volatile的64位的long 和 double, JVM允许64位的读或写分解成两个32位的操作。所有在多线程程序中使用共享且可变的long和double变量不安全,需要用关键字volatile声明它们或锁保护。
加锁与可见性:
加锁可以保证一个线程以一种可预测的方式查看另外一个线程的执行结果,同步代码块和同步方法不仅可以确保以原子的方式执行操作;同步还可以确保内存可见性。
volatile变量:
仅当满足下面所有条件时,才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或能确保只有单个线程更新变量的值;
- 该变量不会与其他状态变量一起纳入不变性条件中;
- 在访问变量时不需要加锁
发布与逸出
“发布(Publish)”一个对象,是指这个对象在当前作用域之外的代码中可以使用。如一个非私有方法返回一个引用或将引用传递到其他类的方法,则这个引用的对象都被”发布”。
不应该发布的对象被发布,叫逸出(Escape)
不要在构造过程中使this引用发布。
线程封闭(Thread Confinement)
不共享可变数据,但要确保封闭在线程中的对象不会从线程中逸出。
- Ad-hoc线程封闭
- 栈封闭(局部变量)
- ThreadLocal
不可变性(Immutable)
如果某个对象在被创建后其状态不能被修改,则这个对象是不可变对象。
不可变对象是线程安全,不需要同步,可以任意发布。
不可变对象必须满足下面条件:
- 对象创建后其状态不能被修改。
- 对象的所有域是final类型。
- 对象是正确创建的(在对象创建过程中,this引用没有逸出)。
示例:下面的Person对象是不可变的(使用了clone()方法,birthday变量没有被发布出去)。
public class Person {
private final String id;
private final String name;
private final Date birthday;
public Person(String id, String name, Date birthday) {
this.id = id;
this.name = name;
this.birthday = (Date)birthday.clone();
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public Date getBirthday() {
return (Date)birthday.clone();
}
}
安全发布
可变对象必须通过安全的方式来发布,常用模式:
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile域或AtomicReference对象中。
- 将对象的引用保存到某个正确构造对象的final类型域中。
- 将对象的引用保存到一个有锁保护的域中(如安全容器Hashtable, ConcurrentMap)。
对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布。
- 事实不可变对象必须通过安全方式来发布。
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来(访问对象的状态时同步)。
并发程序中使用和共享对象时,一些实用策略
- 线程封闭
- 只读共享(包括不可变对象和事实不可变对象)
- 线程安全共享(对象内部同步)
- 保护对象 (如放到同步容器中)