什么是单例模式?

保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。

为什么要用单例模式?

1、单例模式节省公共资源

比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。

对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。

2、单例模式方便控制

就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,避免了这种多人干扰的问题出现。

实现单例模式的思路

1. 构造私有:

如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化。

2.以静态方法返回实例

因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。

下面的代码案例中均是使用的getInstance()方法来返回实例。

3.确保对象实例只有一个

只对类进行一次实例化,以后都直接获取第一次实例化的对象。


几种单例模式的演化(从漏洞百出到逐步完善)

(饿汉模式->懒汉模式(与之平行的是内部类模式)->枚举类)


1、饿汉模式

饿汉模式的意思是,我先把对象(面包)创建好,等我要用(吃)的直接直接来拿就行了。

package single;// 饿汉单例(一开始就加载)
public class a_Hungry {// 可能会浪费空间private byte[] data1 = new byte[1024 * 1024];private byte[] data2 = new byte[1024 * 1024];private byte[] data3 = new byte[1024 * 1024];private byte[] data4 = new byte[1024 * 1024];// 构造器私有化private a_Hungry() {}private final static a_Hungry HUNGRY = new a_Hungry();public static a_Hungry getInstance() {return HUNGRY;}
}

上面的案例就是使用的饿汉模式。 这种模式是最简单最省心的,不足的地方是容易造成资源上的浪费(比如:我事先把面包都做好了,但是你并不一定吃,这样容易造成资源的浪费)。

比如代码中标注可能会浪费空间后面的那四条代码。无论你是不是要使用该类对象,data1-data4已经占用了内存空间。


2.1、懒汉模式(最简单情况下)

因为饿汉模式可能会造成资源浪费的问题,所以就有了懒汉模式,

懒汉模式的意思是,我先不创建类的对象实例,等你需要的时候我再创建。

package single;// 懒汉单例(多线程不安全)
public class b_Lazy {// 构造器私有化private b_Lazy() {System.out.println(Thread.currentThread().getName() + "——OK");}private static b_Lazy b_lazy;public static b_Lazy getInstance() {if (b_lazy == null) {b_lazy = new b_Lazy();}return b_lazy;}// 多线程并发public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {b_Lazy.getInstance();}).start();}}
}

但很明显,这种最简单的懒汉模式在多线程并发情况下,会出现线程不安全的问题。所以会有以下DCL模式改进。


2.2、懒汉模式(DCL模式)

package single;// 懒汉单例(多线程不安全)
public class b_Lazy_DCL {// 构造器私有化private b_Lazy_DCL() {System.out.println(Thread.currentThread().getName() + "——OK");}private static b_Lazy_DCL b_lazy;// 双重检测锁模式 懒汉式单例 CDL模式public static b_Lazy_DCL getInstance() {if (b_lazy == null) {synchronized (b_Lazy.class) {if (b_lazy == null) {b_lazy = new b_Lazy_DCL();}}}return b_lazy;}// 多线程并发public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {b_Lazy_DCL.getInstance();}).start();}}
}

如代码中所示,在返回实例的方法getInstance()方法中,进行了双重检测,首先判断实例是否被创建,如果没有则会通过synchronized关键字来上锁,并开始创建实例。

但这种DCL模式下还是有指令重排的问题,什么是指令重排和如果解决,办法如下。


2.3、懒汉模式(DCL模式完善)

package single;// 懒汉单例(多线程不安全)
public class b_Lazy_DCL_issue {// 构造器私有化private b_Lazy_DCL_issue() {System.out.println(Thread.currentThread().getName() + "——OK");}private volatile static b_Lazy_DCL_issue b_lazy;// 双重检测锁模式 懒汉式单例 CDL模式public static b_Lazy_DCL_issue getInstance() {if (b_lazy == null) {synchronized (b_Lazy.class) {if (b_lazy == null) {b_lazy = new b_Lazy_DCL_issue();    // 不是原子性操作/*问题1、分配内存空间2、执行构造方法,初始化对象3、把这个对象指向这个空间(占用空间)指令重排理想状态:123可能状态:132 A线程先到了3B线程判断为!null,直接return了,但b_lazy还没有完成构造。*/// 解决办法:b_lazy避免指令重排加上volatile}}}return b_lazy;}// 多线程并发public static void main(String[] args) {for (int i = 0; i < 10; i++) {new Thread(() -> {b_Lazy_DCL_issue.getInstance();}).start();}}
}

问题如注释,而解决指令重排的办法就是在属性前加上volatile关键字。

到此,在正常情况下是没问题的。但Java中著名的反射机制,依旧可以破坏上面的单例模式。


2.4、懒汉模式(解决反射的破坏)

package single;import java.lang.reflect.Constructor;// 如果初始化是正常初始化没有问题
public class b_Lazy_DCL_reflect {// 构造器私有化private b_Lazy_DCL_reflect() {// 解决暴力反射破坏单例(第三把锁)synchronized (b_Lazy_DCL_reflect.class) {if (b_lazy != null) {   // 已经创建了,这次是来搞破坏的反射throw new RuntimeException("异常——不要试图使用反射破坏单例");}}}private volatile static b_Lazy_DCL_reflect b_lazy;// 双重检测锁模式 懒汉式单例 CDL模式public static b_Lazy_DCL_reflect getInstance() {if (b_lazy == null) {synchronized (b_Lazy.class) {if (b_lazy == null) {b_lazy = new b_Lazy_DCL_reflect();}}}return b_lazy;}// 暴力反射public static void main(String[] args) throws Exception {b_Lazy_DCL_reflect instance1 = b_Lazy_DCL_reflect.getInstance();Constructor<b_Lazy_DCL_reflect> declaredConstructor = b_Lazy_DCL_reflect.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);    // 无视了私有的构造器b_Lazy_DCL_reflect instance2 = declaredConstructor.newInstance();System.out.println(instance1 == instance2);}
}

main方法中,instance1是规范下通过getInstance()方法创建实例,但instance2是在反射破坏掉单例模式下创建的,instance2是该类新的一个实例。

而解决办法就是在构造器中加上第三把锁,拒绝掉通过反射创建的新的实例对象。

但依旧会有漏洞,如果main方法中的第一个实例就是通过反射机制来创建的,上面的单例还是被破坏掉了。解决办法如下。


2.5、懒汉模式(通过设置标志位判断是否是反射创建实例)

package single;import java.lang.reflect.Constructor;// 简单三把锁被反射初始化破坏
// 解决办法:通过标志位,强调必须用反编译允许程序
public class b_Lazy_DCL_reflect2 {// 红绿灯private static boolean single = false;// 构造器私有化private b_Lazy_DCL_reflect2() {// 解决暴力反射破坏单例(第三把锁)synchronized (b_Lazy_DCL_reflect2.class) {if (single == false) {single = true;} else {throw new RuntimeException("异常——不要试图使用反射初始化破坏单例");}}}private volatile static b_Lazy_DCL_reflect2 b_lazy;// 双重检测锁模式 懒汉式单例 CDL模式public static b_Lazy_DCL_reflect2 getInstance() {if (b_lazy == null) {synchronized (b_Lazy.class) {if (b_lazy == null) {b_lazy = new b_Lazy_DCL_reflect2();}}}return b_lazy;}// 暴力反射public static void main(String[] args) throws Exception {
//        b_Lazy_DCL_reflect2 instance1 = b_Lazy_DCL_reflect2.getInstance();    // 还是被破坏掉了Constructor<b_Lazy_DCL_reflect2> declaredConstructor = b_Lazy_DCL_reflect2.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);    // 无视了私有的构造器b_Lazy_DCL_reflect2 instance1 = declaredConstructor.newInstance();b_Lazy_DCL_reflect2 instance2 = declaredConstructor.newInstance();System.out.println(instance1 == instance2);}
}

这里的机制就是,反射是不会反编译该程序,直接跳到创建实例对象,因此也就不会改变标志位的内容。而正常情况下创建该类实例,则会改变标志位内容。

所有加上标志位single并在构造方法中判断标志位是否为true来判断第一次是否是通过反射创建实例。

但反射是强大的,你可以通过设置标志位来加密,也就有可能会出现通过反射来解密并且修改你的标志位内容来破坏你的单例。

解决反射的最终办法,通过枚举类来创建单例模式。


3、枚举类(一个字,强大)

package single;import java.lang.reflect.Constructor;// enum本身也是一个Class类
public enum d_EnumSingle {INSTANCE;public d_EnumSingle getInstance() {return INSTANCE;}
}class Test {public static void main(String[] args) throws Exception {d_EnumSingle instance1 = d_EnumSingle.INSTANCE;Constructor<d_EnumSingle> declaredConstructor = d_EnumSingle.class.getDeclaredConstructor(String.class, int.class);declaredConstructor.setAccessible(true);d_EnumSingle instance2 = declaredConstructor.newInstance();System.out.println(instance1 == instance2);}
}

枚举类本身也是Class,枚举是一种特殊的数据类型,之所以特殊是因为它既是一种类(Class)类型却又比类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁,安全性以及便捷性。

代码中可见,已经通过反射来准备迫害单例,,但最终被枚举类的机制判断出,并进行了阻止。


额外,也就是通过内部类创建单例模式

package single;// 静态内部类
public class c_Holder {// 构造器私有化private c_Holder() {}public static c_Holder getInstance() {return InnerClass.HOLDER;}public static class InnerClass {private static final c_Holder HOLDER = new c_Holder();}
}

静态内部类原理:

当外部内被访问时,并不会加载内部类,所以只要不访问InnserClass这个内部类,private static final c_Holder HOLDER = new c_Holder()就不会实例化,这就相当于实现懒加载的效果,只有当InnerClass.HOLDER被调用时访问内部类的属性,此时才会将对象进行实例化,这样既解决了恶汉模式下可能造成资源浪费的问题,也避免了了懒汉模式下的并发问题。


到此,单例模式算是基本说全了,其实在没有写真实项目前,我也一直没有明白保护单例模式的作用具体是什么,希望未来在工作中能够逐渐了解。

软件设计模式——单例模式相关推荐

  1. 软件设计模式“单例模式”和“工厂模式”

    软件设计模式"单例模式"和"工厂模式" 单例模式 什么是单例模式 单例模式的实现方式有哪些 单例模式的优缺点 单例模式的应用场景 总结 工厂模式 什么是工厂模式 ...

  2. 软件设计模式—单例模式

    前篇--软件设计模式-基础 前篇--软件设计模式-三种工厂模式 前篇--软件设计模式-装饰者模式 单例模式是创建型模式 目录 1.定义及理解 1.1 定义 1.2 特点 1.3 类图 1.3.1结构说 ...

  3. 软件设计模式—命令模式

    前篇--软件设计模式-基础 前篇--软件设计模式-三种工厂模式 前篇--软件设计模式-装饰者模式 前篇--软件设计模式-单例模式 前篇--软件设计模式-原型模式 命令模式是对象行为型模式 目录 1. ...

  4. 软件设计模式之单例模式

    设计模式之单例模式 定义 保证一个类仅有一个实例,并提供一个全局访问点 类型 创建型 使用场景 想确保任何情况下都绝对只有一个实例 优点 在内存里只有一个实例,减少了内存开销. 可以避免对资源的多重占 ...

  5. Python设计模式-单例模式

    Python设计模式-单例模式 基于Python3.5.2,代码如下 #coding:utf-8 import threading import timeclass Singleton(object) ...

  6. 【学习笔记】ABAP OOD设计模式 - 单例模式

    ABAP OOD设计模式 - 单例模式 整理转自-<SAP ABAP 面向对象程序设计(原则.模式及实践)> 单例模式(Singleton Pattern)是常用的且较为简单的软件设计模式 ...

  7. Go 语言实现 23 种设计模式 单例模式

    Go 语言实现 23 种设计模式 单例模式 单例模式 单例模式是一种常用的软件设计模式,在使用过程中,单例对象的类只有一个实例.使用单例模式,1 可以节省内存等资源,例如windows操作系统的资源管 ...

  8. 【系统架构设计师】软考高级职称,一次通过,倾尽所有,2016年下半年系统架构设计师考试论文真题(论述软件设计模式技术及应用)

    [系统架构设计师]软考高级职称,一次通过,倾尽所有,看完这篇就够了,学习方法和技巧这里全都有. 2016年下半年系统架构设计师考试论文真题(论述软件设计模式技术及应用) 论软件设计模式及其应用 软件设 ...

  9. 设计模式----创建型设计模式(单例模式、工厂方法模式、构建者模式)

    创建型设计模式 单例模式(Singleton Pattern) 单例模式介绍 代码演示 饿汉式(静态常量) 饿汉式(静态代码块) 懒汉式(线程不安全) 懒汉式(线程安全,同步方法) 懒汉式(线程安全, ...

最新文章

  1. 【Redfin SDE intern】跪经
  2. 图像几何变换:旋转,缩放,斜切
  3. SpringBoot中用itext实现PDF导出时实现循环添加元素
  4. 区位码\机器码\内码关系
  5. 简单调试 Bash 脚本
  6. linux bind命令,LINUX命令bind-系统管理-显示或设置键盘按键与其相关的功能
  7. SpringBoot文件上传下载
  8. 算法 --- 阿克曼(Ackmann)函数
  9. 2003服务器系统pe,SERVER 2003 PE(移动存储PE系统)v16.68免费版
  10. linux(所有版本)下安装有道词典
  11. 远程服务器 检索{00024500-0000-0000-C000-000000000046}组件失败 80080005 服务器运行失败 解决方案
  12. and5.1PowerManagerService深入分析(三)updatePowerStateLocked函数
  13. ImportError: cannot import name ‘structural_similarity‘ from ‘skimage.measure‘
  14. 在高分辨率或者扩展屏下微信截图出现放大问题的解决
  15. 【C语言】之实现回文数判断
  16. 2022苹果春季发布会带来新款iPhoneSE价格预计2500左右
  17. 如何用word制作英语答题卡_考研英语答题卡模板(word打印版)
  18. spec006 使用
  19. win10系统打开tftp服务器,大神演示win10系统开启TFTp的问题
  20. 邮件代发、国外邮件代发、外贸邮件代发平台,送达率、打开率高的秘密

热门文章

  1. 双主机切换下导致的显示器闪动
  2. GetAsyncKeyState函数中按键的信息
  3. 服务器上搭建git仓库
  4. Android IPC 机制详解:IBinder
  5. Mac有滚动截图工具吗?----解救 MAC 使用者们的高效截图工具- Xnip
  6. 脚注交叉引用序号不一样_不为人知的Word交叉引用设置
  7. BVT BAT (版本验证测试和版本验收测试)
  8. 【WLAN】【测试】盘点如何查看系统连接过的WIFI密码(包括手机、电脑及不同系统)
  9. 随记--做一个“懒惰”的程序员
  10. 【Linux】动态库与静态库