单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到很多东西,比如:多线程是否安全?是否懒加载?性能等等。还有你知道几种单例模式的写法呢?如何防止反射破坏单例模式?

一、 单例模式

1.1 定义

单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象。有点像 Java 的静态变量,但是单例模式要优于静态变量:

  1. 静态变量在程序启动的时候JVM就会进行加载,如果不使用,会造成大量的资源浪费;
  2. 单例模式能够实现懒加载,能够在使用实例的时候才去创建实例。

开发工具类库中的很多工具类都应用了单例模式,比例线程池、缓存、日志对象等,它们都只需要创建一个对象,如果创建多份实例,可能会带来不可预知的问题,比如资源的浪费、结果处理不一致等问题。

1.2 单例的实现思路

  1. 静态化实例对象;
  2. 私有化构造方法,禁止通过构造方法创建实例;
  3. 提供一个公共的静态方法,用来返回唯一实例。

1.3 单例的好处

  1. 只有一个对象,内存开支少、性能好;
  2. 避免对资源的多重占用;
  3. 在系统设置全局访问点,优化和共享资源访问。

二、 单例模式的实现

  1. 饿汉模式
  2. 懒汉模式
  3. 双重检查锁模式
  4. 静态内部类单例模式
  5. 枚举类实现单例模式

2.1 饿汉模式

在定义静态属性时,直接实例化了对象

public class HungryMode {/** * 利用静态变量来存储唯一实例 */ private static final HungryMode instance = new HungryMode(); /** * 私有化构造函数 */ private HungryMode(){ // 里面可以有很多操作 } /** * 提供公开获取实例接口 * @return */ public static HungryMode getInstance(){ return instance; } }

2.1.1 优点

由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM层面的线程安全

2.1.2 缺点

不能实现懒加载,造成空间浪费:如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。

所以,能不能只有用到 getInstance()方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了:懒汉式

2.2 懒汉模式

懒汉模式是一种偷懒的模式,在程序初始化时不会创建实例,只有在使用实例的时候才会创建实例,所以懒汉模式解决了饿汉模式带来的空间浪费问题。

2.2.1 懒汉模式的一般实现

public class LazyMode {/** * 定义静态变量时,未初始化实例 */ private static LazyMode instance; /** * 私有化构造函数 */ private LazyMode(){ // 里面可以有很多操作 } /** * 提供公开获取实例接口 * @return */ public static LazyMode getInstance(){ // 使用时,先判断实例是否为空,如果实例为空,则实例化对象 if (instance == null) { instance = new LazyMode(); } return instance; } }

但是这种实现在多线程的情况下是不安全的,有可能会出现多份实例的情况:

if (instance == null) {instance = new LazyMode();
}

假设有两个线程同时进入到上面这段代码,因为没有任何资源保护措施,所以两个线程可以同时判断的 instance 都为空,都将去初始化实例,所以就会出现多份实例的情况。

2.2.2 懒汉模式的优化

我们给getInstance()方法加上synchronized关键字,使得getInstance()方法成为受保护的资源就能够解决多份实例的问题。

public class LazyModeSynchronized {/** * 定义静态变量时,未初始化实例 */ private static LazyModeSynchronized instance; /** * 私有化构造函数 */ private LazyModeSynchronized(){ // 里面可以有很多操作 } /** * 提供公开获取实例接口 * @return */ public synchronized static LazyModeSynchronized getInstance(){ /** * 添加class类锁,影响了性能,加锁之后将代码进行了串行化, * 我们的代码块绝大部分是读操作,在读操作的情况下,代码线程是安全的 * */ if (instance == null) { instance = new LazyModeSynchronized(); } return instance; } }

2.2.3 懒汉模式的优点

实现了懒加载,节约了内存空间。

2.2.4 懒汉模式的缺点

  1. 在不加锁的情况下,线程不安全,可能出现多份实例;
  2. 在加锁的情况下,会使程序串行化,使系统有严重的性能问题。

懒汉模式中加锁的问题,对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的问题。由此也产生了一种新的实现模式:双重检查锁模式

2.3 双重检查锁模式

2.3.1 双重检查锁模式的一般实现

public class DoubleCheckLockMode {private static DoubleCheckLockMode instance; /** * 私有化构造函数 */ private DoubleCheckLockMode(){ } /** * 提供公开获取实例接口 * @return */ public static DoubleCheckLockMode getInstance(){ // 第一次判断,如果这里为空,不进入抢锁阶段,直接返回实例 if (instance == null) { synchronized (DoubleCheckLockMode.class) { // 抢到锁之后再次判断是否为空 if (instance == null) { instance = new DoubleCheckLockMode(); } } } return instance; } }

双重检查锁模式解决了单例、性能、线程安全问题,但是这种写法同样存在问题:在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

2.3.2 什么是指令重排?

private SingletonObject(){// 第一步 int x = 10; // 第二步 int y = 30; // 第三步 Object o = new Object(); }

上面的构造函数SingletonObject()JVM 会对它进行指令重排序,所以执行顺序可能会乱掉,但是不管是那种执行顺序,JVM 最后都会保证所以实例都完成实例化。 如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。双重检测锁出现空指针问题的原因就是出现在这里,当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题

2.3.3 双重检查锁模式优化

要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字,volatile关键字严格遵循happens-before原则,即:在读操作前,写操作必须全部完成。

public class DoubleCheckLockModelVolatile {/** * 添加volatile关键字,保证在读操作前,写操作必须全部完成 */ private static volatile DoubleCheckLockModelVolatile instance; /** * 私有化构造函数 */ private DoubleCheckLockModelVolatile(){ } /** * 提供公开获取实例接口 * @return */ public static DoubleCheckLockModelVolatile getInstance(){ if (instance == null) { synchronized (DoubleCheckLockModelVolatile.class) { if (instance == null) { instance = new DoubleCheckLockModelVolatile(); } } } return instance; } }

2.4 静态内部类模式

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

public class StaticInnerClassMode {private StaticInnerClassMode(){ } /** * 单例持有者 */ private static class InstanceHolder{ private final static StaticInnerClassMode instance = new StaticInnerClassMode(); } /** * 提供公开获取实例接口 * @return */ public static StaticInnerClassMode getInstance(){ // 调用内部类属性 return InstanceHolder.instance; } }

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方:

  1. 饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用;
  2. 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance()方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

所以这种方式在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费

2.5 枚举类实现单例模式

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

public class EnumerationMode {private EnumerationMode(){ } /** * 枚举类型是线程安全的,并且只会装载一次 */ private enum Singleton{ INSTANCE; private final EnumerationMode instance; Singleton(){ instance = new EnumerationMode(); } private EnumerationMode getInstance(){ return instance; } } public static EnumerationMode getInstance(){ return Singleton.INSTANCE.getInstance(); } }

适用场合:

  1. 需要频繁的进行创建和销毁的对象;
  2. 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  3. 工具类对象;
  4. 频繁访问数据库或文件的对象。

三、单例模式的问题及解决办法

除枚举方式外, 其他方法都会通过反射的方式破坏单例

3.1 单例模式的破坏

/*** 以静态内部类实现为例* @throws Exception*/
@Test
public void singletonTest() throws Exception { Constructor constructor = StaticInnerClassMode.class.getDeclaredConstructor(); constructor.setAccessible(true); StaticInnerClassMode obj1 = StaticInnerClassMode.getInstance(); StaticInnerClassMode obj2 = StaticInnerClassMode.getInstance(); StaticInnerClassMode obj3 = (StaticInnerClassMode) constructor.newInstance(); System.out.println("输出结果为:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode()); }

控制台打印:

输出结果为:1454171136,1454171136,1195396074

从输出的结果我们就可以看出obj1obj2为同一对象,obj3为新对象。obj3是我们通过反射机制,进而调用了私有的构造函数,然后产生了一个新的对象。

3.2 如何阻止单例破坏

可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:

public class StaticInnerClassModeProtection {private static boolean flag = false; private StaticInnerClassModeProtection(){ synchronized(StaticInnerClassModeProtection.class){ if(flag == false){ flag = true; }else { throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取!"); } } } /** * 单例持有者 */ private static class InstanceHolder{ private final static StaticInnerClassModeProtection instance = new StaticInnerClassModeProtection(); } /** * 提供公开获取实例接口 * @return */ public static StaticInnerClassModeProtection getInstance(){ // 调用内部类属性 return InstanceHolder.instance; } }

测试:

/*** 在构造方法中进行判断,若存在则抛出RuntimeException* @throws Exception*/
@Test
public void destroyTest() throws Exception { Constructor constructor = StaticInnerClassModeProtection.class.getDeclaredConstructor(); constructor.setAccessible(true); StaticInnerClassModeProtection obj1 = StaticInnerClassModeProtection.getInstance(); StaticInnerClassModeProtection obj2 = StaticInnerClassModeProtection.getInstance(); StaticInnerClassModeProtection obj3 = (StaticInnerClassModeProtection) constructor.newInstance(); System.out.println("输出结果为:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode()); }

控制台打印:

Caused by: java.lang.RuntimeException: 实例已经存在,请通过 getInstance()方法获取! at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22) ... 35 more

四、总结

4.1 各种实现的对比

名称 饿汉模式 懒汉模式 双重检查锁模式 静态内部类实现 枚举类实现
可用性 可用 不推荐使用 推荐使用 推荐使用 推荐使用
特点 不能实现懒加载,可能造成空间浪费 不加锁线程不安全;加锁性能差 线程安全;延迟加载;效率较高 避免了线程不安全,延迟加载,效率高。 写法简单;线程安全;只装载一次

转载原文链接:https://www.cnblogs.com/vandusty/p/11444293.html

单例模式中,你不知道的事~~相关推荐

  1. 那些兼职中你不知道的事

    简单聊一些网上兼职都有哪些? 起因是因为今年疫情的原因,公司情况不是很景气.后来因为某些特殊的关系需要自媒体平台的推广,接触的时间长了,也就发现各种平台上推广的各类兼职.一时兴起,就想测评一下各种兼职 ...

  2. WebView中你不知道的事localStorage

    今天在开发项目的时候碰到一个怪事:本着以为开发webview都是简简单单的事情 首先:实例化webview,启用JavaScript,接着再设置WebViewClient mWebView = (We ...

  3. 关于克隆猴中中华华的12点你不知道的事

    关于克隆猴-中中华华的几点你不知道的事 克隆猴的细胞来源是一只流产的母猴胎儿,也就是说,这只猴宝宝本来已经死了,但是它奇迹般的以2只克隆猴的形态复活了. 除了占满眼球的由胎儿细胞克隆成功的猴,还有1组 ...

  4. python3.9.0 print_关于 Python 3.9,那些你不知道的事

    原标题:关于 Python 3.9,那些你不知道的事 作者 | Ayushi Rawat 编译 | 高卫华 题图 | 视觉中国 Python一直在满足社区需求,并且将成为未来使用最多的语言. Pyth ...

  5. 物联网通信技术,那些你不知道的事

    摘要:通信技术是物联网的基础,如果把物联网比作是物流系统,那么通信技术就相当于 是送快递的各种运输方式,比如空运.水运还有陆运等.在通信技术当中,大体上它可以分为两大类,一类是无线通信技术,另一类是有 ...

  6. setTimeout,setInterval你不知道的事

    javascript线程解释(setTimeout,setInterval你不知道的事) 标签: javascript引擎任务浏览器functionxmlhttprequest 2011-11-21 ...

  7. 你不知道的事-建站始末1【准备篇】

    本篇内容会有些长,希望各位看官可以认真的阅读下去,我相信肯定会有收获. 写在前面 蝴蝶眨几次眼睛,才学会飞行,夜空洒满了星星,但几颗会落地. --你不知道的事 蝴蝶眨眼睛?星星会落地?当然很多人会认为 ...

  8. 关于MVT矢量瓦片,你不知道的事

    作者:LX 其实博主已经在前面的系列文章中介绍了矢量瓦片,比如 <创建矢量瓦片之制图最佳实践>介绍了制作瓦片的注意事项: <SuperMap地图系列:矢量瓦片的应用>介绍了矢量 ...

  9. --守望先锋中你不知道的秘密--

    守望先锋中你不知道的秘密 ============================ 学号:16340098 学院:数据科学与计算机学院 目录 守望先锋中你不知道的秘密 目录 1开局时有趣的对话 原文参 ...

  10. 关于算法,那些你不知道的事

    关于算法,那些你不知道的事 1.算法,不止于刷题 提到算法,不管是科班出身还是半路出家的程序员可能都会说上几句,算法谁没学过谁不知道啊?对于走工业界路线而非学术路线的同学来说,算法学习的最大作用也许是 ...

最新文章

  1. Android学习笔记-Wifi网络操作
  2. mysql创建新用户方法_Mysql创建新用户方法
  3. 前沿分享|阿里云数据库解决方案资深专家 李圣陶:云原生数据库解决方案 加速企业国产化升级
  4. 学习阮一峰Javascript模块化编程,requireJS使用
  5. LEBERT:基于词汇增强的中文NER模型
  6. intellij idea rearrange code
  7. printf,sprintf,vsprintf 区别【转】
  8. arcgis栅格数据绘制等值线_ArcGIS教程:绘制等值线的工作原理
  9. mbedtls学习4.mbedtls_RAM/ROM优化指南
  10. 英语六级-day10
  11. UE4 WebBrowser插件清除浏览器缓存
  12. linux常用操作命令
  13. lob 索引 oracle,LOB字段相关概念(自动创建LOB索引段和重建索引方法)
  14. IP地址管理(IPAM)解决方案有哪些?
  15. linux替换屏幕保护进程,有没有一个体面的方式来阻止linux中的屏幕保护程序?...
  16. 【ChatGPT】ChatGPT使用指南——句词分类
  17. 大数据分析与可视化介绍,基于机器学习的大数据分析具有哪些独特的特点
  18. Linux DDos防御
  19. Aspose导出word
  20. Lumerical Python API学习笔记(一):Lumopt配置以及样例运行

热门文章

  1. 不可思议!乌克兰国防军队的系统账密居然是 admin 和 123456...
  2. 当当的羊毛又该剪了!官方活动基础上的额外优惠券,速领!
  3. JAVA 和 GO 真香!谁用谁知道!
  4. php 自动登录脚本_php利用cookie实现自动登录的方法
  5. open3d python 学习笔记
  6. 轻量人脸检测,5个关键点
  7. opencv 连通域笔记
  8. graphviz.backend.ExecutableNotFound: failed to execute ['dot', '-Tpdf', '-O', 'Digraph.gv']
  9. 人群计数最全代码、数据、论文合集
  10. 'datetime.datetime' object is not callable