SINGLETON
- 核心作用:
- 保证一个类只有一个实例,并且提供一个访问该实例的全局访问点
- 常见场景:
- Windows的任务管理器
- Spring中的bean
- PROS:
- 只生成一个实例,减少系统开销
- 设置全局的访问点,优化共享资源访问
- 常见的五种单例模式:
- 饿汉模式、懒汉模式、DCL懒汉模式、静态内部类模式、枚举单例
一、饿汉模式
public class Demo1 {
// 私有化构造器
private Demo1() {}
// static : 只会生成一份实例,不存在并发问题
// 类初始化的时候立即加载该对象
private static Demo1 instance = new Demo1();
// 提供获取该对象的 方法,没有synchronized(考虑并发问题要加synchronized),效率高
public static Demo1 getInstance() {
return instance;
}
}
二、懒汉模式
public class Demo2 {
private Demo2() {}
// 类初始化时,不立即加载对象
private static Demo2 instance;
// 由于线程不安全,加上synchronized,使它成为一个同步方法
public static synchronized Demo2 getInstance() {
if (instance == null) {
// 线程 1 在这里等待
instance = new Demo2();
}
return instance;
}
}
为什么懒汉模式线程不安全?
- 假设有两个线程,线程1、线程2 ,当线程 1 在上面所示位置等待时,cpu突然停止使用线程 1,转而去调用线程2。线程2执行了instance == null后去new对象,当轮到线程1使用时,仍认为instance == null,就会再一次去new对象。
- 这种单例模式在并发数越高的情况下越容易产生问题
由于这种模式效率低下,于是出现了双重检测机制
三、双重检测机制(DCL)懒汉式
public class Demo3 {
private Demo3() {}
// volatile:当一个变量对它修改时,另一个变量对它修改时的缓存就失效了
// 加了volatile能避免指令的重排
private volatile static Demo3 instance;
public static Demo3 getInstance() {
if (instance == null) {
// 将锁范围缩小,提高性能
synchronized (Demo3.class) {
if (instance == null) {
// 其实锁住这里面,有一次判断为空就够了,
// 外面那个判断为空主要为了提高性能
instance = new Demo3();
}
}
}
return instance;
}
}
由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况(未加volatile的情况),分析如下:
instance = new Demo3();
这个步骤,在jvm里分三次执行:
- 在堆内存开辟空间
- 在堆内存实例化Demo3里面的各个参数
- 把对象指向堆内存空间
由于jvm存在乱序执行功能,所以可能在2没执行时就执行3,如果此时切换到其它线程,由于执行了3,instance已经非空了,会被直接拿来用,这样的话就会出现异常。这就是著名的DLC失效问题。
不过在jdk1.5之后,官方也发现了这个问题,在jdk1.6后,使用了volatile,定义为private volatile static Demo3 instance;就能解决DLC失效问题。volatile确保instance每次都在主内存中读取,这样会牺牲一点效率,但也无伤大雅。
四、静态内部类模式
public class Demo4 {
private Demo4() {}
// 静态内部类
private static class Inner {
private static final Demo4 instance = new Demo4();
}
public static Demo4 getInstance() {
return Inner.instance;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不会去初始化instance,故而不占内存。只有当getInstance()方法第一次被调用时,才会去初始化instance。
那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。
类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
- 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
- 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
- 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
那么instance在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:
- 虚拟机会保证一个类的
()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待,直到活动线程执行 ()方法完毕。如果在一个类的 ()方法中有耗时很长的操作,就可能造成多个进程阻塞 - 需要注意的是,其他线程虽然会被阻塞,但如果执行
()方法后,其他线程唤醒之后不会再次进入 ()方法。同一个加载器下,一个类型只会初始化一次。在实际应用中,这种阻塞往往是很隐蔽的。
故而,可以看出instance在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
缺点:
- 那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。
用反射破坏
class Demo4Test {
public static void main(String[] args) throws Exception {
Demo4 instance1 = Demo4.getInstance();
Constructor<Demo4> demo4Constructor = Demo4.class.getDeclaredConstructor(null);
demo4Constructor.setAccessible(true);
Demo4 instance2 = demo4Constructor.newInstance();
System.out.println(instance1 == instance2); //false
}
}
五、枚举单例
public enum Demo5 {
INSTANCE; //自动就是单例
public Demo5 getInstance() {
return INSTANCE;
}
}
class test {
public static void main(String[] args) {
Demo5 instance1 = Demo5.INSTANCE;
Demo5 instance2 = Demo5.INSTANCE;
System.out.println(instance1 == instance2); //true
}
}
六、总结:
- 饿汉式(线程安全,调用效率高,不能延时加载)
- 懒汉式(线程安全,调用效率不高,可以延时加载)
- 双重检测机制(DCL)懒汉式(线程安全,调用效率较高,可以延时加载)
- 饿汉改进:静态内部类(线程安全,调用效率高,可以延时加载)
- 枚举单例(线程安全,调用效率高,不能延时加载)
前面四种都可以被反射机制破坏
浙公网安备 33010602011771号