设计模式之单例模式

一、介绍

  单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

TIPS:
    1、单例类只能有一个实例。
    2、单例类必须自己创建自己的唯一实例。
    3、单例类必须给所有其他对象提供这一实例。

 

实现的目标:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

解决得问题:一个全局使用的类频繁地创建与销毁。

核心思想:构造函数是私有的。并且对外提供一个全局的访问方法。

使用场景: 

1、要求生产唯一序列号。
2、代码级别的全局的JVM缓存。(有状态的单例)
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

 

二、懒汉式

  所谓的懒汉式单例,是指:第一次调用才初始化,避免内存浪费。典型的时间换取空间的做法。因为第一次获取单例的时候会执行单例的实例化,浪费一些时间。但是后面再继续获取就不存时间问题了。

2.1、懒汉式的简化版

 1 package com.shf.mode.singleton;
 2 
 3 /**
 4  * 描述:懒汉式
 5  * 单例实例在第一次调用的时候创建
 6  * 典型的以时间换取空间,第一次调用 getInstance()才会实例化单例。
 7  *
 8  * @Author shf
 9  * @Date 2019/7/15 16:12
10  * @Version V1.0
11  **/
12 public class SingletonExample2 {
13     // 私有构造方法,禁止外部直接创建实例
14     private SingletonExample2(){}
15     // 私有 单例对象 将对象的管理权封闭
16     private static SingletonExample2 instance = null;
17 
18     // 静态的工厂方法
19     public static SingletonExample2 getInstance(){
20         if(instance == null){
21             instance = new SingletonExample2();
22         }
23         return instance;
24     }
25 }

 

  ok,我们看一下这种最简单的懒汉式存在的问题。

  在多线程环境下,当两个线程同时访问这个方法,同时制定到instance==null的判断。都判断为null,接下来同时执行new操作。这样类的构造函数被执行了两次。一旦构造函数中涉及到某些资源的处理,那么就会发生错误。所以说最简式是线程不安全的。

 

2.2、懒汉式 synchronized版

 1 public class SingletonExample2 {
 2     // 私有构造方法,禁止外部直接创建实例
 3     private SingletonExample2(){}
 4     // 私有 单例对象 将对象的管理权封闭
 5     private static SingletonExample2 instance = null;
 6 
 7     // 静态的工厂方法
 8     public synchronized static SingletonExample2 getInstance(){
 9         if(instance == null){
10             instance = new SingletonExample2();
11         }
12         return instance;
13     }
14 }

 

  如上代码所示,我们采用synchronized修饰getInstance()方法。有效的解决了线程安全性的问题,但是,,,但是synchronized直接锁死了getInstance()方法,意味着在多线程环境下getInstance()方法的并发效率会大大降低,性能损耗十分严重,因为getInstance()方法已经成为了一个同步方法。

 

2.3、懒汉式的双重检测机制

 1 public class SingletonExample2 {
 2     // 私有构造方法,禁止外部直接创建实例
 3     private SingletonExample2(){}
 4     // 私有 单例对象 将对象的管理权封闭
 5     private static SingletonExample2 instance = null;
 6 
 7     // 静态的工厂方法
 8     public static SingletonExample2 getInstance(){
 9         if(instance == null){// 双重监测机制
10             synchronized(SingletonExample2.class){// 同步锁
11                 if(instance == null){
12                     instance = new SingletonExample2();
13                 }
14             }
15         }
16         return instance;
17     }
18 }

 

  双重检测机制保证了 synchronized 代码块中代码的线程安全性,另外当已经初始化完成了单例后调用 getInstance()方法,不会再次进入synchronized 代码块中,貌似是满足了线程安全性和性能的提升。但是,,,真的是线程安全的吗?NO,因为 instance = new SingletonExample2(); 该行代码会发生指令重排。

  在上述代码中,执行new操作的时候,CPU一共进行了三次指令 

1,memory = allocate() 分配对象的内存空间 
2,ctorInstance() 初始化对象 
3,instance = memory 设置instance指向刚分配的内存

  在程序运行过程中,CPU在不违背 happens-before 八大原则的前提下,为提高运算速度会做出违背代码原有顺序的优化。我们称之为乱序执行优化或者说是指令重排。那么上面知识点中的三步指令极有可能被优化为1,3,2的顺序。当我们有两个线程A与B,A线程遵从132的顺序,经过了两此instance的空值判断后,执行了new操作,并且cpu在某一瞬间刚结束指令3,并且还没有执行指令2。而在此时线程B恰巧在进行第一次的instance空值判断,由于线程A执行完3指令,为instance分配了内存,线程B判断instance不为空,直接执行return,返回了instance,这样就出现了错误。

 

2.4、懒汉式终极版:双重检测机制+volatile禁止指令重排

 1 package com.shf.mode.singleton;
 2 
 3 /**
 4  * 描述:懒汉式
 5  * 单例实例在第一次调用的时候创建
 6  * 典型的以时间换取空间,第一次调用 getInstance()才会实例化单例。
 7  *
 8  * @Author shf
 9  * @Date 2019/7/15 16:12
10  * @Version V1.0
11  **/
12 public class SingletonExample2 {
13     // 私有构造方法,禁止外部直接创建实例
14     private SingletonExample2(){}
15     // 1、memory = allocate() 分配对象的内存空间
16     // 2、ctorInstance() 初始化对象
17     // 3、instance = memory 设置instance指向刚分配的内存
18 
19     // 单例对象 volatile + 双重检测机制 -> 禁止指令重排
20     // 私有 单例对象 将对象的管理权封闭
21     private volatile static SingletonExample2 instance = null;
22 
23     // 静态的工厂方法
24     public static SingletonExample2 getInstance(){
25         if(instance == null){// 双重监测机制
26             synchronized(SingletonExample2.class){// 同步锁
27                 if(instance == null){
28                     instance = new SingletonExample2();
29                 }
30             }
31         }
32         return instance;
33     }
34 }

  关于happens-before原则和volatile禁止指令重排感兴趣的可以度娘一下。或者参考一下这篇博客

  双重检测机制又称双重校验锁(DCL,即 double-checked locking)。面试官很中意这个玩意哦。

 

三、饿汉式

  所谓饿汉式就是不管你用不用,我都先给你创建出来。空间换时间,但是毕竟如果不用的话岂不是白白浪费了空间,如果保证这个单例一定会被调用,饿汉式也是一个不错的选择,而且实现的难度相对于懒汉式也简单不少。

 1 package com.shf.mode.singleton;
 2 
 3 /**
 4  * 描述:饿汉式
 5  * 单例实例在类装载时创建
 6  * 典型的用空间换取时间----不管用不用都先创建出来
 7  *
 8  * @Author shf
 9  * @Date 2019/7/15 15:58
10  * @Version V1.0
11  **/
12 public class SingletonExample1 {
13     // 私有构造方法,禁止外部直接创建实例
14     private SingletonExample1(){}
15     // 私有 单例对象 将对象的管理权封闭
16     private static SingletonExample1 instance = null;
17     // 静态方法块 完成单例的实例化
18     static {
19         instance = new SingletonExample1();
20     }
21     // public 对外开发的获取单例的唯一渠道
22     public SingletonExample1 getInstance(){
23         return instance;
24     }
25 }

 

  

TIPS:
        静态域和静态代码块的顺序不要乱了哦,静态域与静态块是按照顺序执行的。这是Java基础知识的内容,就不过多解释了。

 

 

四、枚举式

  这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,这个是JVM保证的。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

 1 package com.shf.mode.singleton;
 2 
 3 /**
 4  * 描述:
 5  *
 6  * @Author shf
 7  * @Date 2019/7/16 0:28
 8  * @Version V1.0
 9  **/
10 public class SingletonExample3 {
11     // 私有构造方法,禁止外部直接创建实例
12     private SingletonExample3() {}
13 
14     public static SingletonExample3 getInstance() {
15         return Singleton.INSTANCE.getInstance();
16     }
17 
18     private enum Singleton {
19         INSTANCE;
20         private SingletonExample3 singleton;
21 
22         Singleton() {
23             singleton = new SingletonExample3();
24         }
25 
26         public SingletonExample3 getInstance() {
27             return singleton;
28         }
29     }
30 }

 

 

  

  如有错误的地方还请留言指正。

  原创不易,转载请注明原文地址:https://www.cnblogs.com/hello-shf/p/11192500.html

posted @ 2019-07-16 14:25  超级小小黑  阅读(1251)  评论(1编辑  收藏  举报