很多单线程程序,一旦放到多线程环境下运行,就会出现问题,所以在设计程序时应尽可能考虑线程安全问题。本次要介绍的是“对象引用逃逸”引起的线程不安全问题。
对象引用逃逸:指对象在构建完成前(constructor执行完之前)就被其它对象引用。
出现对象引用逃逸的前置条件:多线程环境。
对象引用逃逸可能引发的问题:程序出现不可预期的行为,因为对象可能在还没有完全初始化就被引用了,这时如果使用了对象里的数据,就是脏数据。
出现对象引用逃逸的可能情况:在构建函数完成前,返回内部匿名类(AIC)对象给外部对象。这时内部类对象隐式包含了指向外层类的指针,外部对象通过此指针就可访问未构建完成的对象了。
示例代码:
(为方便移动端查看,代码做了简化。手机横屏看代码更酸爽!完整代码可上Github)
final List<EventListener> listeners = new ArrayList<>();Runnable task = () -> {try {ThisRefEscape thisRefEscape = new ThisRefEscape(listeners);} catch (InterruptedException e) {throw new RuntimeException(e);}};new Thread(task).start();// Wait for the above thread to create EventListener object.Thread.sleep(1000);System.out.printf("[Thread-%s]Start hacking...%n", Thread.currentThread().getId());for(EventListener l : listeners) {for (Field f : l.getClass().getDeclaredFields()) {String name = f.getName();String type = f.getType().getName();Object value = f.get(l);System.out.printf(" Field name: %s, %n Field Type: %s, %n Field value: %s%n",name , type, value);if (f.getName().startsWith("this$") && value instanceof ThisRefEscape) {((ThisRefEscape)value).catchYou("HACKED! thisRefEscape is escaped!");}}}
以上代码,在thisRefEscape还没完成初始化时,主线程已经可以通过EventListener引用到thisRefEscape。
public ThisRefEscape(List<EventListener> listeners)throws InterruptedException{System.out.printf("[Thread-%s]ThisRefEscape Constructing.%n",Thread.currentThread().getId());listeners.add(new EventListener() { // Escape Point@Overridepublic void doSomething(String message) {System.out.printf("message: %s%n", message);}});System.out.printf("[Thread-%s]ThisRefEscape Add Listener Completed.%n",Thread.currentThread().getId());Thread.sleep(2000);System.out.printf("[Thread-%s]ThisRefEscape Constructor Completed.%n",Thread.currentThread().getId());}public void catchYou(String message) {System.out.printf("[Thread-%s]%s%n",Thread.currentThread().getId(), message);}
运行结果:
[Thread-11]ThisRefEscape Constructing.[Thread-11]ThisRefEscape Add Listener Completed.[Thread-1]Start hacking...Field name: this$0,Field Type: com.hoogia.poc.thread.ThisRefEscape,Field value: com.hoogia.poc.thread.ThisRefEscape@7ef20235[Thread-1]HACKED! thisRefEscape is escaped![Thread-11]ThisRefEscape Constructor Completed.
解决对象引用逃逸问题的方法之一:
在完成对象构建之前,将最终要暴露给外部对象的内部类对象先私有保存起来,在完成对象构建后,再将内部类对象的引用赋值给外部对象。
private EventListener eventListener;public static NoRefEscape getInstance(List<EventListener> listeners)throws InterruptedException{NoRefEscape noRefEscape = new NoRefEscape();listeners.add(noRefEscape.eventListener);return noRefEscape;}private NoRefEscape() throws InterruptedException {eventListener = new EventListener() {public void doSomething(String message) {System.out.printf("message: %s%n", message);}};Thread.sleep(2000);}
以上示例代码可从Github上完整下载:
https://github.com/yang-yihao/proof-of-concept/tree/master/thread-ref-escape