不安全的发布
这种发布会导致其他线程看到尚未未构建完成的对象,另一个线程在调用assertSanity 方法可能会抛出AssertionError(stackoverflow里的相关问题)
/**
* StuffIntoPublic
* <p/>
* Unsafe publication
*
* @author Brian Goetz and Tim Peierls
*/
public class StuffIntoPublic {
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
}
/**
* Holder
* <p/>
* Class at risk of failure if not properly published
*
* @author Brian Goetz and Tim Peierls
*/
public class Holder {
private int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if (n != n)
throw new AssertionError("This statement is false.");
}
}
不可变对象与初始化安全性
由于不可变对象是一种非常重要的对象,由于java内存模式为不可变对象提供了一种特殊的初始化安全保证。(如果将Holder里的n声明为final类型,那么Holder是不可变的,从而避免出现未正确发布的问题)我们知道即使一个对象的引用对于其他线程是可见的,也并不意味对象状态对于其他线程是可见的。为了确保状态一致得必须使用同步。
任何线程都可以在不需要额外同步的情况下去访问不可变对象,即使在发布这个对象时没有使用同步。这种保证还将延续到不可变对象的final域。在没有额外同步的情况下,也可以安全的访问所有的final域。然而,如果final域所指向的是一个可变对象,那么在访问这些域所指向的对象的状态仍然需要额外同步。
安全发布常用模型
可变对象可以用安全的方式来发布
- 在静态初始化域中初始化一个对象引用(public static Holder holder = new Holder(42);)
静态初始化器在jvm在类的初始化阶段执行,由于jvm内部存在同步机制,因此通过这种方式初始化的对象都可以被安全的发布 - 将对像引用保存到volatile类型的域或AtomicRefrance对象中
- 将对象的引用保存到一个正确构造的对象的final域中
- 将对象的引用保存到一个由锁保护的域中
事实不可变对象
技术上是可变的,不满足不可变对象的要求,但是其状态确实在发布后不会改变,这种对象称为事实不可变对象。
在没有额外同步的状态下任何线程都可以使用被安全发布的事实不可变对象
可变对象
如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅在发布对象时使用同步,而且在每次对象访问时同样需要使用同步来确保后续操作的可见性。要安全地共享可变对象,这些对象就必须被安全地发布,并且必须是某个线程安全的或者由某个锁保护起来。
对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布
- 事实不可变对象必须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且必须是线程安全的类或者由某个锁保护起来
安全的共享对象
当获得对象的一个引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改他的状态,或者只能读取它?许多并发错误都是由于没有理解共享对象的这些“既定规则”而导致的。当发布一个对象时,必须明确的说明对象的访问方式。
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
- 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
- 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象
- 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
- 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。