大家好,我是三叔,很高兴这期又和大家见面了,一个奋斗在互联网的打工人。
笔者曾在深入理解JMM中介绍过线程安全问题中说到,final 关键字也可以保证可见性,但是final关键字在多线程中一定是线程安全的吗? 并不是!
- 被final修饰的字段在构造器中一旦被初始化完成,并且构造器把 this 的引用传递出去,this引用逃逸是非常危险的事情,会造成引用字段在初始化一半的对象被其它线程引用,那么其它线程中就能看见final字段的值。
- 如果 final 变量引用的是一个可变对象(例如一个集合或数组),那么在多个线程同时访问该对象时,仍然可能会出现线程安全问题。虽然 final 变量本身不能被修改,但是该变量所引用的对象的状态仍然可以被修改。这可能导致数据竞争和其他线程安全问题。
在编写多线程代码时,应该使用其他机制来确保线程安全性,例如同步块、锁、并发集合等。final 变量可以作为一种附加保证,但它本身并不能保证线程安全。
那么什么是this逃逸
this 引用逃逸指的是在对象的构造函数中,将对象的 this 引用作为参数传递给其他方法或线程。在这种情况下,其他方法或线程可能会在对象完全构造之前使用该引用,这可能会导致不可预期的行为或线程安全问题。(笔者的这篇博客介绍过对象的创建过程,为什么对象的创建在多线程环境下是不安全的)
public class Demo {
// final修饰的int类型的变量value
private final int value;
// 构造函数
public Demo() {
// Demo类的构造函数中使用了 this 引用并将其传递给了 OtherClass.doSomething() 方法。这个方法可能会在对象完全构造之前就访问 this 引用中的成员变量 value。
// 由于 Java 中对象的构造过程是非常复杂的,对象的状态可能在构造函数中的不同阶段发生变化,因此在对象完全构造之前访问成员变量可能会导致不可预期的行为或线程安全问题。
OtherClass.doSomething(this);
value = 1;
}
public int getValue() {
return value;
}
}
class OtherClass {
public static void doSomething(Demo demo) {
// 使用 Demo 中的 value
int value = demo.getValue();
// ...
}
}
具体来说,如果在对象的构造函数中,将对象的 this 引用作为参数传递给其他方法或线程,并且该方法或线程在对象完全构造之前使用该引用,那么就会发生 this 引用逃逸。这可能会导致访问未初始化的对象状态或在不同的线程之间共享未初始化的对象状态,从而导致线程安全问题。
为了避免 this 引用逃逸,要遵循以下几个原则:
- 尽可能将对象的初始化过程限制在构造函数中,避免在构造函数中将 this 引用传递给其他方法或线程。
- 避免在对象的构造函数中启动新的线程或注册回调函数,因为这些操作可能会导致 this 引用逃逸。
- 将对象的构造函数声明为私有的,并使用工厂方法来创建对象。这可以确保对象完全构造之后才能使用它。
- 在对象的构造函数中使用同步机制来保护共享的状态,以避免在对象未完全构造之前就共享状态。
避免this逃逸的方法
使用工厂方法
通过将构造函数声明为私有的,并添加一个公共的静态工厂方法,来控制对象的创建和初始化过程。在工厂方法中,可以确保对象在完全构造之后才会被传递给其他代码或线程。
SafeObj类的构造函数被声明为私有的,并添加了一个名为 createBean() 的工厂方法来创建对象。createBean() 方法确保对象在完全构造之后才会将其传递给 OtherClass.doSomething() 方法。由于对象完全构造之后才会被传递,因此 this 引用逃逸的问题被避免了。
public class SafeObj {
private final int value;
// 私有化构造方法
private SafeObj(int value) {
this.value = value;
}
// createBean() 方法确保对象完全构造之后才会将其传递给 OtherClass.doSomething() 方法。这可以避免 final 关键字的 this 引用逃逸的问题。
public static SafeObj createBean() {
SafeObj obj = new SafeObj(1);
OtherClass.doSomething(obj);
return obj;
}
public int getValue() {
return value;
}
}
class OtherClass {
public static void doSomething(SafeObj obj) {
// 使用 obj 中的 value
int value = obj.getValue();
// ...
}
}