this逸出
this逸出就是:在对象还未实例化完成时,就能被其他对象锁获取(发布)。
什么是this逸出
对于一个类C来说,“外部方法”指的是行为不完全由类C规定的方法,包括其他类定义的方法,以及类C中可以被改写的方法。当把类C的对象传递给某个外部方法时,相当于发布了该对象,此时如果C的实例未完成实例化,就称为类C的实例的this逸出。最常见的“外部方法”使用场景是在构造器中生成内部类实例。
1 | 1 // 定义一个事件监听的接口 |
上述代码中存在两处this逸出,一个是33行、另一个是在41行,最后name的值取决于1115行、3036行、45~49行这三处,它们分别是:
- 11~15行:listener注册完成后多久才会调用到onEvent?
- 30~36行:线程t何时启动?
- 45~49行:thisEscape对象需要多久才能构造完成?
前两处在实际应用的过程中都有可能不是构造器能够控制的,无论是Runable还是EventListener,它们的本质是相同的:在构造器中初始化一个内部类的实例,导致this隐式的泄露。
避免this逸出
为了避免this逸出,有如下策略:
- 可以在构造器中创建线程,但不要直接启动该线程,应该确保在对象初始化完成后再启动该线程
- 只要将构造器设置为private,然后使用工厂方法发布对象,就一定不存在this逸出。
1 | // 基于工厂方法防止this引用逸出 |
线程封闭
共享的对象在堆中可以被所有线程访问到,所以会存在线程不安全的问题,但如果某个对象只能被单线程访问,就不存在线程安全问题。这种仅在单线程内访问对象、将某个对象封闭起来的技术称为线程封闭。
单线程写入volatile变量
在volatile变量上存在一种特殊的线程封闭,只要能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享volatile变量上执行“读取-修改-写入”操作,这种情况相当于将修改操作封闭在单个线程中,避免了竞态条件,并且volatile变量的可见性可以保证其他线程能够看到最新的修改。
栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中,只有通过局部变量才能访问对象,局部变量都存在于栈中,因此,它是线程安全的。例如:
1 | public int calculte(List<Integer> list) { |
ThreadLocal
维持线程封闭另一种做法是使用ThreadLocal,这个类能够使线程与对象关联起来,在线程的上下文都可以获取某一个对象,常用在服务器会话上下文变量的传递等场景下。详见ThreadLocal原理。
不变性
不可变的对象一定是线程安全的。不可变对象指的是:只有一种状态,且该状态在构造函数内完成,一旦对象构造完成,不再改变。
final关键字
final关键字用于构造不可变对象,final类型的域是不能修改的。与c/c++的constant常量有些相似;但JMM中,final关键字被增强了,还有另外一层语义:初始化过程是安全的,final域禁止处理器把final域的写重排序到构造函数之外,一个对象的final域的初始化一定在该对象初始化完成之前完成。
finnal域的写
原理图如下,在写final域b=2操作后,添加了一个storestore屏障,然后才是构造函数执行结束,而普通域a,则有可能重排序到构造函数执行后。
final域的读
同样的,还有一个loadload屏障用于读final域,初次读对象与读对象的final域之间有一个loadload屏障,一个对象的final域的初始化一定在该对象初始化完成之前完成。
final与this逸出
如果出现了this逸出(this逸出:对象在构造函数执行结束前就能被其他对象或线程获取),上述storestore屏障相当于失效了,因此,final的安全性建立在没有this逸出的前提下。
final域的安全性
在storestore、loadload屏障、没有this逸出的保证下,final关键字声明的域是可以安全发布的,一旦构造完成就不可变,且无法读取到未构造完的final域,对象为null时读取不到final域。
综上,final保证对象只能被初始化一次,且初始化过程是安全的:
1 | private final List<String> list = new ArrayList<>(16); |
然而,若final域引用的对象是可变的,这些被引用的对象可以被修改,还是存在线程不安全。比如下面的例子,虽然list不能被初始化两次,仍然可以修改list的内容。
1 | private final List<String> list = new ArrayList<>(16); |