设计模式 - 单例模式(饿汉,懒汉),就该这样学!

目录

一、何为单例模式?

二、饿汉模式

a)Java

b)Kotin

三、懒汉模式

a)Java 

b)Kotlin

四、懒汉和饿汉,谁线程安全,为什么?

五、如何修改,让懒汉模式也线程安全?

 面试题1:上图中的两个if一模一样,为什么要判断两遍?

面试题2:下图中的volatile有什么用?(懒汉模式完整代码)


一、何为单例模式?

        是一种给常见的设计模式,先来他谈谈何为设计模式,在代码领域里,很多程序员的水平参差不齐,于是就有大佬们根据一些常见的需求,整理出来的一些应对办法;那么单例就是指单个实例(对象),也就是说一个类只能有一个实例,例如,在中国,一个男人只能娶一个老婆是一个道理;

        单例模式,本质上就是借助变成语言的语法特性,强制限制某个类,不能创建多个实例。

        在Java中有些东西是天然的单例,例如static,他可以修饰成员/属性,也就是我们口中熟知的类成员/类属性,实际上这种叫法,也是有一定原因的,也就是这个类特有的成员和属性;更具体的来说,类对象是通过JVM针对某个.class文件只会加载一次,就只有一个类对象,包括类成员,都是靠static修饰,也就只有一份;


二、饿汉模式

a)Java

        这个模式表示一个类在加载的时候就创建好实例了,“饿汉”一词便体现出创建这个实例是非常急迫,非常早的;

来看看具体代码:

class Singleton{
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
    private Singleton(){

    }
}

分析:

         用private修饰就是为了防止在类外对Singleton实例进行修改,static修饰保证了这个类无论加载多少次,都可以保证这个实例的创建只被加载一次;这里的instance便是Singleton的唯一实例,在类加载的时候,便已经创建好了

         这里便限制了如何拿到instance这个实例——只能通过getInstance这个方法才能拿到这个实例;

         将构造方法设置为private,之后在类外,就无法通过new再实例对象了(如下图),所以之后要使用这个唯一的实例,只能通过getInstance方法获得;

b)Kotin

Kotlin 的 object 天然支持饿汉单例模式.  如下:

object Singleton

fun main() {

    val a = Singleton

}

反编译成 Java 代码如下:

public final class Singleton {
   @NotNull
   public static final Singleton INSTANCE;

   private Singleton() {
   }

   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}

public final class SingletonKt {
   public static final void main() {
      Singleton a = Singleton.INSTANCE;
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

Singleton 实例在 static 静态代码块中就已经初始化好了~


三、懒汉模式

a)Java 

        还有一种经典的单例模式叫“懒汉模式”,这里的“懒”是一个褒义词,创建实例来的更迟,但是效率更高;为什么效率更高呢?一般程序刚启动的时候,要初始化的东西有很多,系统资源紧张,所以构造实例这个过程实际上可以往后放放,当这个创建实例的过程晚了,跟其他耗时操作岔开了,初始化效率就高了,速度自然也就跟上了

代码如下:(跟多线程还没扯上关系,不是最优版本)

class SingletonLazy{
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}

分析:

         刚开始是并没有直接创建实例,而是通过赋值null一笔带过,在真正需要实例的时候,通过调用getInstance方法来获取实例,若instance为空的时候,才去创建实例,不为空的时候说明实例已经创建好,直接返回即可

懒汉模式最佳写法:

class SingletonLazy{
    private volatile static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        if(instance == null) {
            synchronized(SingletonLazy.class){
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

以面将会一步一步分析这样写的原因

b)Kotlin

硬编码的话这样的:

class SingletonLazy {

    companion object {
        @Volatile
        private var instance: SingletonLazy? = null
    }

    fun getInstance(): SingletonLazy {
        instance ?: synchronized(Singleton::class) {
            instance ?: { instance = SingletonLazy() }
        }
        return instance!!
    }

}

 但是 Kotlin 比较骚的是天然支持单例模式中的懒汉和饿汉模式,而且是线程安全的,如下:

class SingletonLazy2 {

    companion object {
        val instance: SingletonLazy2 by lazy { SingletonLazy2() }
    }

}


四、懒汉和饿汉,谁线程安全,为什么?

        咱们可以先来对比一下双方获取实例的方法:

饿汉模式

         这个操作只是单纯的“读操作”,不涉及修改;

懒汉模式

         这个操作即设计到读操作(装载,比较),也涉及到修改操作(new,赋值),在多线程情况下,就不安全了;

如下图:(创建对象简化为:NEW,赋值简化为:ASSGIN)

         分析(脏读):线程二在LOAD操作的时候,线程一还没有修改完,线程二读到还是旧数据,导致线程二CMP时,instance依然是null,所以依然可以NEW,最后实例就被创建了多份!而我们的需求是,让线程二读到的数据是线程一修改完后的数据...


五、如何修改,让懒汉模式也线程安全?

        把多个操作打包成一个操作(原子操作)——加锁;

如下代码:(还不是最高效的)

class SingletonLazy{
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        synchronized(SingletonLazy.class){
            if(instance == null){
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}

分析:

        通过这种加锁方式,就保证了线程安全(如下图)

        但是有引入了新的问题...

        每次都需要先加锁后,再判断是否创建实例,而加锁操作操作的开销实际上还挺大,加锁可能会涉及到 用户态->内核态 之间的切换,这样的切换成本是很高的,如何解决呢?再来分析一下刚刚的代码:懒汉式代码线程不安全,不是一直线程都不安全,而是在第一次调用的时候才会触发不安全,一旦实例创建好了,就不会有线程不安全问题了,也就是说,加锁操作,只需要在第一次调用的时候加锁即可!所以此时可以再加一个if语句;(如下图,还不是最高效,最后一张图才是)

 面试题1:上图中的两个if一模一样,为什么要判断两遍?

        第一个if是为了判断是否要加锁,第二个是用来判断是否创建实例,而这两个条件碰巧写法一样,但所表示的意义不同,为什么呢?这两代码中间隔着一个加锁操作,看起来是只隔了一行代码,实际上确隔了孙悟空一个跟头,加锁就可能产生竞争,竞争就会导致阻塞,一旦阻塞,什么时候唤醒就不知道了,所以再个阻塞时间里,一旦线程二创建instance实例,这个时候线程一的第二个if判断就不可省去,也就是说,第二个if和第一个if的结果可能是截然不同的,第一个if成立了,第二个if不一定成立;

面试题2:下图中的volatile有什么用?(懒汉模式完整代码)

 解释原因:

        假设这样一个场景,俩个线程同时调用getInstance方法,第一个线程拿到了锁,进入第二个if,开始new操作,而这里的new操作实际上是三步操作:

1.申请内存,得到内存的首地址;

2.调用构造方法,来初始化实例;

3.把内存的首地址赋值给instance引用;

        这样一个场景之下,可能就会触发指令重排序,例如执行顺序变成了1、3、2 ,再单线程下,3和2是可以相互调换顺序的,没有什么影响,但是在刚刚假设的场景之下调换了3和2的顺序,就有问题了;假设此时触发了指令重排序,按照1、3、2的顺序执行,那么如果线程2在线程1执行了1,3之后(得到了一个空的对象,只分配了内存,数据是无效的),执行2之前,调用了getInstance方法,这时线程2就会进入第一个if,判断instance为非空,就返回了instance,而后续操作一旦涉及到对instance进行访问,就会得到无效数据;这里便是指令重排序带来的问题,要解决这个问题,就需要用到volatile,这个关键字既能保证内存可见性,也能禁止指令重排序,上述代码,就完成了完整的单例模式的懒汉实现


  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是懒汉单例模式饿汉单例模式的异同: 相同点: 1. 都是单例模式,即保证一个类只有一个实例对象。 2. 都使用了私有的构造函数,防止外部创建实例对象。 3. 都使用了静态变量来保存实例对象。 不同点: 1. 创建对象的时机不同:饿汉在类加载时就创建了对象实例,而懒汉是在使用时才创建。 2. 线程安全性不同:饿汉天生是线程安全的,因为在类加载时就已经创建了对象实例,而懒汉需要考虑线程安全问题,可以使用synchronized关键字或者双重检查锁定等方来保证线程安全。 3. 性能不同:饿汉在类加载时就创建了对象实例,所以在访问速度和反应时间上都比懒汉快,但是如果这个实例一直没有被使用,那么就会造成内存浪费。而懒汉只有在使用时才会创建对象实例,所以在内存占用上比饿汉要低,但是在访问速度和反应时间上会稍微慢一些。 下面是懒汉单例模式的示例代码: ```python class Singleton: __instance = None def __init__(self): if Singleton.__instance != None: raise Exception("该类已经实例化过了") else: Singleton.__instance = self @staticmethod def getInstance(): if Singleton.__instance == None: Singleton() return Singleton.__instance ``` 下面是饿汉单例模式的示例代码: ```python class Singleton: __instance = Singleton() def __init__(self): if Singleton.__instance != None: raise Exception("该类已经实例化过了") else: Singleton.__instance = self @staticmethod def getInstance(): return Singleton.__instance ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

陈亦康

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

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

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

打赏作者

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

抵扣说明:

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

余额充值