vlambda博客
学习文章列表

是时候学习23种设计模式了-单例模式

1、前言

单例模式属于创建型模式,其目的是确保在整个应用运行期间一个类只有一个实例,并提供一个静态的全局访问点,无论在什么情况下,只会产生一个实例。

2、如何设计单例

关于如何设计单例模式其实很简单,只需要考虑一个问题,实例是否可以保证全局唯一。而关于实例是否保证全局唯一又延伸出如下问题:
(1)是否线程安全,不安全肯定就无法保证全局只有一个实例。
(2)是否支持序列化,支持序列化的类,被反序列化之后肯定就不是全局唯一了。
(3)是否支持反射,支持反射也无法保证全局唯一。
(4)是否可以被克隆,如果可以被克隆,自然也无法保证全局唯一。

3、常见的实现方式

常见的单例模式实现有5种,分别是:懒汉式、饿汉式、双重检查、静态内部类、枚举。也可以将其大致分成以下三类:
(1)是否支持延迟加载,分为懒汉式和饿汉式。
(2)线程安全设计了双重检查、静态内部类实现方式。
(3)不支持序列化、反射、克隆使用枚举实现方式。

其中枚举的实现方式最简单,也最安全,所以推荐使用枚举实现。其次推荐使用静态内部类方式实现。

4、实现单例模式

4.1、懒汉式

public class Singleton1 {

//声明一个私有的静态变量
private static Singleton1 singleton;

/**
* 构造方式私有化,保证在外部无法通过new来创建对象
*/

private Singleton1() {
}

/**
* 提供一个static方法来获取当前实例
*
* @return
*/

public static Singleton1 getInstance() {
if (singleton == null) {
singleton = new Singleton1();
}
return singleton;
}
}

Singleton1通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton1的唯一实例只能通过getInstance()方法访问。(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的 Java 单例实现失效。并且以上例子中没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton1实例。

懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。

4.2、饿汉式

public class Singleton2 {
//声明一个私有的静态变量,并直接创建当前类对象
private static Singleton2 singleton = new Singleton2();

/**
* 构造方式私有化,保证在外部无法通过new来创建对象
*/

private Singleton2() {
}

/**
* 提供一个static方法来获取当前实例
*
* @return
*/

public static Singleton2 getInstance() {
return singleton;
}
}

该方式的优点在于线程安全,代码简单。而缺点在于当类加载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候就不需要再判断,节省了运行时间。

4.3、双重检查

public class Singleton3 {
//声明一个私有的静态变量,使用volatile修饰
private static volatile Singleton3 singleton;

/**
* 构造方式私有化,保证在外部无法通过new来创建对象
*/

private Singleton3() {
}

/**
* 提供一个static方法来获取当前实例
*
* @return
*/

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

使用“双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受很大的影响。所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

这里通常会涉及到一个常见的面试题:为什么使用volatile修饰singleton变量?

volatile的作用是: 防止重排序优化,如果不用volatile修饰,多线程的情况下,可能会出现线程A进入synchronized代码块,执行new Singleton3();,首先给singleton分配内存,但是还没有初始化变量,这时候线程B进入getInstance方法,进行第一个判断,此时singleton已经不为空,直接返回singleton,然后肯定报错。使用volatile修饰之后禁止jvm重排序优化,所以就不会出现上面的问题。

注意: 由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

4.4、静态内部类

public class Singleton4 {

/**
* 构造方式私有化,保证在外部无法通过new来创建对象
*/

private Singleton4() {
}

/**
* 静态内部类
*/

private static class SingletonHolder {
private static final Singleton4 instance = new Singleton4();
}

public static Singleton4 getInstance() {
return SingletonHolder.instance;
}
}

getInstance()方法第一次被调用的时候,它第一次读取SingletonHolder.INSTANCE,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton4的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

4.5、枚举实现

public enum Singleton5 {

SINGLETON;

public void method() {
//do something
}
}

使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

5、单例模式在JDK中的应用

在JDK中,java.lang.Runtime就是经典的单例模式(饿汉式)。其源代码片段如下:

public class Runtime {
private static Runtime currentRuntime = new Runtime();

/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/

public static Runtime getRuntime() {
return currentRuntime;
}

/** Don't let anyone else instantiate this class */
private Runtime() {}

6、其它说明

(1)单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,同时对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
(2)单例模式使用的场景:需要频繁的创建和销毁对象、创建对象时耗时过多或耗费资源过多但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、Hibernate的sessionFactory等)。

END


如有收获,请划至底部,点击"在看"。万分感谢!