单例模式是设计模式中使用最为普遍的模式之一。它是一只对象创建模式,用于产生一个对象的具体实现,它可以确保系统中一个类只产生一个实例。在Java语言中,这样的行为能够带来两大好处:
1)对于频繁使用的对象,可以省略创建对象所花费的时间。这对于那些重量级对象而言,是非常可观的一笔系统开销;
2)由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
因此对于系统的关键组件和被频繁使用的对象,使用单例模式便可以有效地改善系统的性能。
单例模式的参与者非常简单,只有单例类(提供单例的工厂,返回单例)和使用者(获取并使用单例)两个:
1)SimpleSingleton
package com.panny; public class SimpleSingleton { /* * 这种单例的实现方式非常简单,而且十分可靠。 * 它唯一的不足是无法对instance实例做延迟加载。假如单例的创建过程很慢,而由于instance成员变量 * 是static定义的,因此在JVM加载单例时,单例对象就会被立即建立,如果此时,这个单例类在系统中还扮演着 * 其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。 * */ private static SimpleSingleton inistance = new SimpleSingleton(); private SimpleSingleton() { System.out.println("Creating SimpleSingleton..."); // 创建单例的过程可能会比较慢 } public static SimpleSingleton getInstence() { return inistance; } public static void greeting() { System.out.println("Hello, SimpleSingleton..."); } }
2) lazySingleton
package com.panny; public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { System.out.println("Creating LazySingleton..."); } /*再次需要特别注意:getInstance()方法必须是同步的。 否则在多线程环境下,当线程1正新建单例时,完成赋值操作前, 线程2可能判断instance为null,故线程2也将新建单例, 而导致多个实例被创建, 故同步关键字是必须的。*/ public static synchronized LazySingleton getInstance() { if(null == instance) { instance = new LazySingleton(); } return instance; } public static void greeting() { System.out.println("Hello, LazySingleton..."); } }
3) StaticSingleton
package com.panny; public class StaticSingleton { /*在这个实现中,单例模式使用内部类来维护单例的实例, * 当StaticSingleton被加载时,其内部类不会被初始化, * 故可以确保当StaticSingleton被载入JVM时,不会初始化单例类, * 而当getInstence()方法被调用时,才会加载内部类(StaticSingletonHolder), * 从而初始化instance。同时,由于实例的建立是在类加载时完成,故天生对多线程友好, * getInstence()方法也不需要使用synchronized关键字。*/ private StaticSingleton() { System.out.println("Creating StaticSingleton..."); } private static class StaticSingletonHolder { private static StaticSingleton instance = new StaticSingleton(); } public static StaticSingleton getInstence() { return StaticSingletonHolder.instance; } }
4) SerSingleton
package com.panny; public class SerSingleton implements java.io.Serializable { private static SerSingleton instance = new SerSingleton(); private SerSingleton() { System.out.println("Creating SerSingleton..."); } public static SerSingleton getInstance() { return instance; } private Object readResolve() { return instance; } }
5) MyTest
package com.panny; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class MyTest { public static void main(String[] args) throws IOException, ClassNotFoundException { // SimpleSingleton.greeting(); /* * Creating SimpleSingleton... * Hello, SimpleSingleton... * * 可以看到,虽然此时并没有使用单例类,但它还是被创建出来,这也许是开发人员不愿意见到的。 * 为了解决这个问题,并以此提高系统在相关函数调用时的反应速度,就需要引入延迟加载机制:LazySingleton */ // LazySingleton.greeting(); // Hello, LazySingleton... /* * 虽然LazySingleton实现了延迟加载的功能,但和SimpleSingleton相比, * 它引入了synchronized关键字,因此在多线程环境中,它的耗时要远远大于SimpleSingleton, * 以下测试码说明了这个问题: * */ class MyThread implements Runnable { String name; public MyThread(String name) { this.name = name; } @Override public void run() { long beginTime = System.currentTimeMillis(); for(int i = 0; i < 1000000; i++) { // SimpleSingleton.getInstence(); /*T1 spend: 7 T3 spend: 12 T4 spend: 12 T5 spend: 11 T2 spend: 12*/ LazySingleton.getInstance(); /*T1 spend: 259 T3 spend: 266 T5 spend: 269 T4 spend: 272 T2 spend: 273*/ // StaticSingleton.getInstence(); /*T5 spend: 6 T1 spend: 12 T4 spend: 11 T2 spend: 7 T3 spend: 11*/ } System.out.println(this.name + " spend: " + (System.currentTimeMillis() - beginTime)); } } Thread t1 = new Thread(new MyThread("T1")); t1.start(); Thread t2 = new Thread(new MyThread("T2")); t2.start(); Thread t3 = new Thread(new MyThread("T3")); t3.start(); Thread t4 = new Thread(new MyThread("T4")); t4.start(); Thread t5 = new Thread(new MyThread("T5")); t5.start(); /*开启5个线程同时运行 * 两种类型性能至少相差2个数量级, * 为了使用延迟加载引入的synchronized关键字反而降低了系统性能,是不是有些得不偿失呢? * 为了解决这个问题,需要对其进行改进:StaticSingleton * 查看测试码,实现了性能改进。*/ /*通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。 * 但仍然有例外的情况可能导致系统生成多个实例,比如:1)在代码中通过反射机制,强行调用 * 单例类的私有构造函数,生成多个单例;2)通过java.io.Serializable序列化单例类生成多个单例。 * 第一种方式太极端,一般不会遇见,以下尝试解决第二种方式产生的问题:SerSingleton */ SerSingleton s0 = SerSingleton.getInstance(); FileOutputStream fos = new FileOutputStream("SerSingleton.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s0); oos.flush(); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SerSingleton.txt")); SerSingleton s1 = (SerSingleton) ois.readObject(); ois.close(); System.out.println(s0 == s1); /*当去掉SerSingleton中的readResolve()方法时,s0 == s1为false, * 说明s0和s1分别引用了不同的实例,即在反序列化后生成了多个实例对象。 * 而当恢复SerSingleton中的readResolve()方法时,s0 == s1为true, * 说明即便经过了反序列化,仍然保持了单例的特征。 * 实际上,在实现了私有的readResolve()方法后,readObject()已经形同虚设, * 它直接使用readResolve()替换了原本的返回值,从而形式上构造了单例。 * 注意:序列化和反序列化可能会破坏单例,一般来说,对单例进行序列化和反序列化 * 的场景不多,但如果存在,就要多加注意。*/ } }
鸣谢:感谢葛一鸣老师的《Java程序性能与优化》,从中我学习了许多珍贵的编程技巧与经验。
本文出自 “好寂寞先生” 博客,请务必保留此出处http://pannyhjm.blog.51cto.com/4473736/1384382