职责型设计模式 之 单例模式

一、基础

单例模式 (Singleton pattern ):确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。(Ensure a class has only one instance,and provide a global point of access to it.)

在Java中对象都是通过new关键字来创建的(当然也可以通过对象复制、反射等),我们如何才能控制对象的创建呢?最简单的方式是我们将构造函数设置为私有(private)访问权限,这样就禁止外部创建对象了。

public class Singleton {
// 限制产生多个对象
private Singleton() {}

private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

}

上面的这种方式有时被称为饿汉模式,也就是提前将实例创建好,无论是否需要调用。Singleton类称为单例类,通过使用private的构造函数确保在一个应用中只产生一个实例,并且是自行实例化的,其通用类图如下:

这种方式基于加载机制从而避免了多线程的同步问题,INSTANCE在类装载时就实例化了。另外我们可以使用静态代码块,以及构造块来实现,达到的效果是一样的,不过有些繁琐而已。

简单叙述:static{…}是静态块,而只有{…}的是叫做构造块。

  • 静态块在一个程序里面只执行一次;而构造块是,只要建立一个对象,构造代码块都会执行一次。
  • 静态块优先于主方法的执行,静态块优先于构造快,然后是构造方法的执行,而且只执行一次!

与饿汉模式相对应的是懒汉模式,顾名思义,也就是在生成单例类的实例时延迟加载,在第一次使用时(调用)才进行创建实例。

最简单的实现方式如下:

public class SingleTon {
    private static SingleTon INSTANCE = null;
    private SingleTon(){}
    public static SingleTon getInstance(){
        //返回实例的时候检查是否创建
        if(null==INSTANCE){
            INSTANCE=new SingleTon();
        }
        return INSTANCE;
    }
}

这种方式可以实现我们的需求,但是在高并发的情况下会出问题,因为其存在线程问题:

针对上面的缺陷,我们进行如下优化:

public class SingleTon {
private static SingleTon INSTANCE = null;
    private SingleTon(){}
    public static synchronized SingleTon getInstance(){
        //返回实例的时候检查是否创建
        if(null==INSTANCE){
            INSTANCE=new SingleTon();
        }
        return INSTANCE;
    }
}

这种写法能够在多线程中很好的工作,但是,遗憾的是,效率很低,99%情况下不需要同步。

升级版-双重检查锁定(1.5之后的新特性)

public class SingleTon {
    private volatile static SingleTon singleton;

    private SingleTon() {    }

    public static SingleTon getSingleton() {
        if (null == singleton) {
            synchronized (SingleTon.class) {
                if (null == singleton) {
                    singleton = new SingleTon();
                }
            }
        }
        return singleton;
    }
}

懒汉与饿汉区别:

  1. 饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变。懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的。
  2. 从实现方式来讲他们最大的区别就是懒汉式是延时加载,他是在需要的时候才创建对象,而饿汉式在虚拟机启动的时候就会创建,饿汉式无需关注多线程问题、写法简单明了、能用则用。但是它是加载类时创建实例、所以如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用一块创建。

二、单例模式的应用

优点

  1. 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  2. 由于单例模式只申城一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其它依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在JavaEE中采用单例模式时需要注意JVM垃圾回收机制)
  3. 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
  4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

缺点

  1. 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能提供接口呢?因为接口对单例模式没有任何意义的,它要求“自行实例化”,并且提供单一示例、接口或抽象类是不可能被实例化的。当然,在特殊情况下单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
  2. 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
  3. 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,并不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

单例模式的使用场景

  1. 要求生成唯一序列号的环境
  2. 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的
  3. 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源
  4. 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)

注意事项

  1. 如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
  2. 如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
  3. 在高并发情况下,一定要注意单例模式的线程同步问题,特别是饿汉模式。
  4. 对象不知情况。在Java中,对象默认是不可以被复制的,若实现了Cloneable接口,并且实现了clone方法,则可以直接通过对象复制方式创建一个对象,对象复制是不用调用类的构造函数,因此即使是私有的构造函数,对象仍然可以被复制。这种情况很少见,但是还是提出来比较好,解决该问题的最好方法是单例类不实现Cloneable接口。
  5. 注意JVM的垃圾回收机制,如果我们的一个单例对象在内存中长久不使用,JVM就认为这个对象是一个垃圾,在CPU资源空闲的情况下该对象会被清理掉,下次再调用时就需要重新产生一个队形。如果我们在应用中使用单例类作为有状态值(如计数器)的管理,则会出现恢复原状的情况,应用就会出现故障。

三、拓展

静态内部类实现方式

这里了解另一种特殊的懒汉模式:静态内部类

public class SingleTon {
    private static class SingletonHolder {
        private static SingleTon INSTANCE = new SingleTon();
    }
    private SingleTon() {}
    public static synchronized SingleTon getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

看起来没有太大区别,但是在加载方式上的细微区别。前面所提及的几种方式只要Singleton类被装载了,那么INSTANCE就会被实例化,而静态内部类的方式是Singleton类被装载了,INSTANCE也不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化INSTANCE

这种方式同样利用了classloder的机制来保证初始化INSTANCE时只有一个线程。

想象一下,如果实例化INSTANCE很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化INSTANCE显然是不合适的。这个时候,静态内部类的方式就显得很合理了。

序列化

我们都知道在实现对象的序列化的时候,我们只需要实现 Serializable接口即可,这样我们就可以把对象往内存里写然后再从内存里读出而”组装”成一个跟原来一模一样的对象了。

不过当序列化遇到单例时,这里边就有了个问题: 从内存中读出而组装的对象破坏了单例的规则。单例是要求一个JVM中只有一个类对象的,而现在通过反序列化,一个新的对象克隆了出来。示例:

public final class MySingleton implements Serializable {
    private MySingleton() { }
    private static final MySingleton INSTANCE = new MySingleton();
    public static MySingleton getInstance() { return INSTANCE; }
}

当把 MySingleton对象(通过getInstance方法获得的那个单例对象)序列化后再从内存中读出时, 就有一个全新的但跟原来一样的MySingleton对象存在了。

那怎么来维护单例模式呢?这就要用到readResolve方法了。 如下所示:

public final class MySingleton implements Serializable{
    private MySingleton() { }
    private static final MySingleton INSTANCE = new MySingleton();
    public static MySingleton getInstance() { return INSTANCE; }
    // 使用readResolve方法来保护保护单例属性
    private Object readResolve() throws ObjectStreamException {
        // Return the one true MySingleton and let the grabage collector
        // take care of MySingleton impersonator
        return INSTANCE;
    }
}

这样当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了,单例规则也就得到了保证。

恶意破坏

上面所提及的方式都可以在正常情况下正常使用,但是如果恶意的去利用反射机制完全可以将其破坏掉,从而不再是单例。

例如享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制来调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

下面我们实地操作一下,来破坏单例模式:

public class SingleTon {
    public final static SingleTon INSTANCE = new SingleTon();
    private SingleTon() {}
}
public class Ceshi_Singleton {
    public static void main(String[] args) {
        SingleTon s1 = SingleTon.INSTANCE;
        SingleTon s2 = SingleTon.INSTANCE;
        System.out.println("正常情况下单例模式执行情况:" + (s1 == s2));
        SingleTon s3 = SingleTon.INSTANCE;
        SingleTon s4 = destroySingleton();
        System.out.println("非正常情况下单例模式执行情况:" + (s3 == s4));
    }

    // 破坏单例模式的方法
    private static SingleTon destroySingleton() {
        SingleTon returnSingleton = null;
        try {
            Class cls = SingleTon.class;
            Constructor constructor = cls.getDeclaredConstructor(new Class[] {});
            constructor.setAccessible(true);
            returnSingleton = constructor.newInstance(new Object[] {});
        } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException        | IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
        }
        return returnSingleton;
    }
}

运行结果:

正常情况下单例模式执行情况:true
非正常情况下单例模式执行情况:false

对于这个结果很显然证明了我们成功的破坏了单例模式的唯一性,对于单例模式,是否很诧异呢?

JDK1.5 为单例模式带来的第三种实现方式(最佳实现)

JDK1.5 引入了枚举类型,借用这一新特性,我们可以用它来实现单例模式:

public enum SingleTon {
    INSTANCE;
    public void a(){...};
    //...
}

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅更加简洁同时能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候。

虽然这种方法还没有广泛使用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

参考资料:

  • 《设计模式之禅 第二版》
  • 《Effective Java 第二版》第三条——用私有构造器或者枚举类型强化Singleton属性
  • readResolve()方法与序列化
  • 单例模式的七种写法
 

你可能感兴趣的:(设计模式,设计模式,新特性,Java,JDK1.5)