一. 前言
在常用模式中,单例模式是唯一一个能够用短短几十行代码完整实现的模式,所以,单例模式常常出现在面试题中. 在此,在前人的基础上,对其做个总结.
本文主要围绕以下几个问题展开:
1. 单例模式是什么? (what)
2. 什么时候会用到? 使用过程中,单例模式有什么优势? (why)
3. 怎么实现单例模式? (how)
二. 概述及应用场景
1. 定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式要求一个类有且仅有一个实例,并且提供了一个全局的访问点。这就提出了一个问题:如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?客户程序在调用某一个类时,它是不会考虑这个类是否只能有一个实例等问题的,所以,这应该是类设计者的责任,而不是类使用者的责任。
从另一个角度来说,单例模式其实也是一种职责型模式。因为我们创建了一个对象,这个对象扮演了独一无二的角色,在这个单独的对象实例中,它集中了它所属类的所有权力,同时它也肩负了行使这种权力的职责!
2. 日常生活中的例子
- 我们使用的电脑下的回收站就是典型的例子。在整个系统运行过程中,回收站一直维护着仅有的一个实例.
- 网站的计数器,一般也是采用单例模式实现,否则难以同步.
- 还有应用程序的日志,日志是共享的,因为只有一个实例去操作,所以内容才同步.
从以上可看出,
单例模式应用场景一般具备以下条件:
(1) 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置等等.
(2) 控制资源的情况下,方便资源之间的互相通信。如线程池等。
3. 使用单例的优点
- 单例类只有一个实例
- 共享资源,全局使用
- 节省创建时间,提高性能
不足:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
三. 模型图
四. 实现Singleton模型的多种解法
1.懒汉式,线程不安全
由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以禁止他人创建实例.我们定义一个静态的实例,在需要的时候创建该实例.
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)。
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
虽然做到了线程安全,并且解决了多实例的问题,但是它并不完美.我们每次线程调用getInstance() 方法时,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免.
3.双重检验锁
我们只是在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例,而当实例已经创建之后,我们不需要再做加锁操作.于是,对第二种解法可以做进一步改进:
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 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
这种方式和名字很贴切,饥不择食,在类装载的时候就创建,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。
5.静态内部类
我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。
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》中作者推荐了一种更简洁方便的使用方式,就是使用「枚举」。
public enum Singleton {
//定义一个枚举的元素,它就是 Singleton 的一个实例
INSTANCE;
public void doSomeThing() {
// do something...
}
}
使用方法如下:
public static void main(String args[]) {
Singleton singleton = Singleton.instance;
singleton.doSomeThing();
}
枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。
五. 代码实现
这是一个简单的计数器例子,四个线程同时进行计数。
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();
}
}
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 = "";
}
}
}
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;
}
}
最终输出结果:
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
六. 总结
一般来说,线程安全的单例模式常用有5种写法:[懒汉]、[饿汉]、[双重检验锁]、[静态内部类]、[枚举]。
很多时候取决人个人的喜好,我比较钟爱双重检验锁,觉得这种方式可读性高、安全、优雅,有时为了方便,也会使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。