我所理解的单例模式
2016-04-17 19:17
225 查看
单例模式可以说是23种设计模式中最简单的模式之一,但应用场景却很多,所以也是重要的模式之一。下面先给出单例模式的定义,结构图和代码示例。
定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
结构图(来自GoF):
代码示例:
确实简单。
表面上看是的,但单例模式却有很多内容可以讨论。比如:
懒汉式还是饿汉式?
是否线程安全?
能否应付反射攻击?
反序列化呢?
而且针对以上问题,可以出现各个版本的单例模式。下面一一讨论。
一、懒汉、饿汉
首先代码示例是饿汉式的,还有一种写法效果是一样的:
饿汉式的写法因为第一次加载类就已经初始化,而且singleton是final类型的,所以它是线程安全的(除非你的应用实现了多个ClassLoader,用不同的Loader来加载这个类)。缺点是不能延迟加载,占用内存。不过这只能说是特点不能说是缺点。因为虽然对创建开销很大的类不建议提前初始化,但有些类是建议提前实例化的,比如Spring中生命周期为Singleton的bean,默认就是跟随容器初始化,因为这样不需要等到getBean的时候就可以提前发现bean是否有错。解决这个问题通常有两种方法,用静态内部类或者直接改用懒汉式:
版本2已经满足了延迟加载和线程安全的问题,因为只有第一次访问内部类时内部类才会被加载,new Singleton()才会发生,而且因为singleton是在JVM加载内部类时被赋值初始化的,所以不存在线程安全问题。版本3就存在线程安全问题,所以要用锁来保证:
3.1的版本把锁加到整个方法上,会影响性能。因为当singleton实例化后只读即可,不需要加锁。所以改进为3.2双重校验锁的版本(这个版本的解析已经有很多前辈写了,这里不赘述)。
至此,版本2和3.2已经算是完美了。但是它们却应对不了反射的攻击,还有如果单例类需要序列化和反序列化的情况也有问题。
反射攻击比如版本2的客户端:
应对这种情况《Effective Java》上提供的一种思路是修改构造器,令其在第二次实例化的时候抛出异常。但要注意你的修改自身是self-contained的,即不会被反射攻击。
至于反序列化默认会生成一个新的示例,这时在Singleton里添加方法:
这样当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证。
上面的两种方法分别解决了反射和反序列化的问题,但都比较麻烦,《Effective Java》上建议一种更优雅的解法,枚举类型实现单例模式:
《Thinking in Java》里对枚举类型有提到,一旦enum的定义结束,编译器就不允许我们再使用其构造器来创建任何实例。这就保证了线程安全和反射安全的问题。而且枚举类型可以绝对保证除了所声明的常量之外,不会有别的实例,这是JVM提供的保障。所以前面说的四点只有延迟初始化做不到,其他都可以优雅的完成。所以一般情况下推荐枚举类型实现单例模式。
一开始我还想是不是也存在Cloneable的问题?谁这么无聊在单例类里面专门实现可复制,这不是自找麻烦吗?但还是会存在这种情况,比如我想继承某个类A的同时实现单例,这时类A可能就存在可复制的风险:
这时只需要在单例类里不让复制即可:
最后还有一点值得讨论一下,就是单例与静态类(这里指的是成员变量与方法都是static 的类)的问题,感觉静态类就可以直接访问了啊,为什么还要专门用所谓的单例模式用一个singleton变量和一个getInstance()方法提供一个实例?就好比java.lang.Math这个类,把构造器私有化,然后把所有方法设成static。确实Math也可以达到“单例”的效果,但单例模式更多的是体现一种思想,是面向对象的。而这里所说的静态类没有对象的概念,更多的是体现c语言面向过程的思想。这应该是它们最大的区别吧。在一些没有域的工具类里面,确实推荐用静态类的方法,这样性能会更好,因为static方法是在编译期绑定的。就像提到的Math类。
以上是学习过程中对单例的理解,有问题的地方请不吝指教。
支持原创,转载请注明出处!
定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
结构图(来自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类。
以上是学习过程中对单例的理解,有问题的地方请不吝指教。
支持原创,转载请注明出处!
相关文章推荐
- PropertyChangeListener简单理解
- 什么是设计模式
- 设计模式之创建型模式 - 特别的变量问题
- 七、设计模式——装饰模式
- 设计模式总结
- 设计模式之创建型模式
- 浅谈设计模式的学习
- Ruby设计模式编程之适配器模式实战攻略
- 实例讲解Ruby使用设计模式中的装饰器模式的方法
- 设计模式中的模板方法模式在Ruby中的应用实例两则
- Ruby设计模式编程中对外观模式的应用实例分析
- 实例解析Ruby设计模式编程中Strategy策略模式的使用
- Ruby中使用设计模式中的简单工厂模式和工厂方法模式
- Ruby使用设计模式中的代理模式与装饰模式的代码实例
- 详解组合模式的结构及其在Ruby设计模式编程中的运用
- C#编程中使用设计模式中的原型模式的实例讲解
- 使用设计模式中的工厂方法模式进行C#编程的示例讲解
- 实例解析C#设计模式编程中简单工厂模式的使用
- 详解C#设计模式编程中生成器模式的使用
- 深入解析C#设计模式编程中对建造者模式的运用