一. 前言
在常用模式中,单例模式是唯一一个能够用短短几十行代码完整实现的模式,所以,单例模式常常出现在面试题中. 在此,在前人的基础上,对其做个总结.
本文主要围绕以下几个问题展开:
- 单例模式是什么? (what)
- 什么时候会用到? 使用过程中,单例模式有什么优势? (why)
- 怎么实现单例模式? (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; 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 { INSTANCE; public void doSomeThing() { } }
|
使用方法如下:
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++){ 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 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种写法:[懒汉]、[饿汉]、[双重检验锁]、[静态内部类]、[枚举]。
很多时候取决人个人的喜好,我比较钟爱双重检验锁,觉得这种方式可读性高、安全、优雅,有时为了方便,也会使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。
参考资料
- 单例模式
- 单件模式(Singleton Pattern)
- 【Java】设计模式:深入理解单例模式
- 如何正确地写出单例模式
- 设计模式之——单例模式(Singleton)的常见应用场景