共享对象##
编写正确的并发程序的关键在于对共享的、可变的对象状态进行访问管理。
上一章使用同步来避免多个线程在同一时间访问同一数据。
同步还有另外的方面:内存可见性。
3.1 可见性
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
只要数据需要被跨线程共享,就进行恰当的同步。
3.1.1 过期数据
NoVisibility演示了一种没有恰当同步的程序,它能够引起意外结果:过期数据。
当线程检查read变量时,它可能看到一个过期的值。
非线程安全的可变整数访问器:
线程安全的可变整数访问器:
3.1.2 非原子的64位操作
最低限的安全性应用于所有的变量,除了一个例外:没有声明为volatile 的64位数值变量(double和long)。Java允许64位的读或写划分为两个32位才值。如果读写发生在不同的线程,非volatile 的64位数可能会出现一个值的高32位和另一个值的低32位。
3.1.3 锁和可见性
当访问一个共享变量时,为什么要求所有线程由同一个锁同步。
为了保证一个线程崔数值进行的写入,其他线程也可见。
3.1.3 Volatile
弱同步形式:Volatile变量
Volatile确保对一个变量的更新以可预见的方式告知其他线程。
volatile的典型应用:状态检查标记。
加锁可以保证可见性与原子性;volatile变量只能保证可见性。
3.2 发布与逸出
发布一个对象:使它能够在当前范围之外的代码所使用。一个对象在尚未准备好时就将它发布,被称作逸出。
发布对象:
允许内部可变的逸出:
隐式地允许this引用逸出:
内部类的实例包含了对封装实例隐含的引用。
3.2.1 安全构建的实践
不要让this引用在构造器期间逸出。
在构造函数中创建线程会导致this在构造期间逸出。
如果想在构造函数中注册监听器或启动线程,可以使用一个私有的构造方法和一个公共的工厂方法。
3.3 线程封闭
访问共享的、可变的数据要求使用同步。一个避免同步的方式就是使用不共享数据。
线程封闭是实现线程安全的最简单方式之一。
另一个常见使用线程限制的是应用池化的JDBC Connection对象。线程总是从池中获得一个Connection对象,并且用它处理单一请求,最后把它归还。
3.3.1 Ad-hoc 线程限制
Ad-hoc 线程限制是指维护线程限制性的任务全部落在实现上这种情况。
未经过设计而得到的线程封闭行为。
3.3.2 栈限制
在栈限制中,只有通过本地变量才可以触及对象。
类似局部变量,只存在当前执行线程中。
3.3.3 ThreadLocal
ThreadLocal 提供了get和set方法,可以将数值关联在线程上。
ThreadLocal 通常用于防止在基于可变的单体或者全局变量的设计中。
3.4 不可变性
为了满足同步的需要,另一种方法是使用不可变对象。
不可变对象永远是线程安全的。
3.4.1 Final域
Final对创建不可变对象提供了支持。
Final域是不能被修改的(指向的对象是可变的,这个对象仍然可被修改)。
3.4.2 使用volatile发布不可变对象
使用可变的容器,就必须使用锁以确保原子性;使用不可变对象,一旦一个对象获取了它的引用,用于不用担心其他线程会修改它的状态。
3.5 安全发布
由于可见性的问题,容器还是会在其他线程中被设置为一个不一致状态。这种不正确的发布会导致其他线程可以观察到“局部创建对象”。
3.5.1 不正确发布:当好对象变坏时
因为没有同步Holer对其他线程可见,所以称Holer是“非正确发布的”。
没有同步Holer会导致两种错误:其他线程会看到Holer域的过期值;
3.5.2 不可变对象与初始化安全性
Java存储模型为共享不可变对象提供了特殊的初始化安全性保证。
3.5.3 安全发布模式
静态初始化器创建对象:
public static Holder holder = newHolder(42);
线程安全库中的容器提供了如下的线程安全保证: