详解23种设计模式之单例模式


提示:以下是本篇文章正文内容,下面案例可供参考

在这里插入图片描述

23.7.什么是单例模式:

单例模式是指在内存中仅创建一次对象的设计模式。

23.71.单例模式的类型:

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
23.72.两种模型的代码怎么写:
懒汉式:懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。
public class Singleton {
    
    private static Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    
}
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
public class Singleton{
    
    private static final Singleton singleton = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        return singleton;
    }
} 
23.722.大多数情况下,懒汉式使用更多,但是在多线程情况下懒汉式会有并发安全问题。

试想一下,如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了。所以,我们要解决的是线程安全问题。

public static synchronized Singleton getInstance() {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
}
// 或者    第一次判断的时候,进入第一个if的线程可能有很多,但抢到锁的只能有一个线程,如果synchronized代码块里面不判断一次的话,那么卡主的这些线程,进入到synchronized代码块时就都会创建新的对象。
public static Singleton getInstance() {
    if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
            if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}
这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。
接下来要做的就是优化性能,目标是:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例
所以直接在方法上加锁的方式就被废掉了,因为这种方式无论如何都需要先获取锁
23.7235.为什么要双重校验呢:

既然第二种方式已经看保证线程安全了,为什么需要从第二种方式变为第三种方式?或者为什么要使用双层同步锁?或者第三种方式 if (instance == null) + synchronized + if (instance == null) 的懒汉式是如何减少对同步锁的竞争?

回答:如果使用第二种方式,即第三种方式没有第一层if (instance == null) ,每次调用newInstance()方法都会先synchronized/lock 然后判断 if (instance == null) ,对于一共 n 次调用newInstance静态方法,对于第二层的 if (instance == null),只有第一次创建在为true,后面都是为false,不需要再次新建了,因为是单例,所以对于后面的 n-1 次调用newInstance,都是先获取到同步锁,然后 if(instance==null)为false,这样白白的消耗性能,后面的 n-1 次,每个线程辛辛苦苦的获取到的同步锁,发现没卵用,还不如不要获取同步锁,尝试的解决方法有两个:

① synchronized/lock + if (instance == null) 变为 if (instance == null) + synchronized/lock,但是这样修改后,第一批进入的线程破坏单例模式。

② synchronized/lock + if (instance == null) 变为 if (instance == null) + synchronized/lock + if (instance == null),在保证线程安全的第二种方式前面加一层 if (instance == null)判断,变为双层检测,这样在保证单例的情况下,提高效率,减少性能浪费。

问题:为什么说刚才的第一种方式 synchronized/lock + if (instance == null) 变为 if (instance == null) + synchronized/lock ,无法保证第一批进入的线程仅创建一个对象?

回答:虽然 if (instance == null) 实现后面调用阻止进入,提高了效率,同时 synchronized/lock 保证内部新建单例的原子性,但是由于没有内层 if (instance == null) ,第一批进入的每一个线程都会创建一个对象,破坏单例模式

23.723.懒汉式的代码还可能存在什么问题:了解吗:

还可能出现指令重排的问题,其实是这样的,创建一个对象,在JVM中会经过三步:

(1)为singleton分配内存空间

(2)初始化singleton对象

(3)将singleton指向分配好的内存空间

指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,

那么就有可能出现这样的一种低概率情况,线程1先判断singleton为null,抢到锁进入了同步代码块中,再次判断singleton为null,开始执行singleton = new Singleton();这行代码,执行的时候发生指令重排了,即执行完第一步以后,就执行了第三步,而恰好此时,线程2到了第一个if处,此时就出现了问题,对线程2来说,singleton并不是null,所以直接就返回了,但问题是此时的singleton指向的对象还没有初始化,就会报NPE异常。

23.733.那么最终的懒汉式单例模式该怎么写呢:

volatile可以禁止发生指令重排。使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。

public class Singleton {
    
    private static volatile Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getInstance() {
        if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
            synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
                if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}
23.8. 万恶的反射,
public static void main(String[] args) {
    // 获取类的显式构造器
    Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
    // 可访问私有构造器
    construct.setAccessible(true); 
    // 利用反射构造新对象
    Singleton obj1 = construct.newInstance(); 
    // 通过正常方式获取单例对象
    Singleton obj2 = Singleton.getInstance(); 
    System.out.println(obj1 == obj2); // false
}
通过结果可以看出,反射的方式创建了新的对象,这就破坏了单例模式
23.9. 有没有什么方法,能把反射也避免了。

用枚举类方式的单例模式,即满足前面的优点(线程安全,禁止指令重排),也可以避免这个万恶的反射机制,。

public enum Singleton {
     INSTANCE;
    //这只是一个业务方法。
     public void businessMethod() {
          System.out.println("我是一个单例!");
     }
}
//我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化
public class MainClass {
    public static void main(String[] args) {
        Singleton s1 = Singleton.INSTANCE;
        Singleton s2 = Singleton.INSTANCE;
        System.out.println(s1==s2);//true
    }
}   

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rCi5ILvi-1684935293071)(C:\Users\huawei\AppData\Roaming\Typora\typora-user-images\image-20230228130208390.png)]

上面就是枚举防止反射的原理:

23.96. 单例模式总结:
  1. 总结

(1)单例模式常见的写法有两种:懒汉式、饿汉式

(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题

(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加volatile关键字防止指令重排序

(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JH3073

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值