安全发布对象
发布
简单来说就是提供一个对象的引用给作用域之外的代码。比如return
一个对象,或者作为参数传递到其他类的方法中。
逸出
如果一个类还没有构造结束就已经提供给了外部代码一个对象引用即发布了该对象,此时叫做对象逸出,对象的逸出会破坏线程的安全性。
1 | class UnsafeStates{ |
states
变量作用域是private
而我们在getStates
方法中却把它发布了,这样就称为数组states逸出了它所在的作用域。
然而更加隐蔽和需要我们注意的是this
逸出,这个问题要引起重点关注。观察以下代码:
1 | public class ThisEscape{ |
在构造方法中我们定义了一个匿名内部类,匿名内部类是一个事件监听类,当事件监听类注册完毕后,实际上我们已经将EventListener
匿名内部类发布出去了,而此时我们实际上已经携带了this
逸出,重点在于这个时候我们还有一些初始化工作没有做完,这也就是上面所说的,一个类还没有构造结束我们已经将发布了。
安全发布对象
安全发布对象,共有四种方法
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到
volatile
类型域或者AtomicReference
对象中 - 对象的引用保存到某个正确构造对象的
final
类型域中 - 将对象的引用保存到一个由锁保护的域中
单例模式
下面我们用各种单例模式来演示其中的几种方法:
懒汉式
懒汉式(最简式)
1 | public class SingletonExample { |
在多线程环境下,当两个线程同时访问这个方法,同时制定到instance==null
的判断。都判断为null
,接下来同时执行new
操作,这样类的构造函数被执行了两次。一旦构造函数中涉及到某些资源的处理,那么就会发生错误。所以说最简懒汉式是线程不安全的。
懒汉式(synchronized)
1 | //在类的静态方法上使用synchronized修饰 |
使用synchronized修饰静态方法后,同一时间只有一个线程访问该方,保证了线程安全性,但会造成性能损耗。
懒汉式(双重同步锁模式)
1 | public class SingletonExample { |
先判断一次后,再锁定整个类,再加上一次的双重同步锁,保证了最大程度上的避免耗损性能。
为什么要使用volatile
关键字修饰对象引用?
执行new操作的时候,CPU会进行了三次指令:
memory = allocate()
分配对象的内存空间ctorInstance()
初始化对象instance = memory
设置instance指向刚分配的内存
在程序运行过程中,CPU为提高运算速度会做出违背代码原有顺序的优化。我们称之为乱序执行优化或者说是指令重排。
那么上面知识点中的三步指令极有可能被优化为1、3、2的顺序。当我们有两个线程A与B,A线程遵从1、3、2的顺序,经过了两此instance
的空值判断后,执行了new
操作,并且cpu在某一瞬间刚结束指令3,并且还没有执行指令2。而在此时线程B恰巧在进行第一次的instance
空值判断,由于线程A执行完3指令,为instance
分配了内存,线程B判断instance
不为空,直接执行return
,返回了instance
,这样就出现了错误。
饿汉式
1 | public class SingletonExample { |
饿汉模式由于单例实例是在类装载的时候进行创建,因此只会被执行一次,所以它是线程安全的。如果构造函数中有着大量的操作要做,类的装载时间会很长。要是只做了类的构造,却没有使用构造好的对象,那么会造成资源浪费。所以饿汉模式适用于以下场景:私有构造函数在实现的时候没有太多的处理,占用系统资源少;这个类在实例化后很大概率会被使用。
枚举式
1 | public class SingletonExample { |
由于枚举类的特殊性,因为Java保证枚举类的每个枚举都是单例,上诉代码构造函数Singleton
方法只会被实例化一次,且是这个类被调用之前。对比懒汉与饿汉模式,它的优势很明显。