自大学课程初识设计模式以来,就越发觉得有必要系统学习一下设计模式。
刚好在实习前准备期间课比较少,抽出一点时间整理一下记一些笔记,复制粘贴比较多。
笔记比较适合学习过设计模式的同学。
Singleton Pattern(单例模式)
学习链接:极客学院Wiki_Java设计模式之创建型模式
另外感谢刘伟博士,学习设计模式可以看刘伟博士的博客,很详尽。
刘伟技术博客
单例模式的适用范围
(1) 系统只需要一个实例对象
这个很好理解,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
(2) 客户调用类的单个实例只允许使用一个公共访问点
除了该公共访问点,不能通过其他途径访问该实例。
应用实例
java的数据库连接池就使用了单例(多例)模式,它不允许过多的用户同时访问该连接池,避免了信息不同步的问题。
单例模式如何实现
角色
Singleton(单例)
在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。
类图
这是最基本的单例模式类图(懒汉式)当时刚刚接触设计模式时很诧异一个类中包含了自己,现在可以明白,客户端得到的实例事实上不是该类的实例。
两种类型的单例模式
说到了懒汉式,当然就会有其他式,一般的单例模式可以解决单线程任何需要一个实例的问题。然而,当出现多线程编程时,就会出现错误,一个线程修改了标志位但是还来不及通知其他线程,于是就出现了实例化不止一个实例的情况。于是引入下面两种单例模式的扩展。
饿汉式
饿汉,顾名思义,已经急切地要实例化对象(即使还没用到)可以看到,在一开始定义单例类的时候就建成了实例,于是有效地避免了多线程需要创建实例时的不一致性。当然饿汉式也有它的不好,就是上面所说的还没有用到就实例化对象,占用了一定的空间和启动速度。
class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
懒汉式
将基本的懒汉式单例类进行修改,同样可以解决线程不一致的问题。
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
看到这里在静态工厂方法前面加了synchronized
就能知道此方法使用了线程同步,解决了问题,但是发现,每次获取时都要访问该方法,但是并不是都需要进行线程同步的判断,仅仅在第一次创建实例的时候需要,如果像上述一样会增加系统消耗和响应时间,所以进行如下改动:
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
此代码看似完美,但依然还有多线程 存在的问题,若两个线程同时通过了if条件的判断,那么还是可以创建两个实例。于是引入双重检查锁定机制:
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //创建单例实例
}
}
}
return instance;
}
}
学习:需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量 instance 之前增加修饰符 volatile,被 volatile 修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在 JDK 1.5 及以上版本中才能正确执行。由于 volatile 关键字会屏蔽 Java 虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
饿汉式单例类与懒汉式单例类比较
饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。
既然都有缺点,那么什么才是更好的解决方案呢?一种更好的解决方案Initialization Demand Holder
我们可以将线程同步机制交给java虚拟机自己来掌控,充分使用它的static关键字的特性,当然java机制是这样的,很多面向对象语言却没有。
//Initialization on Demand Holder
class Singleton {
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
}
单例模式的优缺点
主要优点
(1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
(3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。思考:如何设计可变数目的实例?(这里提供一种方案:在类中定义成员变量数组)
主要缺点
(1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
(3) 现在很多面向对象语言(如 Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
练习
题目
分别使用饿汉式单例、带双重检查锁定机制的懒汉式单例以及 IoDH 技术实现负载均衡器 LoadBalancer。
实现代码
饿汉式:EagerSingleton.java
package com.joy;
public class EagerSingleton {
private static final EagerSingleton eager = new EagerSingleton();
private EagerSingleton(){
System.out.println("Eager:LoadBalancer has ready!");
}
public static EagerSingleton getInstance(){
return eager;
}
public static void main(String[] args) {
System.out.println("Open LoadBalancer:");
EagerSingleton.getInstance();
EagerSingleton.getInstance();
}
}
运行结果
懒汉式:LazySingleton.java
package com.joy;
public class LazySingleton {
private volatile static LazySingleton lazy;
private LazySingleton(){
System.out.println("Lazy:LoadBalancer has ready!");
}
public static LazySingleton getInstance(){
if(lazy==null){
synchronized (LazySingleton.class) {
if(lazy==null){
lazy = new LazySingleton();
}
}
}
return lazy;
}
public static void main(String[] args) {
System.out.println("Open LoadBalancer:");
LazySingleton.getInstance();
LazySingleton.getInstance();
}
}
运行结果
IoDH技术:IoDH.java
package com.joy;
public class IoDH {
private IoDH() {
System.out.println("IoDH:LoadBalancer has ready!");
}
private static class HolderClass {
private final static IoDH iodh = new IoDH();
}
public static IoDH getInstance() {
return HolderClass.iodh;
}
public static void main(String[] args) {
System.out.println("Open LoadBalancer:");
IoDH.getInstance();
IoDH.getInstance();
}
}
运行结果
总结
根据代码可以看出饿汉式是在一开始就实例化。并且在写代码的时候注意关键字。