设计模式学习总结-单例模式

1. 简介

单例模式确保一个类只有一个实例,自行提供这个实例并向整个系统提供这个实例。
特点:

1) 一个类只能有一个实例
2) 自己创建这个实例
3) 整个系统都要使用这个实例

2. 两种单例类型

1) 饿汉式单例类

public class Singleton {
 
private Singleton(){}
 
//在自己内部定义自己一个实例,是不是很奇怪?
//注意这是private 只供内部调用
 
private static Singleton instance = new Singleton();
 
//这里提供了一个供外部访问本class的静态方法,可以直接访问
public static Singleton getInstance() {
return instance;
}
}

2) 懒汉式单例类

public class Singleton {
 
private static Singleton instance = null;
 
public static synchronized Singleton getInstance() {
 
//这个方法比上面有所改进,不用每次都进行生成对象,只是第一次
 
//使用时生成实例,提高了效率!
if (instance==null)
instance=new Singleton();
return instance;
}
 
}

第二中形式是lazy initialization,也就是说第一次调用时初始Singleton,以后就不用再生成了。

此类的设计确保只创建一个 Singleton 对象。构造函数被声明为 private,getInstance() 方法只创建一个对象。这个实现适合于单线程程序。然而,当引入多线程时,就必须通过同步来保护 getInstance() 方法。如果不保护 getInstance() 方法,则可能返回 Singleton 对象的两个不同的实例。

3. 单例模式多线程同步解决方法

为了解决上述问题,我们需要对 instance 进行第二次检查。这就是“双重检查锁定”名称的由来。

双重检查锁定示例

public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) {  //1
if (instance == null)          //2
instance = new Singleton();  //3
}
}
return instance;
}

双重检查锁定背后的理论是:在 //2 处的第二次检查使创建两个不同的 Singleton 对象成为不可能。假设有下列事件序列:
1. 线程 1 进入 getInstance() 方法。
2. 由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。
3. 线程 1 被线程 2 预占。
4. 线程 2 进入 getInstance() 方法。
5. 由于 instance 仍旧为 null,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 在 //1 处阻塞。
6. 线程 2 被线程 1 预占。
7. 线程 1 执行,由于在 //2 处实例仍旧为 null,线程 1 还创建一个 Singleton 对象并将其引用赋值给 instance。
8. 线程 1 退出 synchronized 块并从 getInstance() 方法返回实例。
9. 线程 1 被线程 2 预占。
10. 线程 2 获取 //1 处的锁并检查 instance 是否为 null。
11. 由于 instance 是非 null 的,并没有创建第二个 Singleton 对象,由线程 1 创建的对象被返回。
双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。
双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。
具体参考一篇来自developerWorks上的文章

4. 单例类更新时的同步做法(Additional)

在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。

例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件 中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。

这种方式极大地简化了在复杂环境下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。

单例对象的属性更新的问题,需要自行编写同步代码,设置一个读计数器,每次读取配置信息前,将计数器加1,读完后将计数器减1。只有在读计数器为0时,才能更新数据,同时要阻塞所有读属性的调用。代码如下:

public class GlobalConfig {
private static GlobalConfig instance;
private Vector properties = null;
private boolean isUpdating = false;
private int readCount = 0;
private GlobalConfig() {
//Load configuration information from DB or file
//Set values for properties
}
private static synchronized void syncInit() {
if (instance == null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance==null) {
syncInit();
}
return instance;
}
public synchronized void update(String p_data) {
syncUpdateIn();
//Update properties
}
private synchronized void syncUpdateIn() {
while (readCount > 0) {
try {
wait();
} catch (Exception e) {
}
}
}
private synchronized void syncReadIn() {
readCount++;
}
private synchronized void syncReadOut() {
readCount--;
notifyAll();
}
public Vector getProperties() {
syncReadIn();
//Process data
syncReadOut();
return properties;
}
}

Leave a Comment.