对象发布(publish)
发布一个对象是使它能够在被当前范围之外的代码所使用。比如创建一个对象之后,提供一个非私有方法返回这个对象的引用,或者把它传递到其他类的方法中。下面是常见的发布对象的例子:
1.把对象的引用保存到静态公有域中 public static List<Object> objectList; public void initList() { objectList =New ArrayList<Object>(); }
在上面的代码中,任何类和线程都可以访问objectList,当objectList通过initList方法实例化一个ArrayList对象后,并把引用赋值给objectList,那么这个ArrayList对象就发布出去了。同时发布一个对象还可能间接的发布了其他对象。比如objectList添加了一个object1,那么object1也被发布出去了。因为任何类或线程都可以通过遍历objectList访问并修改object1。
2.通过非私有方法把对象发布出去 private String[] strArray = {"data1","data2"}; public String[] getStrArray () { return strArray ; }
通过getStrArray方法,任何类或者线程都可以访问和修改strArray,此时strArray对象也被发布出去了。但是这里strArray变量定义为私有对象,通过getStrArray方法,使得strArray超出了它的作用范围,由私有变成公有的了。
发布一个对象,同样发布了该对象的所有非私有域引用的对象,甚至整个非私有域的引用链中的对象,都发布出去了。
对象逸出(escape)
一个对象超出了她应有的作用域,或者未被构造完全就发布了,称为对象逸出。很多情况下我们不希望对象以及对象的内部属性不被暴露出去,但是在其他一些情况中,我们又必须使对象发布出去,但是如果不经意中把对象的内部状态也发布出去了,就使得其他线程可以修改这个对象的内部状态,造成不可预知的线程安全性风险,同时,如果对象没有构造完全就发布出去了同样会危及线程安全。上面第2个代码就是把私有对象发布出去而导致对象逸出。下面是一种对象未构造完全就发布造成的对象逸出:
对象在构建期间逸出 public class ThisEscape() { public ThisEscape(EventSource event) { event.register(new EventListener(){ public void onEvent(Event e){ doSomeThing(e);} }) } }
这段代码的本意是注册监听器,通过匿名内部类发布EventListener,但是由于内部类会持有封装它的外部类的引用this,所以ThisEscape的一个对象也发布出去了。从而导致对象未构造完全就发布出去的逸出。对象只有在构造函数返回后才处于可预知的,真正稳定的状态。所以从构造方法中发布对象,它发布的对象是未构建全的对象,即使是在对象的最后一行发布也是如此。一个非常常见的在构造期对象逃逸的错误,是在构造期启动线程,无论是显式的还是隐式的(因为thread或ruanable是所属对象的内部类),this引用总是被新线程所持有。
如何安全发布对象
为了安全的发布对象,对象的引用以及对象的状态必须同时对其他线程可见。那么如何才能安全的发布对象呢,下面是正确发布对象的几种方法
1.通过静态初始化器初始化对象的引用
常见的例子是在构造方法中注册监听器或启动线程。如果非要在构造函数中启动线程或启动线程,可以使用私有的构造函数和公有的工厂方法,比如上面注册监听器的例子:
public class SafeListener() { private final EventLister listener; private SafeListener() { listener= new EventLister () { public void onEvent(Event e) { doSomething(e); } } } public static SafeListener getInstance(EventSource source) { SafeListener safeListener = new SafeListener(); source.resgister(safeListener ); return safeListener ; } }
2.线程封闭
需要线程同步的基础是线程间共享对象和数据,如果线程不共享数据和对象,对象只封闭在线程内部,那么就不需要线程同步,也就不需要发布对象,即使对象是不线程安全的。
2.1 线程限制
通过编程人员维护对象安全发布,而没有变量修饰词或线程本地变量把对象限制在线程上。这种方法非常容易出错,因此也叫非正式线程限制(Ad-hoc线程限制)。下面是几种常见的线程限制方法:
volatile变量
一张典型的线程限制是使用volatile关键词。只要你确保只要一个线程修改volatile变量,那么volatile变量的读取-修改-写入操作就限制在一个线程中,避免了线程的竞争条件。并且volatile的可见性可以保证其他线程能看到变量的最新值,因此是线程安全的。
栈限制
即使变量的作用域只局限于线程内部,比如局部变量。
2.3 threadlocal
threadlocal允许每个线程与对象关联在一起,它提供了set/get方法,为每个使用它的线程维护一份单独的拷贝。threadlocal通常使用在防止基于可变的单例或全局变量中出现不正确的共享。
2.4 不变对象
不变对象一旦被创建就不能更改,所以永远是线程安全的。所谓的不变对象,必须满足3个条件:创建后状态不能更改,所有域必须是final的,被正确构建(构造期间没有发生对象逸出)。不变对象一般是一些比较简单的对象,它的内部状态在对象构造方法里就赋值了。
3.将对象的引用存储到被正确同步的变量或对象中
具体有下面几种做法:
将对象的引用存储到volatile域或AutomaticReference中;
将对象的引用存储到正确创建对象的final域中
将对象的引用存储到被锁正确保护的域中,比如线程安全的容器 vector,synchronizedMap,concurrentMap,synchronizedList,synchronizedSet,BlockingQueue,concurrentLinkedQueue等。