单例设计模式

1. 单例模式概念

  • 单例模式(Singleton Pattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

2. 单例模式结构

  • 单例模式的主要有以下角色:

    • 单例类。只能创建一个实例的类
    • 访问类。使用单例类

3. 实现之一饿汉式

饿汉式:类加载就会导致该单实例对象被创建

  • 方式一:静态变量

    public class Singleton {//创建一个私有的无参构造方法private Singleton(){}//在成员位置创建该类的对象private static Singleton instance = new Singleton();//给外部提供一个静态方法获取该对象public static Singleton getInstance(){return instance;}}
    

    该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

  • 方式二:静态代码块的方式

    public class Singleton {//私有的无参构造方法private Singleton(){}//在成员位置创建该类的对象private static Singleton instance = null;//静态代码块static {instance = new Singleton();}//给外部提供一个静态方法获取该对象public static Singleton getInstance(){return instance;}
    }
    

    该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是随着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。

4. 实现之二懒汉式

懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

  • 方式一(线程不安全)

    public class Singleton {private Singleton(){}private static Singleton instance = null;public static Singleton getInstance(){//判断instance是否为空(是否为第一次创建)if (instance == null){instance = new Singleton();}return instance;}
    }
    

    该方式在成员位置**声明**Singleton类型的静态变量,并没有进行对象的赋值操作。当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题

  • 方式二(加锁,线程安全)

    public class Singleton {private Singleton(){}private static Singleton instance = null;//加锁机制保证线程安全public synchronized static Singleton getInstance(){if (instance == null){instance = new Singleton();}return instance;}
    }
    

    该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效率特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

  • 方式三(DCL双重锁机制)

    public class Singleton {private Singleton(){}private static Singleton instance = null;public static Singleton getInstance(){//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例if (instance == null){synchronized (Singleton.class){//抢到锁后,第二次判断if (instance == null){instance = new Singleton();}}}return instance;}
    }
    

    双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作

  • 方式四(带有volatile的DCL单例模式,使用 volatile 关键字, volatile 关键字可以保证可见性有序性)推荐使用

    public class Singleton {private Singleton(){}private static volatile Singleton instance = null;public static Singleton getInstance(){//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例if (instance == null){synchronized (Singleton.class){//抢到锁后,第二次判断if (instance == null){instance = new Singleton();}}}return instance;}
    }
    

    添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

  • 方式五(静态内部类方式)推荐使用

    public class Singleton {private Singleton(){}//静态内部类private static class SingletonBuilder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance(){return SingletonBuilder.INSTANCE;}
    }
    

    静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序

    第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonBuilder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性

    静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

5. 单例模式的破坏与解决方案

  • 上述创建单例模式的方式,有些是"安全"的,但所指的"安全"都是相对的,因为Java有个非常强大的机制:反射

  • 使用**序列化反射**可以让上面定义的单例类(Singleton)创建多个对象。

5.1 序列化反序列化

  • 演示序列化反序列化破坏单例模式

    //Singleton类
    public class Singleton implements Serializable {private Singleton() {}private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
    }
    
    public class DemoTest {public static void main(String[] args) throws Exception {//往文件中写对象writeObjectToFile();//从文件中读取对象Singleton instance1 = readObjectFromFile();Singleton instance2 = readObjectFromFile();System.out.println(instance1 == instance2)//false}private static Singleton readObjectFromFile() throws Exception {//创建对象输入流对象ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\Users\\Desktop\\a.txt"));//第一个读取Singleton对象Singleton instance = (Singleton) ois.readObject();return instance;}private static void writeObjectToFile() throws Exception {//获取Singleton类的对象Singleton instance = Singleton.getInstance();//创建对象输出流ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\Users\\Desktop\\a.txt"));//将instance对象写出到文件中oos.writeObject(instance);}}
    

    上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式

  • 解决方案:

    在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

    public class Singleton implements Serializable {private Singleton() {}private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}/*** 下面是为了解决序列化反序列化破解单例模式* 加readResolve方法,方法名固定不可变*/private Object readResolve() {return SingletonHolder.INSTANCE;}
    }
    
  • 源码解析:为什么readResolve()可以解决序列化破坏单例的问题?

    1. 我们从ObjectInputStream的readObject入手,readObject源码

    2. readObject0源码:重点关注方法内的switch语句

      checkResolve方法:检查对象并替换,readOrdinaryObject方法:读取二进制对象

    3. readOrdinaryObject源码:

      可以看到,readOrdinaryObject()方法是通过desc.isInstantiable() 来判断是否需要new一个对象,如果返回true,方法通过反射的方式调用无参构造方法新建一个对象;如果返回false,返回null。

    4. isInstantiable源码:

      cons是一个构造函数,cons ≠ null 是判断类的构造方法是否为空;但Class类的构造方法肯定不为空,所以isInstantiable()返回true,也就是说 obj 接收的一定是一个new的对象。

    5. readOrdinaryObject初始化后的源码:


      .

      要让判断语句为true,hasReadResolveMethod是关键。

    6. hasReadResolveMethod源码

      即readResolveMethod是否为null。

      对readResolveMethod赋值, 通过反射获得类中名为readResolve的方法。也就是说,如果目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类中的readResolve方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)

    7. 回到readOrdinaryObject初始化后的源码:

      当整个判断语句为true时,执行Object rep = desc.invokeReadResolve(obj);

      invoke的意思是调用,看到这大家应该就明白了。

      通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量

      这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。

5.2 反射

  • 演示反射破坏单例模式

    public class Singleton {private Singleton() {}private static volatile Singleton instance = null;public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
    }
    
    public class DemoTest {public static void main(String[] args) throws Exception{//获取Singleton类的反射对象Class clazz = Singleton.class;//获取Singleton类的私有构造方法Constructor constructor = clazz.getDeclaredConstructor();//设置取消访问检查constructor.setAccessible(true);//通过构造方法创建Singleton类的对象实例Singleton instance1 = (Singleton) constructor.newInstance();Singleton instance2 = (Singleton) constructor.newInstance();System.out.println(instance1 == instance2);//false}
    }
    

    上面代码运行结果是false,表明反射已经破坏了单例设计模式

  • 解决方案:

    在无参构造函数中加入判断

    public class Singleton {private static boolean flag = false;private Singleton() {//加锁解决多线程不安全synchronized (Singleton.class){//判断flag的值,为false说明第一次访问,可以创建;为true说明不是第一次访问,不能创建if (flag) {throw new RuntimeException("只能通过反射创建一次!");}//将flag的值设置为trueflag = true;}}private static volatile Singleton instance = null;public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
    }
    

6. 实现之三枚举类

  • 枚举类(枚举类也是一个类,继承了Enum)实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式

    public enum Singleton {INSTANCE;
    }
    
    public class DemoTest {public static void main(String[] args) {Singleton instance1 = Singleton.INSTANCE;Singleton instance2 = Singleton.INSTANCE;System.out.println(instance1 == instance2);//true}
    }
    
  • 思考:反射不能够破坏枚举类?换种说法,为什么不能通过反射创建枚举类对象?

    反射创建对象的过程:

    Class clazz = Singleton.class;Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);constructor.setAccessible(true);Singleton singleton = (Singleton) constructor.newInstance();
    

    核心是通过构造器的newInstacne方法,newInstacne方法的源码:

    如果反射对象是枚举类,则抛出IllegalArgumentException:无法通过反射创建枚举对象

彻底理解单例设计模式相关推荐

  1. python单例设计模式(待补充)

    要点概论: 1. 理解单例设计模式 2. 单例模式中的懒汉式实例化 3. 模块级别的单例模式 4. Monostate单例模式 5. 单例和元类 6.单例模式Ⅰ 7. 单例模式Ⅱ 8. 单例模式的缺点 ...

  2. Java笔记017-类变量和类方法、理解main方法语法、代码块、单例设计模式、final关键字

    目录 面向对象编程(高级部分) 类变量和类方法 类变量-提出问题 传统方法解决 问题分析: 类变量快速入门 类变量内存布局 什么是类变量 如何定义类变量定义语法: 如何访问类变量 定义语法: 类变量使 ...

  3. Java查漏补缺(08)关键字:static、单例设计模式、理解main方法、类的成员之四:代码块、final关键字、抽象类、接口、内部类、枚举类、注解、包装类

    Java查漏补缺(08)关键字:static.单例设计模式.理解main方法.类的成员之四:代码块.final关键字.抽象类.接口.内部类.枚举类.注解.包装类 本章专题与脉络 1. 关键字:stat ...

  4. java软件设计模式只单例设计模式

    概述 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. 毫无疑问,设计 ...

  5. Python单例设计模式

    (1)为什么使用设计模式? 使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. (2)单例设计模式概念 让类创建的对象只有唯一的一个实例,即每次执行  类名() 返回的对象,内存地 ...

  6. 小看--单例设计模式

    (一)单例设计描述 只要了解过设计模式的同学都会知道:单例设计模式,大家都知道单例设计模式是一种创建行的设计模式.既然是创建型,那么先来讲讲,对象的创建的过程吧. --静态成员:静态成员在程序加载的时 ...

  7. 【秒懂设计模式】单例设计模式

     秒懂设计模式--单例设计模式 (三)单例设计模式 1.先解释一下,什么是单例模式呢? 在Java中是这样定义的:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 显然从 ...

  8. static应用知识:单例设计模式

    1.什么是设计模式(Design pattern) 开发中经常遇到一些问题,一个问题通常有n种解法的,但其中肯定有一种解法是最优的,这个最优的解法被人总结出来了,称之为设计模式. 设计模式有20多种, ...

  9. java单例设计模式懒汉_Java设计模式之单例设计模式(懒汉、饿汉)

    [toc] Java设计模式之单例设计模式(懒汉.饿汉) 相信面试过的初中级Java开发的朋友可能都有遇到过单例设计模式的笔试题吧,如果之前没有背下来或者不理解,可以看看下面这篇文章,应该足够应付笔试 ...

最新文章

  1. S3C6410禁用和启用触摸屏
  2. 红帽杯——childRE
  3. Spring AOP 源码系列(一)解析 AOP 配置信息
  4. Docker最全教程——MongoDB容器化(十三)
  5. “约见”面试官系列之常见面试题之第八十九篇之vue生命周期作用(建议收藏)
  6. 小米10预计春节后见 售价超3500元没悬念
  7. xp 系统不能够通过网络访问解决方法
  8. NPAPI确实不安全,因为功能太强大
  9. DataType--数值类型
  10. tomcat设置独立jvm的例子
  11. 给力者基于51单片机的C语言教程,给力者单片机开发教程
  12. 开源图像识别、imageai图像识别、对象识别、识别人、车、猫、狗等80种 简易版
  13. Activiti 会签/或签 设计思路
  14. MFC 加载gif动态图片的方法
  15. Vanilla JavaScript 哈希 URL 路由器
  16. anaconda 创建虚拟环境
  17. LNMP 一键安装包配置 https
  18. tp-link设置为中继模式
  19. MATLAB定义变量小细节(1)
  20. LiteOS 知:简介

热门文章

  1. linux文件中提取某些字节,linux – 如何转储二进制文件的一部分
  2. 读取没开计算机硬盘,电脑开不了机读取不了硬盘。
  3. 游戏开发unity杂项知识系列:合理使用Unity的AssetStore
  4. 基于JAVA高校共享机房管理系统的设计与实现计算机毕业设计源码+系统+数据库+lw文档+部署
  5. 微信支付重复回调,java微信支付回调问题
  6. LFToolbox0.4官方使用文档说明
  7. 30 岁转行做程序员,晚了吗?
  8. 你好,机器作诗了解一下
  9. 人间仙境 绵山水涛沟秋季美景
  10. GPU配置MatConvNet(ECO代码)