11.对象的安全发布

发布与逸出

发布一个对象的意思是,使对象能够在当前的作用域之外的代码中使用。例如将指向对象的引用保存到其他代码可以访问的代码地方去(共享变量),或者对象引用传递给其他方法,或者一个非私有的方法返回一个对象的引用。这些情况下对象的作用域从一个可知的地方,去到一个不可预知的地方,就是发布出去。也即对其他代码可见。也正因为这种可见,在并发编程中,才需要关注其数据的一致性。

在许多情况下我们要确保对象的内部状态封装在内部,不被发布出去;在某些情况下,需要发布某个对象,但是如果需要在发布时确保线程安全,则可能需要使用同步。

如果在对象构造完成之前就将对象发布出去,就会破坏线程安全性。当某个不应该被发布的对象被发布时,就是逸出。常见的错误有this引用逃逸。

发布对象

发布对象最简单的方式就是将一个对象赋给一个公共的静态变量中,以使得任何类和线程可以访问。

//发布一个对象
public static Set<Secret> knowSecret;
public void init(){
    knowSerect = new HashSet<>();   
}
//使内部的可变状态逸出
class UnsafeClass{
    private String[] state = new String[]{
        "A","B"
    };
    public String[] getState(){
        return state;
    }
}

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会发布出去,因为外部可以通过非私有方法访问他们。

当把一个对象传递给某个外部方法时,就相当于发布了这个对象。你无法知道哪些代码会执行,也不知道在外部方法中究竟会发布这个对象还是会保留对象的引用在随后另一个线程中使用。无论发布后其他的代码如何使用这个对象都不重要,因为误用该引用的风险始终存在。

当某个对象逸出后,必须假设会有某个类或线程会误用这个对象,这也是需要使用封装的主要原因:

封装能够使得对程序的正确性进行分析变得可能,并且使得无意中破坏设计约束条件变得更难。

还有一种发布对象的机制是发布一个内部类实例。

public class ThisEscape{
    public ThisEscape(EventSource source){
        source.registerListener(
        new EventListener(){
        //...
        });
    }
}

当ThisEscape发布EventListener时,也隐含发布了ThisEscape实例本身,因为:

在这个内部类的实例中包含了对ThisEscape类实例的隐含引用,
因为内引类(inner class inst ances)的实例包含了对封装实例隐含的引用。
内部类自动拥有对外围类所有成员的访问权,是通过秘密捕获一个指向那个外围类对象的引用实现的,
假如ThisEscape类中有个i变量,那么在EventListener内部类中就可以使用ThisEscape.this.i来访问外部类的变量i。

测试一下:

package jdk.test.innerClass;

public class ThisEscape {

    public ThisEscape(Handler handler) {
        handler.setListener(new EventListener() {
            {
                setName("hello");
            }
        });
    }
}

class EventListener {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

class Handler {
    private EventListener listener;

    public EventListener getListener() {
        return listener;
    }

    public void setListener(EventListener listener) {
        this.listener = listener;
    }

}

client:

public class Client {
    public static void main(String[] args) {
        Handler handler = new Handler();
        ThisEscape obj = new ThisEscape(handler);
        System.out.println(obj);
    }
}

debug查看handler的listener实例,可以看到this$0的属性,指向ThisEscape的实例对象obj。这是内部类实例的属性。

安全的对象构造过程

当且仅当对象的构造函数返回时,对象才处于可预测的和一致性的状态。当从对象的构造器中把this发布出去了,也即this引用逃逸,其他线程可能看到的是一个未构建完成的对象,因为在构建过程中把对象发布了出去,这种对象被认为是不正确构造。

在构造函数中是this引用逃逸的常见错误有

  1. 在构造器中启动一个线程。
  2. 在构造函数中调用一个可以被子类重写方法

当对象在其构造器中创建一个线程时,无论显式还是隐式(内部类),this引用都会被新创建的线程共享。
解释示例:

public class A{ 
   public A(){ 
      this.x=1; 
      this.y=2; 
      this.thread=new MyThread(); 
      this.thread.start(); 
   } 
   
}   

这个会引起什么问题呢?如果有个类B继承了类A,依据java类初始化的顺序,A的构造函数一定会在B的构造函数调用前被调用,那么thread线程也将在B被完全初始化之前启动,当thread运行时使用到了类A中的某些变量,那么就可能使用的不是你预期中的值,因为在B的构造函数中你可能赋给这些变量新的值。也就是说此时将有两个线程在使用这些变量,而这些变量却没有同步。
解决这个问题有两个办法:将A设置为final,不可继承;或者提供单独的start方法用来启动线程,而不是放在构造函数中。

如果要在构造器中注册一个事件监听器或启动线程,可以使用一个私有的构造器和一个公共的工厂方法来避免不正确的构造。
示例:

public class SafeListener{
    private final EventListener listener;
    private SafeListener(){
        listener = new EventListener(){
            pubilc void onEvent(){
                //...
            }
        };
    }
    public static SafeListener newInstance(EventSource source){
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        retuen safe;
    }
}

具体来说,只有当构造函数返回时,this引用才可以从线程中逸出。构造函数可以将this引用存储到某个地方,只要其他线程不会在构造函数返回之前访问到它,就是安全的。SafeListener就是使用的这种策略。

参考资料

[1] Java并发编程实战

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 3,243评论 2 9
  • 又是好久没码字。自从生活多了dota,似乎原来晚上躺着看看文写写字的习惯也随之消失了。晚上陪女票聊天的时间也少了。...
    Forri阅读 186评论 0 0
  • 日日清晨行道舟,唤醒山水伴晨修,人人只盼寺中智,不知天地无所求。 原创作品 (Original Article)
    一诗一境界阅读 249评论 0 0

友情链接更多精彩内容