Contents
  1. 1. 一. 前言
  2. 2. 二. 概述及应用场景
  3. 3. 三. 模型图
  4. 4. 四. 实现Singleton模型的多种解法
    1. 4.1. 1.懒汉式,线程不安全
    2. 4.2. 2.懒汉式,线程安全
    3. 4.3. 3.双重检验锁
    4. 4.4. 4.饿汉式
    5. 4.5. 5.静态内部类
    6. 4.6. 6.枚举
  5. 5. 五. 代码实现
  6. 6. 六. 总结
  7. 7. 参考资料

一. 前言

在常用模式中,单例模式是唯一一个能够用短短几十行代码完整实现的模式,所以,单例模式常常出现在面试题中. 在此,在前人的基础上,对其做个总结.

本文主要围绕以下几个问题展开:

  1. 单例模式是什么? (what)
  2. 什么时候会用到? 使用过程中,单例模式有什么优势? (why)
  3. 怎么实现单例模式? (how)

二. 概述及应用场景

1. 定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式要求一个类有且仅有一个实例,并且提供了一个全局的访问点。这就提出了一个问题:如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?客户程序在调用某一个类时,它是不会考虑这个类是否只能有一个实例等问题的,所以,这应该是类设计者的责任,而不是类使用者的责任。

从另一个角度来说,单例模式其实也是一种职责型模式。因为我们创建了一个对象,这个对象扮演了独一无二的角色,在这个单独的对象实例中,它集中了它所属类的所有权力,同时它也肩负了行使这种权力的职责!

2. 日常生活中的例子

  • 我们使用的电脑下的回收站就是典型的例子。在整个系统运行过程中,回收站一直维护着仅有的一个实例.
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步.
  • 还有应用程序的日志,日志是共享的,因为只有一个实例去操作,所以内容才同步.

从以上可看出,
单例模式应用场景一般具备以下条件:

(1) 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置等等.
(2) 控制资源的情况下,方便资源之间的互相通信。如线程池等。

3. 使用单例的优点

  • 单例类只有一个实例
  • 共享资源,全局使用
  • 节省创建时间,提高性能

不足:

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

三. 模型图

这里写图片描述

四. 实现Singleton模型的多种解法

1.懒汉式,线程不安全

由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以禁止他人创建实例.我们定义一个静态的实例,在需要的时候创建该实例.

1
2
3
4
5
6
7
8
9
10
11
public class Singleton(){
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。

2.懒汉式,线程安全

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

1
2
3
4
5
6
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不完美.我们每次线程调用getInstance() 方法时,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免.

3.双重检验锁

我们只是在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例,而当实例已经创建之后,我们不需要再做加锁操作.于是,对第二种解法可以做进一步改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

这种实现方式对多线程来说是安全的,同时线程不是每次都加锁,只有判断对象实例没有被创建时它才加锁. 但是这样的代码实现起来比较复杂,容易出错,是否有更优秀的解法.

4.饿汉式

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的

1
2
3
4
5
6
7
8
9
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}

这种方式和名字很贴切,饥不择食,在类装载的时候就创建,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。

5.静态内部类

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.Instance;
}
private static class SingletonHolder {
private static final Singleton Instance = new Singleton();
}
}

第一次加载Singleton类时并不会初始化Instance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化Instance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

6.枚举

《Effective Java》中作者推荐了一种更简洁方便的使用方式,就是使用「枚举」。

1
2
3
4
5
6
7
8
public enum Singleton {
//定义一个枚举的元素,它就是 Singleton 的一个实例
INSTANCE;
public void doSomeThing() {
// do something...
}
}

使用方法如下:

1
2
3
4
public static void main(String args[]) {
Singleton singleton = Singleton.instance;
singleton.doSomeThing();
}

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。

五. 代码实现

这是一个简单的计数器例子,四个线程同时进行计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package singleton;
/**
* 执行线程
* @author dingding
*
*/
public class CountClient {
public static void main(String[] args) {
CountMutilThread cmt0 = new CountMutilThread("Thread 0");
CountMutilThread cmt1 = new CountMutilThread("Thread 1");
CountMutilThread cmt2 = new CountMutilThread("Thread 2");
CountMutilThread cmt3 = new CountMutilThread("Thread 3");
CountMutilThread cmt4 = new CountMutilThread("Thread 4");
cmt0.start();
cmt1.start();
cmt2.start();
cmt3.start();
cmt4.start();
}
}
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
package singleton;
/**
* 多线程计数
* @author dingding
* Date:2017-5-27
*/
public class CountMutilThread extends Thread{
public CountMutilThread(String name) {
super();
this.setName(name); //设置线程名称
}
@Override
public void run(){
//构造显示字符串
String result = "";
//创建单例实例
CountSingleton countSingleton = CountSingleton.getInstance();
//循环调用四次
for (int i=1;i<5;i++){
//countSingleton.add();
result += Thread.currentThread().getName()+"-->";
result += "当前的计数值:";
result += countSingleton.getCounter();
result += "\n";
System.out.println(result);
result = "";
}
}
}
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
package singleton;
/**
* 单例模式-利用静态内部类
* @author dingding
*
*/
public class CountSingleton {
private int totNum = 0; //存储计数值
private CountSingleton (){}
private static class SingletonHolder {
private static final CountSingleton INSTANCE = new CountSingleton();
}
public static final CountSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
// public void add(){
// totNum = totNum+1;
// }
//
//计数加1,获取当前计数值
public int getCounter(){
totNum = totNum+1;
return totNum;
}
}

最终输出结果:

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
Thread 2-->当前的计数值:2
Thread 2-->当前的计数值:6
Thread 2-->当前的计数值:7
Thread 4-->当前的计数值:5
Thread 1-->当前的计数值:4
Thread 3-->当前的计数值:3
Thread 0-->当前的计数值:1
Thread 0-->当前的计数值:12
Thread 0-->当前的计数值:13
Thread 0-->当前的计数值:14
Thread 3-->当前的计数值:11
Thread 3-->当前的计数值:15
Thread 3-->当前的计数值:16
Thread 1-->当前的计数值:10
Thread 4-->当前的计数值:9
Thread 2-->当前的计数值:8
Thread 4-->当前的计数值:18
Thread 1-->当前的计数值:17
Thread 1-->当前的计数值:20
Thread 4-->当前的计数值:19

六. 总结

Singleton设计模式是一个非常有用的机制,可用于在面向对象的应用程序中提供单个访问点。
用一句广告词来概括Singleton模式就是“简约而不简单”。

一般来说,线程安全的单例模式常用有5种写法:[懒汉]、[饿汉]、[双重检验锁]、[静态内部类]、[枚举]。

很多时候取决人个人的喜好,我比较钟爱双重检验锁,觉得这种方式可读性高、安全、优雅,有时为了方便,也会使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

参考资料

  1. 单例模式
  2. 单件模式(Singleton Pattern)
  3. 【Java】设计模式:深入理解单例模式
  4. 如何正确地写出单例模式
  5. 设计模式之——单例模式(Singleton)的常见应用场景
comments powered by HyperComments
Contents
  1. 1. 一. 前言
  2. 2. 二. 概述及应用场景
  3. 3. 三. 模型图
  4. 4. 四. 实现Singleton模型的多种解法
    1. 4.1. 1.懒汉式,线程不安全
    2. 4.2. 2.懒汉式,线程安全
    3. 4.3. 3.双重检验锁
    4. 4.4. 4.饿汉式
    5. 4.5. 5.静态内部类
    6. 4.6. 6.枚举
  5. 5. 五. 代码实现
  6. 6. 六. 总结
  7. 7. 参考资料
Fork me on GitHub