除了保证操作的原子性以外,同步还可以保证变量在不同线程之间的内存可见性。原子性和可见性共同构成了同步的两个核心要素。第三章主要讲述如何在线程之间安全的发布和共享变量。
首先可以通过书上的例子来看一下什么是可见性。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
表面上我们启动了两个线程,一个线程对ready flag进行无限期的检查,而另一个线程则对number和ready进行改变。从逻辑上说,我们可能会期待屏幕上打印出42.但是实际上由于可见性问题,另一个线程可能永远也看不到ready的值,因为JMM中线程都拥有自己的工作内存,并且只能对自己的工作内存进行存取,工作内存中保存的其实是主内存的一个副本。也有可能JVM会对指令进行重排序优化,尽管在单线程下会保证得到的最终结果是我们期待的逻辑结果(as-if-serial),但是其指令的执行过程不一定是声明的顺序。所以上面有可能是先对ready进行了置位,导致之后打印出了零。在多线程中如果没有进行同步或者volatile声明(在缺乏同步的程序中),就不能对指令的执行顺序抱有期待。
可见性问题会导致一个问题那就是其他线程得到的数据可能是已经过期的,但其他线程却一无所知。所以在可见性上java提供了volatile关键字来进行可见性的保证。如果将一个变量声明为volatile,jvm会在写入时将线程工作内存的最新值拷贝回主内存保证值是最新的,以及每次使用时都要刷新主内存的值,并且所有与volatile变量相关的语句都将禁止重排序。volatile虽然可以保证可见性,却不能保证操作的原子性。
总的来说。volatile作为更加轻量化的线程安全手段,适用的范围比同步更加有限。书上说:
1.写入的操作不应该依赖于当前的值,因为volatile无法保证原子性。
2.该变量没有被纳入其他变量的不变式关系之中,也就是说他是独立的。
3.访问时不需要加锁。
只有都满足上面三个条件时,volatile才适用。
发布指的是发布一个对象(实质上是发布对对象的引用)使得对象可以再当前作用域之外使用。发布是会破坏封装性的。当一个不应发布的对象被发布时就产生了溢出问题。发布一个对象到外部的最简单的方法是使用一个公有的static变量来保存发布对象。
public static Set<Secret> knownSecrets;
public void initialize() {
knownSecrets = new HashSet<Secret>();
}
通过一个公有的静态变量指向我们新建的HashSet,其他线程得到了新建HashSet的引用。
在发布对象时,可能会间接地发布其他对象。例如我发布一个hashset,那么hashset里面的值便也被间接的发布了。
有一种逸出是对于可变的private对象,private访问权限将这个对象封装到当前类,如果将其发布到外部便使得其他线程得到了关于一个我们希望是pirvate对象的引用,这违背了使用private原有的语义。
class UnsafeStates {
private String[] states = new String[] {
"AK", "AL" ...
};
public String[] getStates() { return states; }
}
另一种比较隐晦的发布其他对象的例子是内部类,由于内部类中保存着对于outerclass的外部应用,当我发布一个innnerclass时会间接的发布外部类的引用,可能会造成意想不到的结果。
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
最后是动态的construct初始化过程,如果我们在constructor的构造过程中向外发布this引用,那么发布出去的对象很有可能是部分构造的,即使是发布代码是最后一行,由于重排序也有可能是部分构造的。所以对象的this引用只有在完成constructor的构造之后才可以发布。
一个比较常见的错误是在构造体内启动一个线程,由于线程是共享this引用的,所以启动的线程可以看到未完全构造好的对象。所以在线程的启动方法一般是start。
对于这种情况,作者的建议是如果要对外发布对象,那么使用静态的工厂方法来完成这个操作。
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
还有其他技术可以完成线程安全。例如线程封闭将对象封闭在一个线程中,这样无需其他同步手段就是线程安全的。1.stack封闭.由于stack是线程私有的,被封闭在stack内的对象自然只有当前线程可以访问。2.使用ThreadLocal类。3.在线程内发布不可变对象,他们一定是线程安全的。满足下列三条的是不可变对象:1。对象创建之后就不可变。2.域都是final类型3.在对象构造的过程中其this引用没有逸出。
例如这种发布将public引用发布,由于不可见性,其他线程不一定可以看到最新的对象。静态初始化和动态初始化不同,静态初始化是JVM在类的load阶段初始化的,JVM可以保证内部的同步机制不出错。
// Unsafe publication
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
但是我们可以知道的是,对于一个对象,即便将这个对象的引用发布到其他线程,它的状态也不一定是对于其他线程可见的。因为其他线程只能通过私有的对象公有API来访问对象。所以即便我们发布了一个对象到其他线程,那么通过同步API方法和同步发布过程(安全发布),同样可以让这个对象是线程安全的。~