面试官让我谈谈懒汉和饿汉,我一下子说出了六种单例模式的实现方式!

单例模式

  • 单例模式是一种创建模式,单例类负责自己创建自己的对象并且一个类只有一个实例对象,并且向整个系统提供这个实例.系统可以直接访问这个实例而不需要实例化
  • 单例模式的特点:
    • 单例类只有一个实例
    • 单例类必须自己创建自身的唯一实例
    • 单例类必须给其余系统对象提供创建的唯一实例

单例模式的实现方式

  • 单例模式要保证一个类只有一个实例,并且提供给全局访问,主要用于解决一个全局使用的类频繁创建和销毁的问题,通过判断系统是否存在这个单例来解决这样的问题,如果有这个单例则返回这个单例,否则就创建这个单例,只要保证构造函数是私有的即可
    • 保证一个类只有一个实例: 将该类的构造方法定义为私有方法即可
    • 提供全局一个该实例的访问点: 单例类自己创建实例,提供一个静态方法作为实例的访问点即可
  • 饿汉和懒汉比较:
    • 懒汉: 单例类对象实例懒加载,不会提前创建对象实例,只有在使用对象实例的时候才会创建对象实例
    • 饿汉: 在单例对象实例进行声明引用时就进行实例化创建对象实例
  • 单例模式除去线程不安全的懒汉,通常有五种实现方式:
    • 懒汉
    • 双检锁
    • 饿汉
    • 静态内部类
    • 枚举
  • 一般情况下,直接使用饿汉实现单例模式
  • 如果明确要求懒加载通常使用静态内部类实现单例模式
  • 如果有关于反序列化创建对象会考虑使用枚举实现单例模式
  • 静态类Static :
    • 静态类在第一次运行时直接初始化,也不需要在延迟加载中使用
    • 在不需要维持任何状态,仅仅用于全局访问时,使用静态类的方式更加方便
    • 如果需要被继承或者需要维持一些特定状态下的情况,就适合使用单例模式

线程不安全懒汉

线程安全懒汉

  • 单例模式线程安全懒汉Singleton示例
  • 解决了多线程环境下创建多个实例的问题
  • 存在每次获取实例都需要申请锁的问题,方法效率低下,因为在任何时候只能有一个线程可以调用getInstance() 方法

双检锁

  • 双重检查锁模式: doule checked locking pattern
    • 使用同步块加锁的方法
    • 会有两次检查instance == null
      • 一次在同步块外
      • 一次在同步块内
        • 因为会有多个线程一起进入同步块外的if
        • 如果不在同步块内不进行二次检验就会导致生成多个实例
  • 单例模式双检锁Singleton示例
  • volatile:
    • 对于计算机中的指令而言 ,CPU和编译器为了提升程序的执行效率,通常会按照一定的规则对指令进行优化
    • 如果两条指令互不依赖,那么指令执行的顺序可能不是源码的编写顺序
    • 形如instance = new Instance() 方法创建实例执行分为三步:
      • 分配对象内存空间: 给新创建的Instance对象分配内存
      • 初始化对象: 调用单例类的构造函数来初始化成员变量
      • 设置instance指向新创建的对象分配的内存地址,此时instance != null
        • 因为上面的初始化对象和设置instance指向新创建的对象分配的内存地址不存在数据上的依赖关系,无论哪一步先执行都不会影响最终结果,所以程序在编译时,顺序就会发生改变:
          • 分配对象内存空间
          • 设置instance指向新创建对象分配的内存地址
          • 初始化对象
        • CPU和编译器在指令重排时,不会关心指令重排执行是否影响多线程的执行结果. 如果不加volatile关键字,如果有多个线程访问getInstance() 方法时,如果刚好发生了指令重排,可能会出现以下情况:
          • 当第一个线程获取锁并且进入到第二个if方法后,先分配内存空间,然后instance指向刚刚分配的内存地址,此时instance不等于null. 但是此时instance还没有初始化完成
          • 如果此时有另一个线程调用getInstance() 方法,在第一个if的判断时结果就为false, 就会直接返回没有初始化完成的instance, 这样可能会导致程序NPE异常
    • 使用volatile的原因是禁止指令重新排序:
      • volatile变量进行赋值操作后会有一个内存隔离
      • 读操作不会重排序到内存隔离之中
      • 比如在上面操作中,读操作必须在执行完1,2,3或者1,3,2步骤之后才会执行读取到结果,否则不会读取到相关结果

饿汉

  • 单例模式饿汉Singleton示例
  • 优点:
    • 在单例类中,装载类的时候就创建对象实例.因为单例类的实例声明为staticfinal变量,在第一次加在类到内存中时就会初始化,所以创建实例本身时线程安全的
  • 缺点:
    • 饿汉模式不是一种懒加载模式,即便客户端没有调用getInstance() 方法,单例类也会在类第一次加载时初始化
    • 使用饿汉模式创建单例类实例在某些场景中无法使用:
      • 比如因为饿汉创建的实例声明为final变量
      • 如果单例类Singleton的实例的创建依赖参数或者配置文件
      • 需要在getInstance() 方法之前调用方法为单例类的实例设置参数,此时这种饿汉模式就无法使用

静态内部类

  • 单例模式静态内部类Singleton示例
  • 使用静态内部类模式创建单例类实例是使用JVM机制保证线程安全:
    • 静态单例对象没有作为单例类的成员变量直接实例化,所以当类加载时不会实例化单例类
    • 第一次调用getInstance() 方法时将加载静态内部类Nest. 在静态内部类中定义了一个static类型的变量instance, 这时会首先初始化这个变量
    • 通过JVM来保证线程安全,确保该成员变量只初始化一次
    • 由于getInstance() 方法并没有加线程锁,所以对性能没有什么影响
  • 静态内部类的优点:
    • 静态内部类Nest是私有的,只能通过getInstance() 方法进行访问,所以这是懒加载的
    • 读取实例时不会进行同步锁的获取,性能较好
    • 静态内部类不依赖JDK版本

枚举

  • 单例模式枚举Singleton示例
  • 使用枚举方式实现单例的最大特点是非常简单
  • 可以通过Enum.INSTANCE来访问实例,和getInstance() 方法比较更加简单
  • 枚举的创建默认就是线程安全的方法,而且能防止反射以及反序列化导致重新创建新的对象
    • Enum类内部使用Enum类型判定防止通过反射创建新的对象
    • Enum类通过对象的类型和枚举名称将对象进行序列化,然后通过valueOf() 方法匹配枚举名称找到内存中的唯一对象实例,这样可以防止反序列化时创建新的对象
  • 懒汉式和饿汉式实现的单例模式破坏 : 无论是通过懒汉式还是饿汉式实现的单例模式,都可能通过反射和反序列化破坏掉单例的特性,可以创建多个对象
  • 反射破坏单例模式: 利用反射,可以强制访问单例类的私有构造器,创建新的对象
public static void main(String[] args) {
  // 利用反射获取单例类的构造器
  Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
  // 设置访问私有构造器
  constructor.setAccessiable(true);
  // 利用反射创建新的对象
  Singleton newInstance = constructor.newInstance();
  // 通过单例模式创建单例对象
  Singleton singletonInstance = Singleton.getInstance();
  // 此时这两个对象是两个不同的对象,返回false
  System.out.println(singletonInstance  == newInstance);
}
  • 反序列化破坏单例模式: 通过readObject() 方法读取对象时会返回一个新的对象实例
public static void main(String[] args) {
  // 创建一个输出流对象
  ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
  // 将单例类对象写入到文件中
  Singleton singletonInstance = Singleton.getInstance();
  os.writeObject(singleton);
  // 从文件中读取单例对象
  File file = new File("Singleton.file");
  ObjectInputStream is = new ObjectInputStream(new FileInputStream(file));
  Singleton newInstance = (Singleton)is.readObject();
  // 此时这两个对象是两个不同的对象,返回false
  System.out.println(singletonInstance == newInstance);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容