想要使用多线程编程,有一个很重要的前提,那就是必须保证操纵的是线程安全的类.
那么如何构建线程安全的类呢? 1. 使用同步来避免多个线程在同一时间访问同一数据. 2. 正确的共享和安全的发布对象,使多个线程能够安全的访问它们.
那么如何正确的共享和安全的发布对象呢? 这正是这篇博客要告诉你的.
1. 多线程之间的可见性问题.
为什么在多线程条件下需要正确的共享和安全的发布对象呢?
这要说到可见性的问题:
在多线程环境下,不能保证一个线程修改完共享对象的数据,对另一个线程是可见的.
一个线程读到的数据也许是一个过期数据,这会导致严重且混乱的问题,比如意外的异常,脏的数据结构,错误的计算和无限的循环.
举个例子:
private static class RenderThread extends Thread{ @Override public void run(){ while(!ready){ Thread.yield(); } System.out.println("num = " + num); } } public static void main(String [] args) throws InterruptedException { new RenderThread().start(); num = 42; ready = true; } }new RenderThread().start()表示创建一个新线程,并执行线程内的run()方法 ,如果ready的值是false,执行Thread.yield()方法(当前线程休息一会让其他线程执行),这时候再交给main方法的主线程执行,给num赋值42,ready赋值true,然后在任务线程中输出num的值.因为可见性的问题,任务线程可能没有看到主线程对num赋值,而输出0.
我们接下来来看看发布对象也会引发的可见性问题.
2. 什么是发布一个对象
发布: 让对象内被当前范围之外的代码所使用.
public class Publish { public int num1; private int num2; public int getNum2(){ return this.num2; } }无论是 publish.num1 还是 publish.getNum2()哪种方法,只要能在类以外的地方获取到对象,我们就称对象被发布了.
如果一个对象在没有完成构造的情况下就发布了,这种情况叫逸出.逸出会导致其他线程看到过期值,危害线程安全.
常见的逸出的情况:
1.最常见的逸出就是将对象的引用放到公共静态域(public static Object obj),发布对象的引用,而在局部方法中实例化这个对象.
public class Test { public static Set<Object> set; public void initialize(){ set = new HashSet<>(); } }2.发布对象的状态,而且状态是可变的(没用final修饰),或状态里包含其他的可变数据.
public class UnsafeStates { private String [] states = new String[]{"a","b","c"}; public String[] getStates(){ return states; } }3.在构造方法中使用内部类. 内部类的实例包含了对封装实隐含的引用.
public class UnsafeStates { private Runnable r; public UnsafeStates() { r = new Runnable() { @Override public void run() { // 内部类在对象没有构造好的情况下,已经可以this引用,逸出了 // do something; } }; } }逸出主要会导致两个方面的问题:
- 发布线程以外的任何线程都能看到对象的域的过期值,因而看到的是一个null引用或者旧值,即使此刻对象已经被赋予了新值.
- 线程看到对象的引用是最新的,但是对象的状态却是过期的.
我们已经了解了逸出的问题,那么如何安全的发布一个对象呢?
为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见(也就是说安全发布就是保证对象的可见性),一个正确创建的对象可以通过下列条件安全发布:
- 通过静态初始化器初始化对象的引用.
