设计模式-单例模式介绍+8种实现方式

关于单例模式,思想很简单,就是确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,减少内存开支

主要有两大实现方式:懒汉式、饿汉式

  • 饿汉式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。
  • 懒汉式:在类加载时不初始化,等到第一次被使用时才初始化。

打一个很形象的比喻,有两个很饿的老汉,一个很懒、一个很勤快,懒汉要别人把饭做好送到嘴边的才肯吃;饿汉呢就是拿着碗等着饭熟,饭一熟立马盛饭开吃

那么在程序里面,懒汉式是用的最多,因为它可以减少性能开销。但是懒汉式在单线程环境中没有任何问题,一旦处于多线程环境,那么它是线程不安全的,而如何保证线程安全,各个语言又会有不同的处理方式,本文以JAVA语言为例,来进行介绍单例模式思想实现单例模式的8种方式

首先我们还是以一个非常简单例子为例,介绍单例模式的思想,如果了解了单例模式,可以跳过这小节

1.我是皇帝我独苗

自从秦始皇确立了皇帝这个位置以后,同一时期基本上就只有一个人孤零零地坐在这个位置。这种情况下臣民们也好处理,大家叩拜、谈论的时候只要提及皇帝,每个人都知道指的是谁,而不用在皇帝前面加上特定的称呼,如张皇帝、李皇帝。这一个过程反应到设计领域就是,要求一个类只能生成一个对象(皇帝),所有对象对它的依赖都是相同的,因为只有一个对象,大家对它的脾气和习性都非常了解,建立健壮稳固的关系,我们把皇帝这种特殊职业通过程序来实现。

皇帝每天要上朝接待臣子、处理政务,臣子每天要叩拜皇帝,皇帝只能有一个,也就是一个类只能产生一个对象,该怎么实现呢?对象产生是通过new关键字完成的(当然也有其他方式,比如对象复制、反射等),这个怎么控制呀,但是大家别忘记了构造函数,使用new关键字创建对象时,都会根据输入的参数调用相应的构造函数,如果我们把构造函数设置为private私有访问权限不就可以禁止外部创建对象了吗?臣子叩拜唯一皇帝的过程类图

只有两个类,Emperor代表皇帝类,Minister代表臣子类,关联到皇帝类非常简单。

1
2
3
4
5
6
7
8
9
10
11
12
public class Emperor {
private static final Emperor emperor =new Emperor(); //初始化一个皇帝
private Emperor(){
//世俗和道德约束你,目的就是不希望产生第二个皇帝}
public static Emperor getInstance(){
return emperor;
}
//皇帝发话了
public static void say(){
System.out.println("我就是皇帝某某某....");
}
}

通过定义一个私有访问权限的构造函数,避免被其他类new出来一个对象,而Emperor自己则可以new一个对象出来,其他类对该类的访问都可以通过getInstance获得同一个对象。

皇帝有了,臣子要出场

1
2
3
4
5
6
7
8
public class Minister {
public static void main(String[] args) {
for(int day=0;day<3;day++){
Emperor emperor=Emperor.getInstance();emperor.say();
}
//三天见的皇帝都是同一个人,荣幸吧!
}
}

臣子参拜皇帝的运行结果如下所示。

1
2
3
4
5
我就是皇帝某某某....

我就是皇帝某某某....

我就是皇帝某某某....

臣子天天要上朝参见皇帝,今天参拜的皇帝应该和昨天、前天的一样(过渡期的不考虑,别找茬哦),大臣磕完头,抬头一看,嗨,还是昨天那个皇帝,老熟人了,容易讲话,这就是单例模式。

2 单例模式的定义

单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:

Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

单例模式的通用类图如下图所示

Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {
private static final Singleton singleton = new Singleton();
//限制产生多个对象
private Singleton(){
}
//通过该方法获得实例对象
public static Singleton getSingleton(){
return singleton;
}
//类中其他方法,尽量是static
public static void doSomething(){
}
}

3 单例模式的应用

3.1 单例模式的优点

● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。

● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

3.2 单例模式的缺点

● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

3.3 单例模式的使用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:

● 要求生成唯一序列号的环境;

● 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;

● 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;

● 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

4 实现单例模式的8种方式

介绍之前我们先看看java种类的加载顺序

类加载(classLoader)机制一般遵从下面的加载顺序

如果类还没有被加载:

  • 先执行父类的静态代码块和静态变量初始化,静态代码块和静态变量的执行顺序跟代码中出现的顺序有关。
  • 执行子类的静态代码块和静态变量初始化。
  • 执行父类的实例变量初始化
  • 执行父类的构造函数
  • 执行子类的实例变量初始化
  • 执行子类的构造函数

同时,加载类的过程是线程私有的,别的线程无法进入。

如果类已经被加载:

静态代码块静态变量不再重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。

static关键字

一个类中如果有成员变量或者方法被static关键字修饰,那么该成员变量或方法将独立于该类的任何对象。它不依赖类特定的实例,被类的所有实例共享,只要这个类被加载,该成员变量或方法就可以通过类名去进行访问,它的作用用一句话来描述就是,不用创建对象就可以调用方法或者变量,这简直就是为单例模式的代码实现量身打造的。

4.1 静态常量(饿汉模式)

这一种是使用静态常量的方式创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingletonTest01 {
public static void main(String[] args) {
SingletonClass01 c1 = SingletonClass01.getInstance();
SingletonClass01 c2 = SingletonClass01.getInstance();
System.out.println(c1 == c2);
}
}
class SingletonClass01 {

// 1. 构造器私有化
private SingletonClass01() {

}

// 2.本类内部创建对象实例
private final static SingletonClass01 instance = new SingletonClass01();

// 3. 提供一个公有的静态方法,返回实例对象
public static SingletonClass01 getInstance() {
return instance;
}
}

4.2 静态代码块(饿汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class SingletonTest02 {
public static void main(String[] args) {
SingletonClass02 c1 = SingletonClass02.getInstance();
SingletonClass02 c2 = SingletonClass02.getInstance();
System.out.println(c1 == c2);
}
}

class SingletonClass02 {

//1. 构造器私有化, 外部不能new
private SingletonClass02() {

}

//2.本类内部创建对象实例
private static SingletonClass02 instance;

static { // 在静态代码块中,创建单例对象
instance = new SingletonClass02();
}

//3. 提供一个公有的静态方法,返回实例对象
public static SingletonClass02 getInstance() {
return instance;
}

}

4.3 线程不安全(懒汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class SingletonTest03 {
public static void main(String[] args) {
// System.out.println("单线程创建实例=======");
// SingletonClass03 c1 = SingletonClass03.getInstance();
// SingletonClass03 c2 = SingletonClass03.getInstance();
// System.out.println(c1 == c2);
System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass03.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

class SingletonClass03 {
private static SingletonClass03 instance;
//1. 构造器私有化, 外部不能new
private SingletonClass03() {
}

//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
//即懒汉式
public static SingletonClass03 getInstance() {
if (instance == null) {
instance = new SingletonClass03();
}
return instance;
}
}

多线程环境测试结果

1
2
3
4
5
6
7
8
9
10
11
多线程创建实例=======
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@4bd8a307
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207
cn.mrxccc.singleton.SingletonClass03@576cc207

这种是线程不安全的单例模式,从多线程的测试结果可以看出,在创建实例时创建了新的对象,这样的单例模式就是线程不安全单例模式,不推荐该方式

4.4 线程安全,同步方法(懒汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SingletonTest04 {
public static void main(String[] args) {
// System.out.println("单线程创建实例=======");
// SingletonClass04 c1 = SingletonClass04.getInstance();
// SingletonClass04 c2 = SingletonClass04.getInstance();
// System.out.println(c1 == c2);

System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass04.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

// 懒汉式(线程安全,同步方法)
class SingletonClass04 {
private static SingletonClass04 instance;
//1. 构造器私有化, 外部不能new
private SingletonClass04() {}

//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
//即懒汉式
public static synchronized SingletonClass04 getInstance() {
if(instance == null) {
instance = new SingletonClass04();
}
return instance;
}
}

这种是线程安全的单例模式,特点就是给获取实例的方法getInstance()加了一个synchronized关键字,加上锁使其成为一个同步方法,保证了同一时刻只能有一个线程访问并获得实例,但是缺点也很明显,因为synchronized是修饰整个方法,每个线程访问都要进行同步,而其实这个方法只执行一次实例化代码就够了,每次都同步方法显然效率低下,不推荐

4.5双重检查(懒汉式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SingletonTest05 {
public static void main(String[] args) {
SingletonClass05 c1 = SingletonClass05.getInstance();
SingletonClass05 c2 = SingletonClass05.getInstance();
System.out.println(c1 == c2);
System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass05.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

// 懒汉式(线程安全,同步方法)
class SingletonClass05 {
// volatile禁止重排序
private static volatile SingletonClass05 instance;

private SingletonClass05() {}

//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
//同时保证了效率, 推荐使用
public static SingletonClass05 getInstance() {
if(instance == null) {
synchronized (SingletonClass05.class) {
if(instance == null) {
instance = new SingletonClass05();
}
}

}
return instance;
}
}

这种写法用了两个if判断,也就是Double-Check,并且同步的不是方法,而是代码块,效率较高,是对第四种写法的改进。为什么要做两次判断呢?这是为了线程安全考虑,还是那个场景,对象还没实例化,两个线程A和B同时访问静态方法并同时运行到第一个if判断语句,这时线程A先进入同步代码块中实例化对象,结束之后线程B也进入同步代码块,如果没有第二个if判断语句,那么线程B也同样会执行实例化对象的操作了。

给对象实例加上volatile保证变量的可见性,是为了防止指令重排,这里介绍指令重排,有点偏移文章重点,暂不介绍,关于什么是指令重排可以网上搜一下相关文章

4.6 静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class SingletonTest06 {
public static void main(String[] args) {
// System.out.println("单线程创建实例=======");
// SingletonClass06 c1 = SingletonClass06.getInstance();
// SingletonClass06 c2 = SingletonClass06.getInstance();
// System.out.println(c1 == c2);
System.out.println("多线程创建实例=======");
//创建10个线程, 在每个 线程中打印单例对象
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//调用Singleton.getInstance()返回单例对象,打印会输出对象的哈希码
System.out.println(SingletonClass06.getInstance());
}
}).start();
}
//程序运行后,输出单例的哈希码都相同,说明是同一个对象
}
}

// 静态内部类完成:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
// 推荐使用
class SingletonClass06 {

//构造器私有化
private SingletonClass06() {}

//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private final static SingletonClass06 INSTANCE = new SingletonClass06();
}

//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE

public static SingletonClass06 getInstance() {
return SingletonInstance.INSTANCE;
}
}

这是很多开发者推荐的一种写法,这种静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成对象的实例化。

同时,因为类的静态属性只会在第一次加载类的时候初始化,也就保证了SingletonInstance中的对象只会被实例化一次,并且这个过程也是线程安全的。

推荐使用

4.7枚举

这种写法在《Effective JAVA》中大为推崇,它可以解决两个问题:

1)线程安全问题。因为Java虚拟机在加载枚举类的时候会使用ClassLoader的方法,这个方法使用了同步代码块来保证线程安全。

2)避免反序列化破坏对象,因为枚举的反序列化并不通过反射实现。

推荐使用

4.8 静态内部类+防止反射破坏单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class SingletonTest08 {
public static void main(String[] args) throws Exception {
Class clazz = SingletonClass08.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object c1 = constructor.newInstance();
Object c2 = SingletonClass08.getInstance();
System.out.println(c1 == c2);
}
}

// 静态内部类完成:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存
// 推荐使用
class SingletonClass08 {

// 构造器私有化
private SingletonClass08() {
if (SingletonInstance.INSTANCE != null) {
throw new RuntimeException("不允许创建多个实例");
}
}

// 写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private static final SingletonClass08 INSTANCE = new SingletonClass08();
}

// 提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static SingletonClass08 getInstance() {
return SingletonInstance.INSTANCE;
}
}

网上的大部分文章介绍单例模式都没有介绍到这一种,这里使用了抛异常的方式防止反射进行创建多个示例

5 总结

至此,关于单例模式的介绍和场景使用已经介绍完了,有任何疑问和沟通讨论的可以给博主留言哦

其他设计模式介绍:

设计模式-工厂方法模式

设计模式-抽象工厂模式

设计模式-建造者模式

更多精彩内容:mrxccc