设计模式篇(Java):单例模式

上一篇:设计模式篇(Java):前言(UML类图、七大原则)

四、单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

4.1 饿汉式

  • 构造器私有化 (防止 new )
  • 类的内部创建对象
  • 向外暴露一个静态的公共方法。getInstance
  • 代码实现

静态变量

class Singleton_01 {// 私有化构造器private Singleton_01() {}// 类内部构建对象private final static Singleton_01 instance = new Singleton_01();// 向外暴露一个对外的静态方法获取到示例public static Singleton_01 getInstance() {return instance;}
}
  • 优点:在类装载的时候就完成实例化。避免了线程同步问题
  • 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费

这种单例模式不可用,可能造成内存浪费

静态代码块

class Singleton_02 {private static Singleton_02 instance;static {instance = new Singleton_02();}private Singleton_02() {}// 向外暴露一个对外的静态方法获取到示例public static Singleton_02 getInstance() {return instance;}
}

这种单例模式优缺点和静态变量的一样。也是在类加载的时候进行实例化,可能会造成内存浪费的问题,也没有达到懒加载的效果。

4.2 懒汉式

线程不安全

// 线程不安全,只能单线程
class Singleton02_01 {private static Singleton02_01 instance;private Singleton02_01() {}// 向外暴露一个对外的静态方法获取到示例public static Singleton02_01 getInstance() {if (instance == null) {instance = new Singleton02_01();}return instance;}
}
  • 起到了Lazy Loading的效果,但是只能在单线程下使用。
  • 如果有多个线程进入到if中,就会产生多个示例。
  • 不推荐使用

同步方法线程安全

// 同步方法线程安全
class Singleton02_02 {private static Singleton02_02 instance;private Singleton02_02() {}// 向外暴露一个对外的静态方法获取到示例public static synchronized Singleton02_02 getInstance() {if (instance == null) {instance = new Singleton02_02();}return instance;}
}
  • 解决了线程安全的问题
  • 效率低,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例, 直接return就行了。方法进行同步效率太低。

同步代码块线程安全

// 同步代码块线程安全
class Singleton02_03 {private static Singleton02_03 instance;private Singleton02_03() {}// 向外暴露一个对外的静态方法获取到示例public static Singleton02_03 getInstance() {if (instance == null) {synchronized (Singleton02.class) {instance = new Singleton02_03();}}return instance;}
}
  • 此方法的单例看上去虽然是对同步方法线程安全的改进,但是改完之后多线程下就有可能破坏单例。
  • 如果两个线程同时进入到if内被阻塞住,那么最后会有两个或者多个实例。

双重检测

// 双重检测
class Singleton02_04 {private static Singleton02_04 instance;private Singleton02_04() {}// 向外暴露一个对外的静态方法获取到示例public static Singleton02_04 getInstance() {if (instance == null) {synchronized (Singleton02.class) {if (instance == null) {instance = new Singleton02_04();}}}return instance;}
}
  • 解决了效率低、线程安全等问题
  • 也达到了懒加载的效果
  • 推荐使用

其实双重检测也不是绝对安全的,因为instance = new Singleton02_04()不是一个原子性操作。

分析:instance = new Singleton02_04()不是一个原子性操作
instance = new Singleton02_04()的执行步骤
1、分配内存空间
2、执行构造方法,初始化对象
3、把这个对象指向这个空间
可能由于指令重排 把执行顺序变成 1-3-2
造成的结果:线程A还没有初始化对象,线程B获取对象是instance !=null就返回对象,此时instance 还没有完成构造

最终的DCL单例模式

// 双重检测
class Singleton02_04 {// + volatile 防止指令重排private volatile static Singleton02_04 instance;private Singleton02_04() {}// 向外暴露一个对外的静态方法获取到示例public static Singleton02_04 getInstance() {if (instance == null) {synchronized (Singleton02.class) {if (instance == null) {instance = new Singleton02_04();}}}return instance;}
}

看后面的4.5能发现DCL也不是绝对安全的

4.3 静态内部类

/*** 静态内部类* @author cVzhanshi* @create 2023-03-27 10:24*/
public class Singleton03 {// 私有化构造器private Singleton03() {}private static class SingletonInstance {private static final Singleton03 INSTANCE = new Singleton03();}// 向外暴露一个对外的静态方法获取到示例public static Singleton03 getInstance() {return SingletonInstance.INSTANCE;}
}
  • 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
  • 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
  • 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
  • 推荐使用

4.4 枚举

enum Singleton04_01 {INSTANCE;public void isOk() {System.out.println("ok");}
}
  • 能避免多线程同步问题,而且还能防止反序列化(反射)重新创建新的对象。
  • 推荐使用。

4.5 反射让单例不安全

单例不安全(因为反射)

  • 情况1:第一个对象通过类去得到,第二个对象通过反射通过构造器造对象,破坏单例

    • 代码示例:
    public class LazyMan {// 私有化构造器private LazyMan(){System.out.println(Thread.currentThread().getName() + "ok");}// + volatile 防止指令重排private volatile static LazyMan lazyMan;// 双重检测锁模式的懒汉式单例 --> DCL懒汉式public static LazyMan getInstance(){if(lazyMan == null){synchronized (LazyMan.class){if(lazyMan == null){lazyMan = new LazyMan(); // 不是一个原子性操作}}}return lazyMan;}public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {// 单线程下绝对正确且安全,但是在多线程下不安全LazyMan lazyMan = LazyMan.getInstance();Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);LazyMan lazyMan1 = declaredConstructor.newInstance();System.out.println(lazyMan);System.out.println(lazyMan1);}
    }
    

    在这里插入图片描述

    • 解决办法:可以在构造器中添加判断
    ...
    private LazyMan(){if(lazyMan != null){throw new RuntimeException("不要试图通过反射破坏单例");}System.out.println(Thread.currentThread().getName() + "ok");
    }
    ...//省略的代码和上面一样
    

    在这里插入图片描述

  • 情况二:两个对象都通过反射得到

    /*** @author cVzhanshi* @create 2021-09-26 10:22*/
    public class LazyMan {// 私有化构造器private LazyMan(){if(lazyMan != null){throw new RuntimeException("不要试图通过反射破坏单例");}System.out.println(Thread.currentThread().getName() + "ok");}// + volatile 防止指令重排private volatile static LazyMan lazyMan;// 双重检测锁模式的懒汉式单例 --> DCL懒汉式public static LazyMan getInstance(){if(lazyMan == null){synchronized (LazyMan.class){if(lazyMan == null){lazyMan = new LazyMan(); // 不是一个原子性操作}}}return lazyMan;}public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {// 单线程下绝对正确且安全,但是在多线程下不安全Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);LazyMan lazyMan = declaredConstructor.newInstance();LazyMan lazyMan1 = declaredConstructor.newInstance();System.out.println(lazyMan);System.out.println(lazyMan1);}
    }

    在这里插入图片描述

    原因:对象都通过反射得到,导致原类中的LazyMan没有被构造且一直为null,所以都能通过构造器里面的判断

    解决方案:设置一个红绿灯(一个标志,非当前对象)来判断

    /*** @author cVzhanshi* @create 2021-09-26 10:22*/
    public class LazyMan {private static boolean cvzhanshi = false;// 私有化构造器private LazyMan(){synchronized (LazyMan.class){if(cvzhanshi == false){cvzhanshi = true;}else{throw new RuntimeException("不要试图通过反射破坏单例");}}System.out.println(Thread.currentThread().getName() + "ok");}// + volatile 防止指令重排private volatile static LazyMan lazyMan;// 双重检测锁模式的懒汉式单例 --> DCL懒汉式public static LazyMan getInstance(){if(lazyMan == null){synchronized (LazyMan.class){if(lazyMan == null){lazyMan = new LazyMan(); // 不是一个原子性操作/*** lazyMan = new LazyMan();的执行步骤* 1、分配内存空间* 2、执行构造方法,初始化对象* 3、把这个对象指向这个空间* 可能由于指令重排 把执行顺序变成 1-3-2* 造成的结果:线程A还没有初始化对象,线程B获取对象是lazyMan!=null就返回对象,此时lazyMan还没有完成构造*/}}}return lazyMan;}public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {// 单线程下绝对正确且安全,但是在多线程下不安全Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);LazyMan lazyMan = declaredConstructor.newInstance();LazyMan lazyMan1 = declaredConstructor.newInstance();System.out.println(lazyMan);System.out.println(lazyMan1);}
    }
    

    在这里插入图片描述

  • 情况三:在二的基础上那个“红绿灯”被破解了,也通过反射进行修改,进而破坏单例

    ...
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {// 单线程下绝对正确且安全,但是在多线程下不安全Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);// 获取cvzhanshi属性Field cvzhanshi = LazyMan.class.getDeclaredField("cvzhanshi");cvzhanshi.setAccessible(false);declaredConstructor.setAccessible(true);LazyMan lazyMan = declaredConstructor.newInstance();cvzhanshi.set(lazyMan,false);LazyMan lazyMan1 = declaredConstructor.newInstance();System.out.println(lazyMan);System.out.println(lazyMan1);
    }
    ...//省略的代码和上面一样
    

    在这里插入图片描述

    查看newInstance方法,发现不能使用反射而破坏枚举的单例模式

    在这里插入图片描述

尝试通过反射,破坏枚举类的单例模式

  1. 正常取枚举类中的对象,确实是单例模式

    /*** @author cVzhanshi* @create 2021-09-26 15:10*/
    public enum EnumSingle {INSTANCE;public EnumSingle getInstance(){return INSTANCE;}
    }class Test{public static void main(String[] args) {EnumSingle instance1 = EnumSingle.INSTANCE;EnumSingle instance2 = EnumSingle.INSTANCE;System.out.println(instance1);System.out.println(instance2);}
    }
    

    在这里插入图片描述

  2. 通过查看枚举类编译的class文件,可以看到一个无参构造器

    package cn.cvzhanshi.single;public enum EnumSingle {INSTANCE;private EnumSingle() {}public EnumSingle getInstance() {return INSTANCE;}
    }
    
  3. 通过反射调用构造器构造对象,破坏单例

    /*** @author cVzhanshi* @create 2021-09-26 15:10*/
    public enum EnumSingle {INSTANCE;public EnumSingle getInstance(){return INSTANCE;}
    }class Test{public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {EnumSingle instance1 = EnumSingle.INSTANCE;Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);EnumSingle instance2 = declaredConstructor.newInstance();System.out.println(instance1);System.out.println(instance2);}
    }
    

    结果不尽人意,报错没有空参构造器

    在这里插入图片描述

    对class文件进行反编译查看代码,发现也有空参构造器

    在这里插入图片描述

    我们使用更专业的反编译工具jad.exe,查看源代码可知他是有参构造器

    结论:idea骗了我们

    public final class EnumSingle extends Enum
    {public static EnumSingle[] values(){return (EnumSingle[])$VALUES.clone();}public static EnumSingle valueOf(String name){return (EnumSingle)Enum.valueOf(com/ogj/single/EnumSingle, name);}private EnumSingle(String s, int i){super(s, i);}public EnumSingle getInstance(){return INSTANCE;}public static final EnumSingle INSTANCE;private static final EnumSingle $VALUES[];static {INSTANCE = new EnumSingle("INSTANCE", 0);$VALUES = (new EnumSingle[] {INSTANCE});}
    }
    
  4. 得知原因后继续通过反射通过构造器构造对象,破坏单例

    ....
    class Test{public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {EnumSingle instance1 = EnumSingle.INSTANCE;Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);declaredConstructor.setAccessible(true);EnumSingle instance2 = declaredConstructor.newInstance();System.out.println(instance1);System.out.println(instance2);}
    }
    ...//省略的代码和上面一样
    

    通过结果,我们得知枚举确实不能通过反射去改变单例模式

    在这里插入图片描述

4.6 jdk代码中的体现

JDK中的RunTime中使用了单例模式。

public class Runtime {private static Runtime currentRuntime = new Runtime();/*** Returns the runtime object associated with the current Java application.* Most of the methods of class <code>Runtime</code> are instance* methods and must be invoked with respect to the current runtime object.** @return  the <code>Runtime</code> object associated with the current*          Java application.*/public static Runtime getRuntime() {return currentRuntime;}/** Don't let anyone else instantiate this class */private Runtime() {}
}

看的出使用了饿汉式的单例模式

4.7 注意事项和细节说明

  • 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  • 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new
  • 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又**经常用到的对象、工具类对象、频繁访问数据库或文件的对象(**比如数据源、session工厂等)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/2142.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux系列P5】gccg++与【动静态库】的美妙邂逅

前言 大家好吖&#xff0c;欢迎来到 YY 滴 Linux系列 &#xff0c;热烈欢迎&#xff01;本章主要内容面向接触过Linux的老铁&#xff0c;主要内容含 欢迎订阅 YY 滴Linux专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 订阅专栏阅读&#xff1a;YY的《…

快速部署K8s仪表板,助力管理轻松搞定!

https://kubernetes.io/zh-cn/docs/tasks/access-application-cluster/web-ui-dashboard/ Dashboard 是基于网页的 Kubernetes 用户界面。 你可以使用 Dashboard 将容器应用部署到 Kubernetes 集群中&#xff0c;也可以对容器应用排错&#xff0c;还能管理集群资源。 你可以使…

CaffeineCache+Redis 接入系统做二层缓存思路实现(借鉴 mybatis 二级缓存、自动装配源码)

本文目录 前言本文术语本文项目地址设计思路开发思路DoubleCacheAble 双缓存注解&#xff08;如何设计&#xff1f;&#xff09;动态条件表达式&#xff1f;例如&#xff1a;#a.id?&#xff08;如何解析&#xff1f;&#xff09;缓存切面&#xff08;如何设计&#xff1f;&…

async异步任务_同步任务选项

需要先看完上文&#xff1a;async创建异步任务_御坂美琴1的博客-CSDN博客 让类里面的一个成员函数当作线程的参数。 async里面有三个参数&#xff0c;一个是成员函数的地址&#xff0c;第二个是 类&#xff0c;第三个是传入的参数。 接下来介绍async的同步线程创建。 asy…

华为云Classroom一站式教学实践平台,开启云端教学新征程

随着高考落下帷幕&#xff0c;各高校将迎来新一届大学新生入学&#xff0c;他们的学长学姐们经过四年的学习&#xff0c;也即将步入社会&#xff0c;迈向一段新的人生旅程。 在这里小智先祝大家未来一切顺意&#xff0c;不忘初心&#xff0c;大鹏一日同风起&#xff0c;扶摇直…

Android跨平台语言分析

跨平台技术发展的三个阶段 第一阶段是混合开发的web容器时代 为了解决原生开发的高成本、低效率&#xff0c;出现了Hybrid混合开发原生中嵌入依托于浏览器的WebViewWeb浏览器中可以实现的需求在WebView中基本都可以实现但是Web最大的问题是&#xff0c;它的性能和体验与原生开发…

需求分析六步法

需求收集可能看起来不言自明&#xff0c;但它很少得到应有的充分关注。就像运动前伸展或睡前刷牙一样&#xff0c;这是一项经常被忽视的简单任务。 但是&#xff0c;忽视这些看似简单的事情的后果可能会导致伤害、蛀牙&#xff0c;或者在项目管理的情况下&#xff0c;导致项目…

qt调用图片并自适应界面大小显示

一、前言 记录qt使用图片打开、查看和关闭等操作 实现点击按键&#xff0c;打开选择的指定位置图片&#xff0c;有缩放显示&#xff0c;并可以点击放大按键放大图片&#xff0c;放大后图片自适应电脑窗口大小&#xff0c;大于窗口尺寸会根据最大宽和高缩放&#xff0c;小于窗…

【系统开发】尚硅谷 - 谷粒商城项目笔记(五):分布式缓存

文章目录 分布式缓存缓存使用场景redis作缓存中间件引入redis依赖配置redis堆外内存溢出 缓存失效问题缓存穿透缓存雪崩缓存击穿 Redisson分布式锁导入依赖redisson配置类可重入锁读写锁缓存一致性解决 缓存-SpringCache简介Cacheable自定义缓存配置CacheEvictCachePut原理与不…

Redis主从/哨兵机制原理介绍

目录 ​编辑 一、主从复制 1.1 什么是主从复制 1.2 主从复制的作用 1.3 主从复制原理 1.3.1 全量复制 1.3.2 增量复制 1.3.3 同步流程 二、哨兵机制 2.1 哨兵机制介绍 2.1.1 集群逻辑图 2.1.2 哨兵机制实现的功能 2.2 哨兵机制原理 2.2.1 监控 2.2.2 下线 2.2.2.1 下线流程 2.…

HarmonyOS学习路之开发篇—AI功能开发(文档检测校正)

基本概念 文档校正提供了文档翻拍过程的辅助增强功能&#xff0c;包含两个子功能&#xff1a; 文档检测&#xff1a;能够自动识别图片中的文档&#xff0c;返回文档在原图中的位置信息。这里的文档泛指外形方正的事物&#xff0c;比如书本、相片、画框等。文档校正&#xff1a…

Midjourney使用教程:三 图片风格提示

这里我根据现在的官方文档来继续我们的Midjourney的教程&#xff0c;看到这里如果你去实践的话&#xff0c;估计你已经有了好多张属于自己的图片。 这时候你不在满足简单的提示生成的Midjourney的默认风格图片&#xff0c;实际上你可以通过一些关键词做提示&#xff0c;来改变…