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 NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B