架构设计之设计模式 (四) Java中多种方式实现单例模式
- 简介
- “单例”即单一实例从名字上望文生义即可知道该类是做什么的,可见设计模式的名字也是很重要的,让人通过名字就能知道模式的用途,通用性强我们再命名自己的模式、函数、过程等时候也要遵循这一命名原则,这也成为了编程中一个不成文的规定。
- GOF是这样定义的:确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
- 特点
- 有状态
- 一个单例对象可以是有状态的(Stateful),一个有状态的单例对象通常也是可变对象(mutable)。
- 一个有状态的单例对象可以作为状态库(repositary),比如一个单例对象拥有Int类型的属性,那么它可以提供唯一序列号,供系统使用。
- 有状态的单例对象才有可能出现进程同步问题,这有点像函数的传值和传引用,传值不会改变传入参数的值当然就没有影响,只是当一个工具函数来使用。
- 无状态
- 另一方面单例也可以是没有状态(stateless),仅当作工具性函数来使用,既然当作工具函数来使用当然也就没有必要再创建多个实例,有一个就够用了,使用单例创建对象很合适。
- 不完全
- 构造函数是公开的,正常情况下设置构造函数不公开是为了防止外部类对其进行实例化,如果构造函数公开外部类也不一定会调用,说明它也是一个单例,我们就称这样的单例为不完全的单例。
- JVM
- 一个单例只在一个JVM中运行,当两个类加载器加载同一个类时可能出现两个单例实例。
- 有状态
- 举例
- 在计算机里面不管是硬件管理还是软件都有很多用到单例的地方,比如硬件打印机、端口、传真口等在某一个特定时刻只允许一个访问者进行通信,否则会出现问题
- 软件方面回收站、软件系统配置文件的读取都是利用单例实现,因为一个系统中只有一个回收站,回收站自己提供这个实例,
- 饿汉与懒汉
- 饿汉式
package com.singleton; /** * Only once instance of the class may be created during the * execution of any given program. Instances of this class should * be aquired through the getInstance() method. Notice that there * are no public constructors for this class. */ public class HungrySingleton { /** * @label Creates */ private static HungrySingleton m_instance = new HungrySingleton(); private HungrySingleton() { } public static synchronized HungrySingleton getInstance() { return m_instance; } }
- 优缺点
- 饿汉单例类在类一加载时就会创建单例对象,并一直存在于内存当中占用了很多存储空间,浪费内存,优点是没有枷锁,执行效率会高一些。
- 懒汉式
package com.singleton; /** * Only once instance of the class may be created during the * execution of any given program. Instances of this class should * be aquired through the getInstance() method. Notice that there * are no public constructors for this class. */ public class lazySingleton { /** * @label Creates */ private static lazySingleton m_instance = null; private lazySingleton() { } public static synchronized lazySingleton getInstance() { if (m_instance == null) { m_instance = new lazySingleton(); } return m_instance; } }
- 优缺点
- 当对象第一次被引用时创建,比饿汉实例化晚一些因此节省资源,但是懒汉会引起线程不安全,需要枷锁,降低执行效率,。
- 那么有没有什么办法克服两个模式的缺点呢?
- 在《Java编程思想》中详细介绍了内部类的使用,如果看一看内部类就会知道如何解决这一个问题,内部类是在外部类被调用时才加载,根据这个特点我们可以把单例中一开始就需要加载的代码移动到内部类里面,这样它的加载顺序就会改变,当然,也就不会再外部类第一次加载时就就行实例化。把饿汉中的初始化代码写在内部类里面,如下:
package com.singleton; /** * 采用内部类的方式实现单例 * @author LLS * */ public class RegisterSingleton { /* * 私有构造方法,防止被外类实例化 */ private RegisterSingleton(){} /* * 公开方法 */ public static RegisterSingleton getInstance() { return Holder.instance; } /* * 私有内部类 */ private static class Holder { private static final RegisterSingleton instance=new RegisterSingleton(); } }
- 从网上查了查原来这个模式叫做登记式单例,登记式单例的初衷是为了克服单例不能继承的缺点,但这样写似乎还是不能够继承,想了想这也算是继承吧。
- 因为引入了内部类,内部类可以实现继承类的所有功能,我们也可以调用内部类,这也相当于继承类的实现。
- 登记式单例(采用继承)
- 这种登记类通过在父类中Register来实现,即把实例化好的对象放入Map(Key,Value),键值对分别代表类名、类对应的对象。
- 在子类中从Map中取出实例,不过,没感觉到这种继承有多大好处,如果想继承父类构造函数最少也是protected级别,那么子类呢,得是public或者protected,这样一来子类就可以通过外部new来实例化,没有多大意义了。
- 饿汉式
- Java中应用
- 在我们目前做的系统中饿汉式单利已经能解决问题,建议用饿汉式单例模式。或者使用内部类的单例来管理对象创建。
- J2EE中单例模式说简单即简单说难即难,如果涉及到多个JVM以及加载器问题,单例会比较复杂一些,只是目前还没有碰到过这种情况。