设计模式-单例模式

阅读更多

1.1 什么是单例模式

单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法

1.2 为什么需要单例模式

单例模式有以下两方面的作用

  1. 控制实例产生的数量,达到节约资源的目的
  2. 作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头

正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。这种方式极大地简化了在复杂环境 下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题

1.3 单例模式的要素

  • 私有的构造方法
  • 指向自己实例的私有静态引用
  • 以自己实例为返回值的静态的公有的方法

1.4 参考文献

所有的分析仅基于个人的理解,若有不正之处,请谅解和批评指正,不胜感激!!!

2 多种单例的实现

2.1 饿汉式单例

所谓饿汉式单例模式是指:在类加载过程中完成单例的初始化

  • 不用考虑线程安全问题:JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕
  • 可能会造成内存资源的浪费:因为我们在使用这个单例之前,这个单例就存在于内存中。我们更希望在第一次使用单例时进行初始化,而不是在类加载过程中初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class {
    private static Singleton instance=new Singleton();

    private (){}

    public static Singleton getSingleton(){
    return instance;
    }
    }

2.2 懒汉式单例

所谓懒汉模式是指:在第一次访问单例对象时,对单例对象执行初始化动作,而非在类加载过程中执行单例对象的初始化动作。首先来看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
class {
private static Singleton instance;

private (){}

public static Singleton getSingleton(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}

显然,这种单例模式在并发环境下是不安全的。我们马上可以想到一种办法来解决这个问题,就是给getSingleton方法加上synchronized关键字,改进过的单例模式如下

1
2
3
4
5
6
7
8
9
10
11
12
class {
大专栏  设计模式-单例模式 class="keyword">private static Singleton instance;

private Singleton(){}

public synchronized static Singleton getSingleton(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}

似乎,这种方式可以解决并发安全的问题,但是又会导致另外一个问题,每次访问这个单例,都会执行加锁解锁操作,显然这会增加额外的性能开销。事实上,只有第一次实例化的时候才需要加锁,实例化之后的访问是不需要加锁的。于是,可以利用 双重检测 的机制来解决这个问题,改进后的单例模式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton{
private static Singleton instance;

private Singleton(){}

public synchronized static Singleton getSingleton(){
if(instance==null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

上述单例模式,采用了synchronized块,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。 但是,这种方式在多线程环境下仍然是不完美的
在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例

  1. A、B线程同时进入了第一个if判断
  2. A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
  3. 由于JVM内部的优化机制,JVM先划出了一些分配给Singleton实例的空白内存,并赋值给instance成员( 注意此时JVM没有开始初始化这个实例 ),然后A离开了synchronized块
  4. B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序
  5. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了

那么完美的懒汉式的单例模式就真的无法实现了吗?其实不然,一种方法是将instance字段加上 volatile关键字,来禁止编译器重排;另一种方式我们可以通过静态内部类来实现延迟加载的机制

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton{

private static final class LazyInitialize{
private static Singleton instance=new Singleton();
}

private Singleton(){}

public static Singleton getSingleton(){
return LazyInitialize.instance;
}
}

加载Singleton的时候并不会加载内部类LazyInitialize,当我们第一次调用getSingleton方法来获取单例对象时,LazyInitialize才被JVM加载,同时JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕

3 结语

本篇是设计模式的第一篇,之后还会陆续推出其余22种常用设计模式

4 参考

原文地址:https://www.cnblogs.com/lijianming180/p/12361303.html