Java 设计模式之单例模式
在《Head First 设计模式》一书中,将单例模式称作单件模式。这里为了适应大环境,把它称之为大家更熟悉的单例模式。
一、了解单例模式
1.1 什么是单例模式
单例模式确保一个类只有一个实例,并提供一个安全访问点。
我们把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。想要获取单例实例,通过单例类是唯一的途径。单例类提供对这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例。
1.2 单例模式 UML 图解
1.3 单例模式应用场景
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。比如线程池、缓存、日志对象等。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
- 以及要求只有一个对象的场景。
二、单例模式具体应用
2.1 经典的单例模式实现
采用经典单例模式实现代码有一个特点:如果我们不需要这个实例 (调用 getInstance() 方法),它就永远不会产生。因此这种方式也被称为“延迟实例化”(lazy instantiaze)。也被大家称为“懒汉式”。
单例类 Singleton
package com.jas.singleton;
public class Singleton {
// 用静态变量来记录 Singleton 类的唯一实例
private static Singleton uniqueInstance;
/**
* 把构造器声明为私有的,只有自己 Singleton 内部才可以调用构造器
*/
private Singleton(){}
/**
* getInstance() 方法来实例化对象
*
* @return Singleton 的实例对象
*/
public static Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
测试类
package com.jas.singleton;
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
/**
* 输出
* true
*/
这虽然是经典的单例模式,但是这样做却存在着一个严重的问题:当多个线程同时访问 getInstance() 方法时,会产生线程安全问题,可能导致产生的实例可能会有多个,这样就违反了单例的原则。
2.2 处理多线程
存在线程安全问题,我们的第一反应可能是加同步锁。就像下面这样,这样做是可以解决线程安全问题,但是却降低了性能。因为只有在第一次执行该方法的时候,才真正需要同步。之后再调用此方法,同步反而会成为一种累赘。
/**
* 通过 synchronized 关键字,来保证不会同时有两个线程进入该方法
*
* @return Singleton 的实例对象
*/
public synchronized static Singleton getInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
2.3 改善多线程问题
为了符合大多数 Java 程序,很明显地,我们需要确保单例模式能在多线程的情况下正常工作。但是同步的做法会击垮其性能,所以提供以下几种方法来解决问题。
(1) 直接同步
直接同步虽然会降低性能,但是如果你的程序可以承受 getInstance() 造成的额外代价,同步确实是一种既简单又有效的方法。但是你必须知道,同步一个方法,可能会使程序的执行效率下降几十倍。因此,如果你需要频繁使用单例对象,那么你就要重新考虑设计了。
(2) “急切”创建实例
如果应用程序总是创建并使用单例创建的对象,或者在创建和运行时方面的负担不太严重,你可以急切 (early) 创建此对象。这种方式也被大家称为“恶汉式”。就像下面这样
package com.jas.singleton;
public class Singleton {
//在静态初始化器中创建对象,用来保证线程安全
private static Singleton uniqueInstance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return uniqueInstance;
}
}
利用上面这种做法,我们依赖 JVM 在加载这个类时马山创建此唯一的实例。JVM 保证在任何时候任何线程访问 getInstance() 方法之前,一定会先创建此实例。这样一来就可以解决多线程之间的安全问题。
(3)双重检验加锁
利用双重检验加锁 (double-checked locking),首先检查实例是否已经被创建了,如果未创建,“才”开始同步。这样一来,只有第一次会同步,这样做正是我们想要的。
package com.jas.singleton;
public class Singleton {
//volatile 关键字用来保证内存可见性,使多线程正确处理 uniqueInstance 对象
private static volatile Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance(){
//使用这种方式,只有第一次才会彻底访问并执这里的代码
if(uniqueInstance == null){ //检查实例,如果不存在进入同步区
synchronized (Singleton.class){
if(uniqueInstance == null){ //进入同步区后,再检查一次。如果为 null,才开始创建实例
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
如果你性能是你关心的重点,那么这种方式会帮你大大减少访问 getInstance() 时的时间消耗。需要在注意的是:这种双重检验加锁的方式并不适用于 1.4 及之前更早的版本。
三、单例模式总结
3.1 优缺点总结
优点
- 实例控制:单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
- 灵活性:因为类控制了实例化过程,所以类可以灵活更改实例化过程。
缺点
- 开销:虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例。
- 可能的开发混淆:使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用 new 关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。
- 对象生存期:不能解决删除单个对象的问题。在提供内存管理的语言,只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。
3.2 部分知识总结
- 单例模式确保程序中一个类最多只有一个实例。单例模式也提供访问这个实例的全局点。
- 如果你使用多个类加载器,可能导致单例模式失效,从而产生多个实例。类加载器可参考博文:虚拟机类加载机制 。
- 确定性能和资源上的限制,我们应当选择合适的方案来实现单例模式。
PS:点击了解更多设计模式 http://blog.csdn.net/codejas/article/details/79236013
四、参考文献
《Head First 设计模式》
https://www.cnblogs.com/tufujie/p/5614682.html
下一篇: 如何隐藏掉Nginx的版本号