单例与线程安全
大家先来回顾一下【双重校验单例模式】,单例实例在第一次使用时进行创建:
public class SingletonExample {
// 私有的默认构造方法,避免外部通过new创建对象。
private SingletonExample() {
}
// 定义单例对象,至少保证有一个对象被创建的。
private static SingletonExample singletonExample = null;
// 静态工厂方法
public static SingletonExample getInstance() {
// 双重检测机制
if (singletonExample == null) {
// 同步锁,判断对象不为空以后,锁着SingletonExample类
// synchronized修饰的内部,同一时间只能由一个线程可以访问的。
synchronized (SingletonExample.class) {
// 再次进行判断,如果singletonExample为空,就进行创建对象。
if (singletonExample == null) {
singletonExample = new SingletonExample();
}
}
}
return singletonExample;
}
}
此实现是,线程不安全的,我们来回顾一下创建对象的过程:
1、memory = allocate() 分配对象的内存空间
2、ctorInstance() 初始化对象
3、instance = memory 设置instance指向刚分配的内存
而实际过程中JVM和CPU优化,会发生指令重排:
1、memory = allocate() 分配对象的内存空间
3、instance = memory 设置instance指向刚分配的内存
2、ctorInstance() 初始化对象
所以当A线程执行完memory = allocate()
时,singletonExample
就不为null
了,B线程就会拿着singletonExample
作后续操作,而此时singletonExample
是还没有执行初始化的!!
解决办法:
将对象的引用保存到volatile
类型域或者AtomicReference
对象中。
public class SingletonExample {
// 1、memory = allocate() 分配对象的内存空间
// 2、ctorInstance() 初始化对象
// 3、instance = memory 设置instance指向刚分配的内存
// 私有的默认构造方法,避免外部通过new创建对象。
private SingletonExample() {
}
// 定义单例对象,至少保证有一个对象被创建的。
// 单例对象 volatile + 双重检测机制 -> 禁止指令重排
// volatile适用场景做状态标识量、双重检测,此处就是volatile的双重检测使用场景。
private volatile static SingletonExample singletonExample = null;
// 静态工厂方法
public static SingletonExample getInstance() {
// 双重检测机制
if (singletonExample == null) {
// 同步锁,判断对象不为空以后,锁着SingletonExample类
// synchronized修饰的内部,同一时间只能由一个线程可以访问的。
synchronized (SingletonExample.class) {
// 再次进行判断,如果singletonExample为空,就进行创建对象。
if (singletonExample == null) {
singletonExample = new SingletonExample();
}
}
}
return singletonExample;
}
}
安全发布对象
1、安全发布对象的发布与逃逸。
发布对象,使一个对象能够被当前范围之外的代码所使用。
对象逸出,一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见。
如果不正确的发布了可变对象,会造成两种错误,首先是发布线程以外的任何线程都可以看到被发布对象的过期的值。其次呢,线程看到的被发布对象的引用是最新的,然而呢,被发布对象的状态却是过期的,如果一个对象是可变对象,那么它就要被安全发布才可以。
2、安全发布对象的四种方式。
第一种,在静态初始化函数中初始化一个对象引用。
第二种,将对象的引用保存到volatile类型域或者AtomicReference对象中。
第三种,将对象的引用保存到某个正确构造对象的final类型域中。
第四种,将对象的引用保存到一个由锁保护的域中。
其他几种线程安全的单例模式
1、饿汉模式,单例实例在类装载时进行创建
线程安全,第一种,在静态初始化函数中初始化一个对象引用
如果单例类构造方法中没有过多的操作处理,是可以接受的
缺点:如果单例类构造方法中存在过多的操作处理,会导致该类加载的过慢。可能会引起性能问题。
public class SingletonExample {
// 私有的默认构造方法,避免外部通过new创建对象。
// 饿汉模式,私有构造方法没有过多处理。饿汉模式创建的对象肯定会在实际中被使用,不会造成资源浪费。
private SingletonExample() {
}
// 定义单例对象,至少保证有一个对象被创建的。在类装载的时候进行创建保证了线程的安全性。
private static SingletonExample singletonExample = new SingletonExample();
// 静态工厂方法
public static SingletonExample getInstance() {
return singletonExample;
}
}
2、懒汉模式 ,单例实例在第一次使用时进行创建
线程安全,第四种,将对象的引用保存到一个由锁保护的域中
缺点:方法加synchronized修饰,不推荐,虽然保证了线程安全性,但是带来了性能方面的开销。
public class SingletonExample {
// 私有的默认构造方法,避免外部通过new创建对象。
private SingletonExample() {
}
// 定义单例对象,至少保证有一个对象被创建的。
private static SingletonExample singletonExample = null;
// 静态工厂方法
// 使用synchronized修饰,方法内部所有实现同一时间内只能由一个线程访问。
// 因此可以保证线程安全的。
public static synchronized SingletonExample getInstance() {
if (null == singletonExample) {
singletonExample = new SingletonExample();
}
return singletonExample;
}
}
3、饿汉模式 ,单例实例在类装载时进行创建,是线程安全的。
public class SingletonExample {
// 私有的默认构造方法,避免外部通过new创建对象。
// 饿汉模式,私有构造方法没有过多处理。饿汉模式创建的对象肯定会在实际中被使用,不会造成资源浪费。
private SingletonExample() {
}
// 定义单例对象,至少保证有一个对象被创建的。在类装载的时候进行创建保证了线程的安全性。
private static SingletonExample singletonExample = null;
// 静态块初始化对象singletonExample
static {
singletonExample = new SingletonExample();
}
// 静态工厂方法
public static SingletonExample getInstance() {
return singletonExample;
}
}
4、枚举方式,线程安全,推荐的方式。
相比于懒汉模式,在安全性方面更容易保证,在饿汉模式,在安全性方面,在实际调用方面才可以初始化,不会造成资源的浪费。
public class SingletonExample {
// 私有的默认构造方法,避免外部通过new创建对象。
private SingletonExample() {
}
// 静态工厂方法
public static SingletonExample getInstance() {
return Singleton.INSTANCE.getInstance();
}
// 枚举类,私有的枚举类。
private enum Singleton {
// instance
INSTANCE;
// 私有的类的实例
private SingletonExample singletonExample;
// JVM保证这个方法绝对只调用一次
// 枚举类的构造方法
Singleton() {
singletonExample = new SingletonExample();
}
// 提供一个方法方便类来获取
public SingletonExample getInstance() {
// 返回枚举类里面的实例
return singletonExample;
}
}
}