Java设计模式 - 单例模式(末尾有彩蛋

2024-02-21 17:30

本文主要是介绍Java设计模式 - 单例模式(末尾有彩蛋,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

 

定义

  确保只有一个类只有一个实例,并提供全局访问点。

为什么要使用它

  对一些类来说保证只有一个实例是很重要的。比如windows操作系统中的资源管理器,回收站等工具必须保证只有一个实例,否则系统将会出现一些意想不到的异常。

优点

  因为只有一个实例,所以很容易控制它的访问权限;避免了过多的使用静态变量等。

适用范围

  当类只能有一个实例而且客户可以从一个众所周知的访问点访问它。

结构(UML)

  单例模式的结构比较简单,只有一个类:它的构造器是私有的并且提供了一个返回一个实例的静态方法。

  单例模式的UML比较简单:

  

实现

  假设现在你有一个地方需要使用到单例模式,你可能首先会想到这样写:

package com.tony.singleton;
/*** 1、私有化构造器  * 2、提供一个返回实例的方法(全局访问点)  * 这种方法叫做懒汉式  */
public class Singleton01 {  private static Singleton01 instance = null; //私有化构造器private Singleton01(){ } //静态工厂方法public static Singleton01 getInstance(){ if(instance == null){ instance = new Singleton01(); } return instance; } 
}

  但是这种方法又有一个问题:现在的程序一般都是多线程的,在并发的情况下可能会出现两个实例!

  让我们来分析一下这种情况:现在有两个线程(A、B)同时调用了getInstance()方法,然后一个A线程发现instance为null。当A线程正准备去new一个实例时,B线程也发现instance为null,所以B线程也去new一个实例。这种情况下就会出现两个实例。

  怎么解决呢?

  此时我们会想这还不简单,给它加一个synchronized同步锁不就OK了?!

package com.tony.singleton;  /*** 1、私有化构造器  * 2、提供一个返回实例的方法(全局访问点)  */
public class Singleton02 {  private static Singleton02 instance = null;  //私有化构造器private Singleton02(){ } //静态工厂方法public static synchronized Singleton02 getInstance(){ if(instance == null){ instance = new Singleton02(); } return instance; } 
}
 

  OK问题解决了!同步这种方法简单易行解决了线程的并发问题,但同时又带来了一个新的问题:对性能的影响。如果getInstance()频繁被调用,那么你就得重新考虑了:每次调用之前都要给它加同步锁!你要知道同步一个方法可能造成程序执行效率下降100倍,而且只有实例化这个对象时才需要同步。

  好吧,既然麻烦都在实例化对象这里,那么我在类加载器加载的时候我就把这个对象实例化了是不是就可以呢?

package com.tony.singleton;  /***  * 这种方式就做 饿汉式  */
public class Singleton03 {  //当被类加载器加载的时候就把这个类给实例化private static Singleton03 instance = new Singleton03(); //私有化构造器private Singleton03(){ } //静态工厂方法public static Singleton03 getInstance(){ return instance; }
}
 

  利用这种做法,我们依赖JVM在加载这个类时马上创建此唯一的实例。JVM保证在任何线程访问instance变量之前,一定先创建此实例。

  这种做法很好,基本上解决了上面的出现的两个问题:1、并发访问;2、对性能的影响。

  这种做法适合不太复杂的实例。如果需要实例化的的类很复杂,在创建和运行时方面的负担太重就会增加JVM的负担。

  有没有这样的方法:除了解决最初的那两个问题外还能延时加载,减轻JVM的负担?答案是有!

  这种方法叫做双重检测锁。这次引进了一个新东西:volatile 关键字。

 package com.tony.singleton;/*** 双重检测锁  **/
public class Singleton04 {  //增加volatile关键字!!!private volatile static Singleton04 instance = null; //私有化构造器private Singleton04(){ } //静态工厂方法public static synchronized Singleton04 getInstance(){ if(instance == null){ //这段代码仅有一次执行的机会:只有第一次才彻底执行synchronized(Singleton04.class){ if(instance == null){ instance = new Singleton04(); } }} return instance; } 
}
 1

  这时候synchronized所同步的那段代码只会执行一次,而且还保证了延时加载。

总结

  实现单例模式的方法还有几种,但比较常用的就是这几种。一般掌握饿汉式、懒汉式和双重检测锁就可以了。

  这几种各有优缺点,具体使用那种还有具体情况具体分析。

  懒汉式:能够延时加载,但可能对性能影响较大。

  懒汉式:对性能影响较小,但不能延时加载。

  双重检测锁:能够延时加载,只需同步一次,但是不支持JDK1.4之前的版本。

-----------------------分割线----11.24-------------------------

    现在看着一年前自己写的博客,感觉好low~~~

    现在有些问题之前没有考虑到的,现在提出来:

  1. 双重检测锁为什么要加上volatile关键字?如果没有会有什么影响?
  2. 双重检测锁真的比其他的单例实现更好吗?

    针对上述的问题,我来一一分析回答。

  1.     针对第一个问题,其实当时写博客的时候我也不知道为什么要加volatile关键字,因为书上是这么写的,?。其实这里涉及到一个重排序的问题。什么是重排序?Java虚拟机在执行字节码的时候为了使代码运行时获得更好的性能和效率有权利对字节码进行语义上的重新调整,只要不影响最后的结果即可。下面我举一个例子:
    private Object a = new Object();

    这是一行再普通不过的代码了,按照我们的经验是从右往左执行的:首先(a)在堆中分配一个Object内存大小的空间,(b)初始化Object实例,然后(c)将引用赋值给a。嗯,没错,一般情况下是这样的。但是在多线程的情况下就不一样了,代码有可能是这样执行的:(a)在堆中分配一个Object内存大小的空间,(c)将引用赋值给a,(b)初始化Object实例。第一步还是一样:分配堆内存,第二步第三步交换了一下顺序。咋一看好像没啥影响啊,但是假如有两条线程(A、B)交替执行就会出问题。继续拿双重检测锁的代码用用,假如A线程首先执行,直到

    instance = new Singleton04();

    这一行代码,此时发生上述的重排序问题,instance获得引用,但此时内存中实例初始化还没有初始化,现在CPU把A线程切换出去。把B线程切换进来,然后执行这段代码,因为instance变量已经获得了内存的引用条件instance == null 都为false,直到return instance。当其他代码拿到实例,然后执行其方法时,GG,NullPointerException。如果能够理解我刚刚的解释,那么为什么要加volatile关键字也就不难理解了。volatile修饰的变量可以保证任何一个线程对其的改动都会被后面的线程所察觉(happens-before,具体信息参看这里)。

  2. 针对第二个问题,我主要想表达随着计算机性能的提高,延迟初始化的优点已经显得微不足道。除非这个类真的非常复杂,加载时需要非常多的资源。如果真的是这样,那么我们可能就要反思一下我们的设计是不是出了问题,一个类不能让它承担太多的职责,否则后面维护起来非常麻烦。而且这样也不利用团队协作~所以就现在的计算能力完全不需要延迟初始化,通过使用static关键字,或者使用枚举类不失为一种好的单例实现方式。

好了,如果大家还有什么疑问欢迎留言,我会尽可能的回答大家的问题~

  

相关知识点

  • happens-before
  • 重排序
  • volatile关键字

参考资料

  《Head First 设计模式》

      《Java 并发编程实战》

  《设计模式》

 

转载于:https://my.oschina.net/liuxiaomian/blog/793345

这篇关于Java设计模式 - 单例模式(末尾有彩蛋的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/732534

相关文章

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

SpringBoot实现不同接口指定上传文件大小的具体步骤

《SpringBoot实现不同接口指定上传文件大小的具体步骤》:本文主要介绍在SpringBoot中通过自定义注解、AOP拦截和配置文件实现不同接口上传文件大小限制的方法,强调需设置全局阈值远大于... 目录一  springboot实现不同接口指定文件大小1.1 思路说明1.2 工程启动说明二 具体实施2

Java实现在Word文档中添加文本水印和图片水印的操作指南

《Java实现在Word文档中添加文本水印和图片水印的操作指南》在当今数字时代,文档的自动化处理与安全防护变得尤为重要,无论是为了保护版权、推广品牌,还是为了在文档中加入特定的标识,为Word文档添加... 目录引言Spire.Doc for Java:高效Word文档处理的利器代码实战:使用Java为Wo

SpringBoot日志级别与日志分组详解

《SpringBoot日志级别与日志分组详解》文章介绍了日志级别(ALL至OFF)及其作用,说明SpringBoot默认日志级别为INFO,可通过application.properties调整全局或... 目录日志级别1、级别内容2、调整日志级别调整默认日志级别调整指定类的日志级别项目开发过程中,利用日志

Java中的抽象类与abstract 关键字使用详解

《Java中的抽象类与abstract关键字使用详解》:本文主要介绍Java中的抽象类与abstract关键字使用详解,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、抽象类的概念二、使用 abstract2.1 修饰类 => 抽象类2.2 修饰方法 => 抽象方法,没有