我所理解的单例模式

单例模式可以说是23种设计模式中最简单的模式之一,但应用场景却很多,所以也是重要的模式之一。下面先给出单例模式的定义,结构图和代码示例。
定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
结构图(来自GoF):
这里写图片描述
代码示例:

//一般形式的单例模式
public class Singleton {
    private static final Singleton singleton = new Singleton();
    private Singleton(){}
    public static final Singleton getInstance() {
        return singleton;
    }
}

确实简单。
表面上看是的,但单例模式却有很多内容可以讨论。比如:

  • 懒汉式还是饿汉式?
  • 是否线程安全?
  • 能否应付反射攻击?
  • 反序列化呢?

而且针对以上问题,可以出现各个版本的单例模式。下面一一讨论。
一、懒汉、饿汉
首先代码示例是饿汉式的,还有一种写法效果是一样的:

//假设为版本1
public class Singleton {
    private static final Singleton singleton;
    static {
        singleton = new Singleton()
    }
    private Singleton(){}
    public static final Singleton getInstance() {
        return singleton;
    }
}

饿汉式的写法因为第一次加载类就已经初始化,而且singleton是final类型的,所以它是线程安全的(除非你的应用实现了多个ClassLoader,用不同的Loader来加载这个类)。缺点是不能延迟加载,占用内存。不过这只能说是特点不能说是缺点。因为虽然对创建开销很大的类不建议提前初始化,但有些类是建议提前实例化的,比如Spring中生命周期为Singleton的bean,默认就是跟随容器初始化,因为这样不需要等到getBean的时候就可以提前发现bean是否有错。解决这个问题通常有两种方法,用静态内部类或者直接改用懒汉式:

//静态内部类实现单例模式,假设为版本2
public class Singleton {
    private Singleton(){}
    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}
//懒汉式,假设为版本3
public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static final Singleton getInstance() {
        if(null == singleton)
            singleton = new Singleton();
        return singleton;
    }
}

版本2已经满足了延迟加载和线程安全的问题,因为只有第一次访问内部类时内部类才会被加载,new Singleton()才会发生,而且因为singleton是在JVM加载内部类时被赋值初始化的,所以不存在线程安全问题。版本3就存在线程安全问题,所以要用锁来保证:

//版本3.1
public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static synchronized final Singleton getInstance() {
        if(null == singleton)
            singleton = new Singleton();
        return singleton;
    }
}
//版本3.2
public class Singleton {
    private static volatile Singleton singleton = null;
    private Singleton(){}
    public static final Singleton getInstance() {
        if(null == singleton) {
            synchronized(Singleton.class) {
                if(null == singleton)
                    singleton = new Singleton();
            }
        }
        return singleton;
    }
}

3.1的版本把锁加到整个方法上,会影响性能。因为当singleton实例化后只读即可,不需要加锁。所以改进为3.2双重校验锁的版本(这个版本的解析已经有很多前辈写了,这里不赘述)。
至此,版本2和3.2已经算是完美了。但是它们却应对不了反射的攻击,还有如果单例类需要序列化和反序列化的情况也有问题。
反射攻击比如版本2的客户端:

public class Client {
    public static void main(String[] args) throws Exception {
        Singleton s1 = Singleton.getInstance();
        Class<?> c = Class.forName("singleton.Singleton");
        Constructor<?> con = c.getDeclaredConstructor();
        con.setAccessible(true);
        Singleton s2 = (Singleton) con.newInstance();
        System.out.println(s1==s2); //输出false,违反单例的规则
    }
}

应对这种情况《Effective Java》上提供的一种思路是修改构造器,令其在第二次实例化的时候抛出异常。但要注意你的修改自身是self-contained的,即不会被反射攻击。
至于反序列化默认会生成一个新的示例,这时在Singleton里添加方法:

  private Object readResolve() throws ObjectStreamException{
    return singleton; //或return SingletonHolder.singleton;
  }

这样当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证。
上面的两种方法分别解决了反射和反序列化的问题,但都比较麻烦,《Effective Java》上建议一种更优雅的解法,枚举类型实现单例模式:

//版本4
public enum EnumSingleton {
    SINGLETON;
    //下面是这个类的业务方法
}

《Thinking in Java》里对枚举类型有提到,一旦enum的定义结束,编译器就不允许我们再使用其构造器来创建任何实例。这就保证了线程安全和反射安全的问题。而且枚举类型可以绝对保证除了所声明的常量之外,不会有别的实例,这是JVM提供的保障。所以前面说的四点只有延迟初始化做不到,其他都可以优雅的完成。所以一般情况下推荐枚举类型实现单例模式。
一开始我还想是不是也存在Cloneable的问题?谁这么无聊在单例类里面专门实现可复制,这不是自找麻烦吗?但还是会存在这种情况,比如我想继承某个类A的同时实现单例,这时类A可能就存在可复制的风险:

public class A implements Cloneable{
    public Object clone() {
        Object a = null;
        try {
            a =  super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return a;
    }
}

class Singleton extends A{
    private Singleton(){}
    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}
//这时客户端是可是拿到两个Singleton的
public class Client {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = (Singleton) s1.clone();
        System.out.println(s1==s2); //false
    }   
}

这时只需要在单例类里不让复制即可:

class Singleton extends A{
    private Singleton(){}
    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
    public Object clone() {
        throw new RuntimeException("double initial");
    }
}

最后还有一点值得讨论一下,就是单例与静态类(这里指的是成员变量与方法都是static 的类)的问题,感觉静态类就可以直接访问了啊,为什么还要专门用所谓的单例模式用一个singleton变量和一个getInstance()方法提供一个实例?就好比java.lang.Math这个类,把构造器私有化,然后把所有方法设成static。确实Math也可以达到“单例”的效果,但单例模式更多的是体现一种思想,是面向对象的。而这里所说的静态类没有对象的概念,更多的是体现c语言面向过程的思想。这应该是它们最大的区别吧。在一些没有域的工具类里面,确实推荐用静态类的方法,这样性能会更好,因为static方法是在编译期绑定的。就像提到的Math类。
以上是学习过程中对单例的理解,有问题的地方请不吝指教。
支持原创,转载请注明出处!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值