单例模式是我们开发过程中很常用也是比较简单的一种设计模式,使用单例模式的目的是使类的一个对象成为系统的唯一一个实
例。举个大家都熟知的例子----Windows任务管理器,在Windows的任务栏里右击选择"任务管理器",会打开一个"任务管理器"窗口
(如下图所示),再连续同样操作几次看看到底打开的是多个窗口还是一个窗口,在不修改Windows内核的情况下,打开的肯定是唯一
一个窗口,也就是说"任务管理器"在我们Windows操作系统中是一个单例,那么微软为什么要这么设计呢?我们可以从以下两点来分
析:第一,如果打开的俩"任务管理器"窗口内容是一致的,那么就是重复对象,"任务管理器"获取电脑的实时状态信息是需要耗费
一定的系统资源的,重复对象显然是在浪费资源。第二,如果打开的窗口显示的内容不一致,那么情况可就糟糕了,一个窗口显示
CPU占用率5%,另一个显示CPU占用率90%,那么到底哪个是对的呢?综上可知,微软把"任务管理器"设计成单例模式是明智的。
回到软件开发中,有时候我们需要节约系统资源,有时为了确保系统中只有唯一一个实例,所有的操作只能基于这个实例。为了确保这个实例的唯一性,我们需要借助单例模式来实现。
以任务管理器为例,任务管理器里有大量成员方法,我选取显示进程和服务为例,对单例模式进行简要介绍。
实现单例模式有三个要点:第一,某个类只能有一个实例;第二,它必须能自行创建一个实例;第三,它必须能自行向整个系统提供一个全局访问方法。
一.饿汉模式:
package com.moluo.test;
/**
* @author xingzhemoluo
*/
public class TaskManager {
//饿汉模式 线程安全
//instance在类装载时就实例化,避免了多线程同步问题
private static TaskManager instance = new TaskManager();
private TaskManager() {
// 执行初始化操作,必须是private,因为要防止其他类采用new的方式来创建实例
}
public void displayService() {
// 显示服务
}
public void displayProcess() {
// 显示进程
}
public static TaskManager getInstance() {
return instance;
}
}
首先声明并创建一个静态的实例,然后将构造行数声明为private,因为这样可以防止在其他类中采取直接New的方式来直接创建一个实例。最后提供一个全局访问方法,讲这个instance提供给外界访问。这个instance在类加载的时候就实现了实例化,它是线程安全的,但没有实现延迟加载,因为实例是由JVM自行创建的,但我们有时候不需要这个实例,但却被JVM加载了,显然是浪费系统资源,所以我们必须改进这种"饿汉模式"来实现延迟加载,于是就有了下面那种"懒汉模式"。
二.懒汉模式:
package com.moluo.test;
/**
* @author xingzhemoluo
*/
public class TaskManager1 {
//懒汉模式 线程不安全
private static TaskManager1 instance;
private TaskManager1() {
// 执行初始化操作,必须是private,因为要防止其他类采用new的方式来创建实例
}
public void displayService() {
// 显示服务
}
public void displayProcess() {
// 显示进程
}
public static TaskManager1 getInstance() {
if (instance == null) {
instance = new TaskManager1();
}
return instance;
}
}
与第一种"饿汉模式" 的区别就是,声明instance的时候并没有直接创建,只是声明罢了,然后在全局访问方法里,也就是getInstance()里执行instance = new TaskManager1()来创建一个实例。当系统调用这个getInstance()方法的时候才会执行实例化,显然这个"懒汉模式"实现了延迟加载,即使用才加载,不使用不加载。但这个"懒汉模式"显然不是线程安全的,因为当多个线程同时访问这个getInstance方法,并且都是在同一时刻通过了If语句,那么就会创建多个实例。因此此方法在多线程并发访问的时候可能会造成实例不是唯一的,所以我们必须加同步锁来使线程安全。
三.改进过的懒汉模式:
package com.moluo.test;
/**
* @author xingzhemoluo
*/
public class TaskManager2 {
//使用同步锁改进过的懒汉模式以处理多个线程同时访问的问题。
private static TaskManager2 instance;
private TaskManager2() {
// 执行初始化操作,必须是private,因为要防止其他类采用new的方式来创建实例
}
public void displayService() {
// 显示服务
}
public void displayProcess() {
// 显示进程
}
public static synchronized TaskManager2 getInstance() {
if (instance == null) {
instance = new TaskManager2();
}
return instance;
}
}
与上面那种"懒汉模式"
相比,这个改进过的
"懒汉模式"只是在全局访问方法getInstance()的签名加上同步锁:synchronized,此改进方法是由JVM来对线程进行锁定,所以线程是安全的,因此创建的实例也是唯一的。但此方法有个不好的缺点:每次调用getInstance()方法的时候都要进行线程锁定判断,因此性能较低,所以该方法还是需要改进。
四.再次改进过的懒汉模式:
package com.moluo.test;
/**
* @author xingzhemoluo
*/
public class TaskManager3 {
//解决了线程安全问题,也实现了延迟加载,但有可能产生多个实例
private static TaskManager3 instance;
private TaskManager3() {
// 执行初始化操作,必须是private,因为要防止其他类采用new的方式来创建实例
}
public void displayService() {
// 显示服务
}
public void displayProcess() {
// 显示进程
}
//将synchronized放到里面来实现延迟加载
public static TaskManager3 getInstance() {
if (instance == null) {
synchronized (TaskManager3.class) {
instance = new TaskManager3();
}
}
return instance;
}
}
与上述改进过的"懒汉模式"相比,在全局访问方法创建实例的时候加同步锁,此方法实现了延迟加载,线程也是安全的,但却还是可能产生多个实例。
假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象。
五.双重判断
package com.moluo.test;
/**
* @author xingzhemoluo
*/
public class TaskManager4 {
//双重锁定,但使用了volatile修饰词,屏蔽了jvm的一些代码优化,代码效率不高
private static volatile TaskManager4 instance;
private TaskManager4() {
// 执行初始化操作,必须是private,因为要防止其他类采用new的方式来创建实例
}
public void displayService() {
// 显示服务
}
public void displayProcess() {
// 显示进程
}
//将synchronized放到里面,并且进行双重判断来实现延迟加载
public static TaskManager4 getInstance() {
if (instance == null) {
synchronized (TaskManager4.class) { //锁定代码块
if (instance == null) {
instance = new TaskManager4();
}
}
}
return instance;
}
}
与上述改进方法相比,此改进方法知识在锁定代码块里再加上一个判断是否为空的条件,注意:双重判断必须在声明Instance的时候加上volatile来修饰,这样才可以正确处理多线程,才能实现单例。但是vollatile会屏蔽Java虚拟机做的一些代码优化,效率较低。所以,还需要改进。
六.静态内部类实现单例
package com.moluo.test;
/**
* @author xingzhemoluo
*/
public class TaskManager5 {
//在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用
private TaskManager5() {
// 执行初始化操作,必须是private,因为要防止其他类采用new的方式来创建实例
}
public void displayService() {
// 显示服务
}
public void displayProcess() {
// 显示进程
}
public static TaskManager5 getInstance() {
return ClassHolder.instance;
}
//内部类
private static class ClassHolder {
private static TaskManager5 instance = new TaskManager5();
}
}
此方法首先是创建了一个静态内部类ClassHolder,并在这个静态内部类里声明并创建一个instance,然后在全局访问方法里调用这个instance,以此来实现延迟加载,并实现单例。因为静态内部类只有在调用getInstance()的时候才会主动去加载静态内部类,这样就实现了延迟加载,并且由JVM来确保这个Instance只被初始化一次,同时没有线程锁定,所以性能较好,显然这种方法是最好的一种单例模式实现方法,既实现了单例,又实现了延迟加载,同时效率又高。
单例模式总结
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
1.主要优点
单例模式的主要优点如下:
(1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系 统的性能。
(3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系 统资源,又解决了单例单例对象共享过多有损性能的问题。
2.主要缺点
单例模式的主要缺点如下:
(1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了 产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
(3) 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利 用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3.适用场景
在以下情况下可以考虑使用单例模式:
(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创 建一个对象。
(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
参考资料:http://blog.csdn.net/lujiancs/article/details/8278843
尊重版权,转载请注明本文链接
欢迎关注行者摩罗微信公众号(xingzhemoluo),共同交流编程经验,扫描下方二维码即可;