设计模式学习总结-单例模式
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; } } |