Java与设计模式(十二)创建型--单例模式

1. 一、单例模式的动机

对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子——Windows任务管理器,我们可以做一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”,看能否打开多个任务管理器窗口?通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。为什么要这样设计呢?我们可以从以下两个方面来分析:

  • 其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源,任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等,浪费是可耻的,而且根本没有必要显示多个内容完全相同的窗口;

  • 其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?给用户带来误解,更不可取。由此可见,确保Windows任务管理器在系统中有且仅有一个非常重要。

    回到实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在

二、代码示例

懒汉式,线程不安全

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  

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

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作

懒汉式,线程安全

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

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

优点:第一次调用才初始化,避免内存浪费。

缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

饿汉式

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}  

这种方式比较常用,但容易产生垃圾对象。

  • 优点:没有加锁,执行效率会提高。
  • 缺点:类加载时就初始化,浪费内存。

它基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

双检锁/双重校验锁(DCL,即 double-checked locking)

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getInstance() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

登记式/静态内部类

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}   

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

这种方式同样利用了 classloder 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理

枚举

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

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

不能通过 reflection attack 来调用私有构造方法。

使用经验

一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。

五、单例模式总结

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。

主要优点

  • 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

主要缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

适用场景

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

六、单例模式与垃圾回收

当一个单例的对象长久不用时,会不会被jvm的垃圾收集机制回收。

我的回答:不会

hotspot虚拟机的垃圾收集算法使用根搜索算法。这个算法的基本思路是:对任何”活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。通过一系列名为根(GC Roots)的引用作为起点,从这些根开始搜索,经过一系列的路径,如果可以到达java堆中的对象,那么这个对象就是”活”的,是不可回收的。可以作为根的对象有:

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI的引用的对象。

方法区是jvm的一块内存区域,用来存放类相关的信息。很明显,java中单例模式创建的对象被自己类中的静态属性所引用,符合第二条,因此,单例对象不会被jvm垃圾收集。

虽然jvm堆中的单例对象不会被垃圾收集,但是单例类本身如果长时间不用会不会被收集呢?因为jvm对方法区也是有垃圾收集机制的。如果单例类被收集,那么堆中的对象就会失去到根的路径,必然会被垃圾收集掉。hotspot虚拟机对方法区的垃圾收集方法,jvm卸载类的判定条件如下:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

只有三个条件都满足,jvm才会在垃圾收集的时候卸载类。显然,单例的类不满足条件一,因此单例类也不会被卸载。也就是说,只要单例类中的静态引用指向jvm堆中的单例对象,那么单例类和单例对象都不会被垃圾收集,依据根搜索算法,对象是否会被垃圾收集与未被使用时间长短无关,仅仅在于这个对象是不是”活”的。假如一个对象长久未使用而被回收,那么收集算法应该是最近最长未使用算法,最近最长未使用算法一般用在操作系统的内外存交换中,如果用在虚拟机垃圾回收中,岂不是太不安全了?

猜你喜欢

转载自blog.csdn.net/qq_40134461/article/details/89400725