单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式
/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static final Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
枚举方式
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}
枚举类型是线程安全的,并且只会装载一次。枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
懒汉式
/**
* 懒汉式
* 线程安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是对于getInstance()
方法来说大部分操作都是读操作,在getInstance()
方法上直接添加synchronized关键字,会导致并行的读操作变成串行,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
双重检查锁模式
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。
为什么要进行两次
instance == null
判断?
第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例。否则进入抢锁阶段,此时一个线程获得锁,其余多个线程等待,获得锁的线程创建出实例,剩下的线程获得锁后不能再创建实例,所以要再次判断instance是否为null。
为什么要使用volatile
关键字?
在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。volatile
关键字可以保证可见性和有序性。参考
静态内部类方式(推荐)
/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
小结:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
补充
什么是指令重排?
一个类在实例化的时候,会要经历如下几步:
- 开辟空间
- 初始化
- 赋值引用
COPYmemory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置 instance 指向刚分配的内存地址
类的初始化和赋值引用是可能出现指令重排的,如果先进行赋值引用,则将导致空指针问题,因为此时类尚未初始化。
volatile 关键字特征
可见性,当一个共享变量被 volatile
修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
有序性,被 volatile
修饰的变量不会被 jvm 指令重排;