先来解释一下两个概念:
对象的发布: 将对象的引用返回给作用范围以外的代码使用
对象的逃逸: 错误的对象发布方式,在对象没有构造完成时,就把对象提供给其他线程使用,这种发布方式是不安全的对象发布的实例
对象的发布
看一个demo:
public class UnsafeObject {
private String[] value = {"a","b","c"};
//使用get发布对象
public String[] getValue() {
return value;
}
public static void main(String[] args) {
UnsafeObject unsafeObject = new UnsafeObject();
//启动另外一个线程,对value数组进行修改
new Thread(() -> {
String[] value1 = unsafeObject.getValue();
value1[0] = "d";
}).start();
//主线程等待一会,这里只是为了显现出来错误:其他线程有可能修改
//有可能比主线程快,有可能慢,所以打印的结果是随机的
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程打印value数组
String[] value = unsafeObject.getValue();
System.out.println(Arrays.toString(value));
}
}
demo中使用get方法把私有属性value数组返回出去,这就是对象的发布,在外面的代码也就是main方法拿到value数组时,同时又启动了一个线程,也就是两个线程同时对value数组进行操作,新线程对value数组进行了修改,而主线程在读取value数组,这时的结果是不确定的,有可能新线程没有修改完成主线程就读到了abc,有可能主线程读到的是修改后的dbc,所以这种对象的发布是不安全的
对象的逃逸
刚才在上面已经讲过了,对象的逃逸是错误对象发布的特例,指的是在对象没有完成构造的时候,就发布了对象。对象逃逸经常发生在构造函数中启动线程或注册监听器时,先来看一个demo
public class ThisEscape4 {
private int value;
private ThisEscape4(){
//新创建一个线程,直接访问了value,也就相当于发布了this对象
new Thread(() -> {
System.out.println(this.value);
}).start();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//未完成的构造函数逻辑
this.value = 1;
}
public static void main(String[] args) {
ThisEscape4 thisEscape4 = new ThisEscape4();
System.out.println(thisEscape4.value);
}
}
可以看到在构造函数中启动了一个线程,访问了this.value,这就相当于把this对象发布给了其他线程,其他线程这时访问了this对象,但是this对象并没有构造完成,在后面还有未完成的构造函数逻辑,这种情况就叫做对象的逃逸,也叫this逃逸
看一下运行情况:
对于这种情况,可以设置为私有构造,然后提供一个工厂方法,在工厂方法种启动线程即可,看一下改造后的demo:
public class ThisEscape5 {
private int value;
private static Thread thread;
private ThisEscape5() {
thread = new Thread(() -> {
System.out.println(value);
});
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.value = 1;
}
private static void init() {
thread.start();
}
public static ThisEscape5 getInstance() {
//调用私有构造
ThisEscape5 thisEscape5 = new ThisEscape5();
//启动线程
init();
return thisEscape5;
}
public static void main(String[] args) {
ThisEscape5 instance = ThisEscape5.getInstance();
System.out.println(instance.value);
}
}
改造完成后,新线程的启动总会在构造完成之后,这种发布对象的方式就是安全的,私有构造加上工厂方法,这其实就是 单例模式 的思想,看一下结果:
对象如何安全发布
在了解了单例模式之后,如何安全的发布的对象,可以分为四个方面:
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型域或者AtomicReference
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
1,2,4点就是单例模式的实现,而final是通过不变来保证对象的发布安全