不灭的焱

革命尚未成功,同志仍须努力下载JDK17

作者:Albert.Wen  添加时间:2018-11-06 23:44:59  修改时间:2024-03-29 19:14:18  分类:Java基础  编辑

并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了。这是危及到线程安全的,因为其他线程有可能通过这个逸出的引用访问到“初始化了一半”的对象(partially-constructed object)。这样就会出现某些线程中看到该对象的状态是没初始化完的状态,而在另外一些线程看到的却是已经初始化完的状态,这种不一致性是不确定的,程序也会因此而产生一些无法预知的并发错误。在说明并发编程中如何避免this引用逸出之前,我们先看看一个对象是如何产生this引用逸出的。

一、this引用逸出是如何产生的

正如代码清单1所示,ThisEscape在构造函数中引入了一个内部类EventListener,而内部类会自动的持有其外部类(这里是ThisEscape)的this引用。source.registerListener会将内部类发布出去,从而ThisEscape.this引用也随着内部类被发布了出去。但此时ThisEscape对象还没有构造完成 —— id已被赋值为1,但name还没被赋值,仍然为null。

 

代码清单1 this引用逸出示例

public class ThisEscape {
    public final int id;
    public final String name;
    public ThisEscape(EventSource<EventListener> source) {
          id = 1;
          source.registerListener(new EventListener() {
                public void onEvent(Object obj) {
                      System.out.println("id: "+ThisEscape.this.id);
                      System.out.println("name: "+ThisEscape.this.name);
                }
          });
          name = "flysqrlboy";
            
    }
 }

 

实际上,清单1中把内部类对象发布出去的source.registerListener语句没什么特殊的,从代码清单2可发现,registerListener方法只是往list中添加一个EventListener元素而已。这样,其他持有EventSource对象从而持有EventListener对象的线程,便可以访问ThisEscape的内部状态了(id和name)。代码清单3中的ListenerRunnable 就是这样的线程。

 

代码清单2 EventSource类

public class EventSource<T> {

    private final List<T> eventListeners ;
      
    public EventSource() {
          eventListeners = new ArrayList<T>() ;
    }
      
    public synchronized void registerListener(T eventListener) {
          this.eventListeners.add(eventListener);
          this.notifyAll();
    }
      
    public synchronized List<T> retrieveListeners() throws InterruptedException {
          List<T> dest = null;
          if(eventListeners.size() <= 0 ) {
                this.wait();
          }
          dest = new ArrayList<T>(eventListeners.size());
          dest.addAll(eventListeners);
          return dest;
    }
}

 

代码清单3 ListenerRunnable 类

public class ListenerRunnable implements Runnable {
     private EventSource<EventListener> source;
     public ListenerRunnable(EventSource<EventListener> source) {
           this.source = source;
     }
     public void run() {
           List<EventListener> listeners = null;
            
           try {
                 listeners = this.source.retrieveListeners();
           } catch (InterruptedException e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
           }
           for(EventListener listener : listeners) {
                 listener.onEvent(new Object());
           }
     }

 }

 

代码清单4,是个普通的消费线程的客户端程序,它先启动了一个ListenerRunnable 线程,用于监视ThisEscape的内部状态。紧接着调用ThisEscape的构造函数,新建一个ThisEscape对象。在ThisEscape构造函数中,如果在source.registerListener语句之后,name="flysqrlboy"赋值语句之前正好发生上下文切换(如图1),ListenerRunnable 线程就有可能看到了还没初始化完的ThisEscape对象--即id为1,但是name仍然为null!虽然正好在这个点上发生上下文切换是“偶然”事件,但理论上它是存在的。而这正是并发编程令人头疼的地方--平时好好的,但有时候就莫名其妙的失败了!而且还很难找出原因。为了使本例的this引用逸出容易被观察到,我们试图改造一下ThisEscape的构造函数(代码清单5),假设在source.registerListener和name赋值语句之间,还有其他的初始化操作,而且是比较耗时的。我们用一个sleep方法来模拟这样的耗时操作。经过这样的改造后,this引用逸出几乎是必然出现的--id等于1,name等于null。

 

代码清单4 ThisEscapeTest

public class ThisEscapeTest {

    public static void main(String[] args) {
        EventSource<EventListener> source = new EventSource<EventListener>();
        ListenerRunnable listRun = new ListenerRunnable(source);
        Thread thread = new Thread(listRun);
        thread.start();
        ThisEscape escape1 = new ThisEscape(source);
    }
}

 

图1 上下文切换

 

代码清单5 改造后的ThisEscape

public class ThisEscape {

    public final int id;
    public final String name;
    public ThisEscape(EventSource<EventListener> source) {
        id = 1;
        source.registerListener(new EventListener() {
              public void onEvent(Object obj) {
                    System.out.println("id: "+ThisEscape.this.id);
                    System.out.println("name: "+ThisEscape.this.name);
              }
        });
        try {
              Thread.sleep(1000); // 调用sleep模拟其他耗时的初始化操作
        } catch (InterruptedException e) {
              // TODO Auto-generated catch block
              e.printStackTrace();
        }
        name = "flysqrlboy";
        
    }
}

二、如何避免this引用逸出       

上文演示了由内部类导致的this引用逸出是怎样产生的。它需要满足两个条件:一个是在构造函数中创建内部类(EventListener),另一个是在构造函数中就把这个内部类给发布了出去(source.registerListener)。因此,我们要防止这一类this引用逸出的方法就是避免让这两个条件同时出现。也就是说,如果要在构造函数中创建内部类,那么就不能在构造函数中把他发布了,应该在构造函数外发布,即等构造函数执行完毕,初始化工作已全部完成,再发布内部类。正如清单6所示的那样,使用一个私有的构造函数进行初始化和一个公共的工厂方法进行发布。

 

代码清单6 安全的构建以防止this引用逸出

public class ThisSafe {

    public final int id;
    public final String name;
    private final EventListener listener;

    private ThisSafe() {
        id = 1;
        listener = new EventListener(){
              public void onEvent(Object obj) {
                    System.out.println("id: "+ThisSafe.this.id);
                    System.out.println("name: "+ThisSafe.this.name);
              }
        };
        name = "flysqrlboy";
    }

    public static ThisSafe getInstance(EventSource<EventListener> source) {
        ThisSafe safe = new ThisSafe();
        source.registerListener(safe.listener);
        return safe;
    }
          
}

 

另一种导致this引用逸出的常见错误,是在构造函数中启动一个线程。其原理跟上文说的内部类导致的this引用逸出相类似。解决的办法也相似,即可以在构造函数中创建线程,但别启动它。在构造函数外面再启动。

 

 

摘自:https://blog.csdn.net/flysqrlboy/article/details/10607295