脑图
概念
发布对象: 使一个对象能够被当前范围之外的代码所使用,日常开发中比较常见的比如通过类的非私有方法返回对象的引用,或者通过公有的静态变量发布对象 等都属于发布对象
对象逸出: 首先需要明确的是对象逸出是一种错误的发布方式。 当一个对象还没有构造完成时,就使它被其他线程所见。
示例
不安全的发布对象Demo
package com.artisan.example.publish; import com.artisan.anno.NotThreadSafe; import lombok.extern.slf4j.Slf4j; @Slf4j @NotThreadSafe public class UnSafePublishObjectDemo { // 私有变量 private String name = "artisan"; // 通过public访问级别的方法getName发布了类的域,在类的外部,任何线程都可以访问这个域 // 这样发布的对象是不安全的,因为我们无法得知其他线程是否会修改这个域导致该类里数据的错误 public String getName() { return name; } public static void main(String[] args) { // 通过new实例化UnSafePublishObjectDemo UnSafePublishObjectDemo unSafePublishObjectDemo = new UnSafePublishObjectDemo(); // 调用getName()方法得到私有属性的引用 String name = unSafePublishObjectDemo.getName(); log.info("name:{}",name); // 假设有第二个线程去修改name属性的值 String name2 = unSafePublishObjectDemo.getName(); name2 = "小工匠"; log.info("name:{}",name2); } }
上面的代码里,通过new对象初始化了UnSafePublishObjectDemo对象。然后调用getName()方法获取到了私有属性的引用,这样就可以在其他任何线程中,修改该属性的值。这样将会导致我们在其他线程中,获取该属性的值时是不确定的,因为并不能得知该属性的值是否已被其他线程所修改过,所以这就是不安全的对象发布。
对象逸出Demo
package com.artisan.example.publish; import com.artisan.anno.NotRecommand; import com.artisan.anno.NotThreadSafe; import lombok.extern.slf4j.Slf4j; /** * * 对象逸出示例,在对象构造完成之前,不可以将其发布 * @author yangshangwei * */ @Slf4j @NotThreadSafe @NotRecommand public class ObjectEscapeDemo { private int thisCanBeEscape = 0; public ObjectEscapeDemo() { new InnerClass(); } private class InnerClass { // this引用的逸出 // 内部类的构造器里包含了对封装实例的隐含引用,这样在对象没有被正确构造完成之前就会被发布,由此会导致不安全的因素在里面 public InnerClass() { log.info("{}", ObjectEscapeDemo.this.thisCanBeEscape); } } public static void main(String[] args) { new ObjectEscapeDemo(); } }
上述代码中,内部类的构造器里包含了对封装实例的隐含引用,这样在对象没有被正确构造完成之前就会被发布,由此会导致不安全的因素在里面。
其中一个就是导致this引用在构造期间逸出的错误,它是在构造函数构造过程中启动了一个线程,无论是显式启动还是隐式启动,都会造成this引用的逸出。
新线程总会在所属对象构造完毕之前就已经看到它了,所以如果要在构造函数中创建线程,那么不要启动它,而是应该采用一个专有的start,或是其他初始化的方式统一启动线程。
这里其实我们可以使用工厂方法和私有构造函数来完成对象创建和监听器的注册等等来避免不正确的发布。
小结
不正确的发布可变对象导致的两种错误:
- 发布线程以外的所有线程都可以看到被发布对象的过期的值
- 线程看到的被发布对象的引用是最新的,然而被发布对象的状态却是过期的

