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

相关文章

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java进程异常故障定位及排查过程

《Java进程异常故障定位及排查过程》:本文主要介绍Java进程异常故障定位及排查过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、故障发现与初步判断1. 监控系统告警2. 日志初步分析二、核心排查工具与步骤1. 进程状态检查2. CPU 飙升问题3. 内存

java中新生代和老生代的关系说明

《java中新生代和老生代的关系说明》:本文主要介绍java中新生代和老生代的关系说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、内存区域划分新生代老年代二、对象生命周期与晋升流程三、新生代与老年代的协作机制1. 跨代引用处理2. 动态年龄判定3. 空间分

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

深度解析Java DTO(最新推荐)

《深度解析JavaDTO(最新推荐)》DTO(DataTransferObject)是一种用于在不同层(如Controller层、Service层)之间传输数据的对象设计模式,其核心目的是封装数据,... 目录一、什么是DTO?DTO的核心特点:二、为什么需要DTO?(对比Entity)三、实际应用场景解析

Java 线程安全与 volatile与单例模式问题及解决方案

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字... 目录什么是线程安全线程安全问题的产生与解决方案线程的调度是随机的多个线程对同一个变量进行修改线程的修改操