Java 单例模式[备忘]
Contents
很久没有复习单例模式了,今天做一个笔记,对 Java 中的单例模式做一个总结,以做备忘。
手写单例模式
是有可能在面试过程中被问到的问题,一般可以从单例模式的使用场景、具体实现方式、线程安全与否、延迟加载与否、如何选择哪种单例模式等方面入手,下面介绍 5 种不同的单例模式。
适用场景
单例模式是一种对象创建模式,用于产生一个对象的具体实例,能够确保系统中一个类只产生一个实例。对于一些关键组件和被频繁使用的对象,使用单例模式可以有效的改善系统性能,由于 new 关键字操作的次数减少,可以减轻 GC 压力,缩短 GC 停顿时间。其核心在于通过一个接口返回唯一的对象实例,让某个类自己负责自身类的创建工作(通常使用 private 关键字),然后由这个类来提供外部可以访问这个类实例的方法(通常使用 public 关键字)。适用场景如下:
- 需要生成唯一序列的环境;
- 需要频繁实例化然后销毁的对象;
- 创建对象时耗时过多或消耗的资源过多,但又是经常需要用到的对象;
- 方便资源相互通信的环境。
饿汉式
饿汉式的实例在类加载的时候就完成了初始化任务,导致类加载的速度比较慢,但获取对象的速度快。该方式是线程安全的,并且不能延迟加载,因为在类初始化时,已经自行实例化了。同时,提供了一个私有的构造器,只有这样才能保证单例不会在系统中的其它地方被实例化。并对外部提供一个公共的获取实例的方法,该方法和INSTANCE
一样,都是使用static
修饰。代码如下所示:
|
|
懒汉式
顾名思义,懒汉式只有在想要获取实例(即调用getInstance()
方法)的时候才创建实例,这种方式可以做到懒加载,即延迟加载。同时使用了synchronized
关键字能够保证该方式是线程安全的,因为在多线程的情况下,当线程 A 正在新建单例的时候,在完成赋值操作之前,此时如果线程 B 将instance
判断为null
的话,则线程 B 也会进行单例的创建,从而会导致多个实例被创建的错误,因此需要使用线程同步。
这里没有对instance
使用final
修饰是因为懒汉式每次创建的实例都是不同的。
在getInstance()
方法中,首先判断当前实例是否已经存在,存在则返回,不存在才进行创建。
|
|
虽然懒汉式在第一次调用的时候才进行初始化,看似节约了系统资源,但第一次加载的时候需要实例化,无疑会出现效率低等问题。
双重校验锁
双重检验锁(Double CheckLock)和懒汉式很相似,能够保证线程安全的同时进行延迟加载。与懒汉式不同的是,在getInstance()
方法内部进行了两次判空操作,第二次判空是因为在多线程环境下其它的线程在调用getInstance()
方法的时候,有可能在此之前instance
已经被创建过了,所以需要再次进行判断。
|
|
此外,使用volatile
关键字修饰instance
,这样可以避免在进行instance = new SingletonThree();
时造成的指令重排问题。
原本的指令instance = new SingletonThree();
可分解为以下三步:
|
|
假如不使用volatile
关键字进行修饰,则有可能会在第 11 行发生指令重排,如下:
|
|
静态内部类
通过使用静态的内部类来创建实例,可以实现延迟加载,即第一次加载SingletonFour
类时不会初始化INSTANCE
,只有在调用getInstance()
方法的时候才初始化对象。此方法可以保证线程安全也能够保证该类的唯一性。
|
|
枚举类
使用枚举方式实现的单例虽然不能延迟加载,但可以保证线程安全,并且能够防止反射和反序列化调用。因为枚举默认是线程安全的,在任何情况下都是单例的。
|
|
最后再简单说一下如何选择,对于单例对象占用资源少、不需要延时加载的情况,枚举好于饿汉式;对于单例对象占用资源多、需要延时加载的情况,静态内部类要好于懒汉式。