设计模式 | 单例模式
1.核心作用
保证一个类只有一个实例,提供一个访问该实例的全局访问点。
2.常见场景
3.单例模式的优点
4.常见的五种单例模式实现方式
4.1 饿汉式
步骤:
- 私有化构造器
- 类初始化时,立即加载对象【不涉及线程安全问题】
- 提供获取该对象的方法【没有Synchronized,效率高】
// 恶汉式
public class SingletonDemo1 {
private SingletonDemo1(){};
private static SingletonDemo1 instance= new SingletonDemo1();
public static SingletonDemo1 GetInstance(){
return instance;
}
}
class SingletonDemo1Test {
public static void main(String[] args) {
SingletonDemo1 instance1 = SingletonDemo1.GetInstance();
SingletonDemo1 instance2 = SingletonDemo1.GetInstance();
boolean b = instance1 == instance2;
System.out.println(b);
//输出:true
}
}
优点: 线程安全,调用效率高
缺点: 不能延时加载,如果长时间不用,会造成资源浪费
解决方案: 懒汉式
4.2 懒汉式
步骤:
- 私有化构造器
- 类初始化时,不立即加载对象
- 提供获取该对象的方法,在该方法上用Synchronized关键字,解决多线程冲突问题
//懒汉式
public class SingletonDemo2 {
//1. 私有化构造器
private SingletonDemo2(){};
//2. 类初始化时,不立即加载对象
private static SingletonDemo2 instance;
//3. 提供获取该对象的方法,在该方法上用Synchronized关键字,解决多线程冲突问题
public static synchronized SingletonDemo2 getInstance(){
if(instance!=null){
return new SingletonDemo2();
}
return instance;
}
}
class SingletonDemo2Test {
public static void main(String[] args) {
SingletonDemo2 instance1 = SingletonDemo2.getInstance();
SingletonDemo2 instance2 = SingletonDemo2.getInstance();
boolean b = instance1 == instance2;
System.out.println(b);
//输出:true
}
}
优点: 延时加载,即在调用方法时产生对象,解决恶汉式资源浪费的问题。
缺点: Synchronized关键字会使代码效率变低
解决方案: DCL懒汉式,即双重检测锁模式,解决代码效率问题。
4.3 DCL懒汉式(双重检测锁模式)
步骤:
- 私有化构造器
- 类初始化时,不立即加载对象
- 提供获取该对象的方法,用volatile关键字避免指令重排带来的问题;缩小Synchronized代码块范围,解决代码效率问题。
//DCL懒汉式(双重检测锁模式)
public class SingletonDemo3 {
//私有化构造器
private SingletonDemo3() {
}
//只提供一个实例,并不创建对象
//使用避免指令重排带来的线程安全问题
//volatile:对于同一个变量,在一个线程中值发生了改变,则在另一个线程中立即生效,可以大幅度避免下面的问题,不排除极端情况
private static volatile SingletonDemo3 instance;
//提供公共的获取方法,因为不是在类加载时就创建对象,因此存在线程安全问题,使用同步代码块提高效率
//现在不需要对整个方法进行同步,缩小了锁的范围,只有第一次会进入创建对象的方法,提高了效率
//当第一个线程执行到创建对象的方法时,但还未出方法返回,此时第二个线程进入,发现instance不为空,但第一个线程此时还未出去,可能发送意想不到的安全问题
public static SingletonDemo3 getInstance() {
if (instance == null) {
synchronized (SingletonDemo3.class) {
if (instance == null) {
instance = new SingletonDemo3();
}
}
}
return instance;
}
}
//测试
class SingletonDemo3Test {
public static void main(String[] args) {
SingletonDemo3 instance = SingletonDemo3.getInstance();
SingletonDemo3 instance1 = SingletonDemo3.getInstance();
System.out.println(instance == instance1); //输出true
}
}
问题:为什么需要两次判断if(singleTon==null)?
分析:
-
第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。
-
第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
-
需要注意的是, private static volatile SingletonDemo3 instance;需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
4.4 静态内部类
步骤:
- 私有化构造器
- 使用静态内部类(实现延迟加载)
- 提供获取该对象的方法
静态内部类:同样也是利用了类的加载机制,它与饿汉模式不同的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
public class SingletonDemo4 {
private SingletonDemo4(){};
private static class InnerClass{
private static final SingletonDemo4 instance=new SingletonDemo4();
}
public static SingletonDemo4 getInstance(){
return InnerClass.instance;
}
}
优点: 线程安全,调用效率高,可延时加载
缺点: 反射机制可以轻易破坏上述单例
class SingletonDemo4Test {
public static void main(String[] args) throws Exception {
SingletonDemo4 instance1 = SingletonDemo4.getInstance();
Constructor<SingletonDemo4> declaredConstructor = SingletonDemo4.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
SingletonDemo4 instance2 = declaredConstructor.newInstance();
System.out.println(instance1==instance2);
//输出:false
}
}
由上面的例子可以得出,反射是可以破坏以上四种的单例模式(这里不一一演示)
那怎样才能解决这个问题呢,我们来看一下反射创建对象的newInstance()方法:
从源码中可以看出,当反射遇到枚举时直接抛出异常,因此,枚举是创建单例的不二之选
4.5 枚举
原理:在反射的源码中,我们发现,反射天然屏蔽枚举类型,所以枚举的对象纯天然是单例的。
public enum SingletonDemo5 {
INSTANCE;
public SingletonDemo5 getInstance(){
return INSTANCE;
}
}
class SingletonDemo5Test {
public static void main(String[] args) {
SingletonDemo5 instance1 = SingletonDemo5.INSTANCE;
SingletonDemo5 instance2 = SingletonDemo5.INSTANCE;
boolean b = instance1 == instance2;
System.out.println(b);
//输出:true
}
}
优点: 线程安全,调用效率高,
缺点: 不能延时加载
5.五种实现单例模式的方式的对比
饿汉式:线程安全(不排除反射),调用效率高,不能延时加载
懒汉式:线程安全(不排除反射),调用效率不高,可以延时加载
DCL懒汉式:由于JVM底层模型原因,偶尔出现问题,不建议使用
静态内部类式:线程安全(不排除反射),调用效率高,可以延时加载
枚举单例:线程安全,调用效率高,不能延时加载,避免反射带来的问题。