一、单例模式:
-
核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
- 例如:Windows的任务管理器(Task Manager),回收站(Recycle Bin),项目中读取配置文件的类,网站计数器,数据库连接池的设计,操作系统的文件系统,Spring中每个Bean默认是单例的,servlet中每个servlet也是单例,spring MVC/Struts中的控制器对象
-
优点:
- 只有一个实例,系统开销小。可以在系统设置全局的访问点,优化对共享资源的访问
-
常见的实现方式:
-
主要:
-
饿汉式–线程安全,调用效率高。但不能延时加载。
package com.wzg.singleton; /** * 饿汉式单例模式 * @author wang * 问题: * 如果只是加载本类而不是要调用getInstance(),甚至永远也没有调用,,会造成资源浪费。 */ public class SingletonDemo1 { // 类初始化时立即加载这个对象(没有延时加载的优势)。由于加载类时天然的线程安全的。 private static SingletonDemo1 instance = new SingletonDemo1(); // 私有化构造器 private SingletonDemo1() {} // 方法没有同步调用效率高 public static SingletonDemo1 getInstance() { return instance; } }
-
懒汉式–线程安全,调用效率不高。但可以延时加载。
package com.wzg.singleton; /** * 懒汉式单例模式--真正用的时候才加载 * @author wang * 问题: * 资源利用率高了。但是每次调用getInstance()都要同步,并发效率低。 */ public class SingletonDemo2 { // 类初始化时,不能初始化这个对象(延时加载,真正用的时候才创建) private static SingletonDemo2 instance; // 私有化构造器 private SingletonDemo2() {} // 方法同步,调用效率低 public static synchronized SingletonDemo2 getInstance() { if (instance == null) { instance = new SingletonDemo2(); } return instance; } }
-
-
其他方法:
-
双重检测锁式–由于JVM底层内部模型原因,偶尔会出现问题。
package com.wzg.singleton; /** * 双重检测锁式实现 * @author wang * 这个模式将同步内容下放到if内部,提高了执行的效率, * 不必每次获取对象时都进行同步,只有第一次才同步,创建以后就没必要了。 */ public class SingletonDemo3 { private static SingletonDemo3 instance = null; // 私有化构造器 private SingletonDemo3() {} // 将同步内容下放到if内部 public static SingletonDemo3 getInstance() { if (instance == null) { SingletonDemo3 sc; synchronized (SingletonDemo3.class) { sc = instance; if(sc == null) { synchronized (SingletonDemo3.class) { if(sc == null) { sc = new SingletonDemo3(); } } instance = sc; } } } return instance; } }
-
静态内部类式–线程安全,调用效率高。可以延时加载。也是一种懒加载方式。
package com.wzg.singleton; /** * 静态内部类实现 * @author wang * 要点: * 外部类没有static属性,则不会像饿汉式那样立即加载对象。 * 只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的。 * instance是static final类型,保证了内存中只有一个这样的实例存在,而且只能被赋值一次, * 从而保证了线程的安全性。 * 兼备了并发高效调用和延迟加载优势 */ public class SingletonDemo4 { private static class SingletonClassInstance{ private static final SingletonDemo4 instance = new SingletonDemo4(); } // 私有化构造器 private SingletonDemo4() {} // public static SingletonDemo4 getInstance() { return SingletonClassInstance.instance; } }
-
枚举单例–线程安全,调用效率高,不能延时加载。
package com.wzg.singleton; /** * 使用枚举实现单例模式 * @author wang * 实现简单。枚举本身就是单例模式。由JVM从根本上提供保障,避免通过反射和反序列化的漏洞。 * 缺点:无延时加载 */ public enum SingletonDemo5 { // 这个枚举元素本身就是单例对象 INSTANCE; // 添加实际需要的操作 public void singletonOperation() { } }
-
-
-
如何选用
- 单例对象 占用资源少,不需要延时加载:枚举式 好于 饿汉式
- 单例对象 占用资源大, 需要延时加载:静态内部类式 好于 懒汉式
-
问题:
-
反射可以破解上面集中实现方式(不包含枚举)(可以再构造方法中手动抛出异常控制)
例如针对上面的懒汉式单例模式(SingletonDemo2类)的破解:
package com.wzg.singleton; import java.lang.reflect.Constructor; /** * 测试反射破解 * @author wang * */ public class Client { public static void main(String[] args) throws Exception{ SingletonDemo2 s1 = SingletonDemo2.getInstance(); SingletonDemo2 s2 = SingletonDemo2.getInstance(); // 下面两行输出显示为同一个对象 System.out.println(s1); System.out.println(s2); // 通过反射直接调用私有构造器 Class<SingletonDemo2> clazz = (Class<SingletonDemo2>) Class.forName("com.wzg.singleton.SingletonDemo2"); Constructor<SingletonDemo2> c = clazz.getDeclaredConstructor(null); // 跳过权限检查,访问私有属性 c.setAccessible(true); SingletonDemo2 s3 = c.newInstance(); SingletonDemo2 s4 = c.newInstance(); // 下面两行输出显示两个不同的新对象 System.out.println(s3); System.out.println(s4); } }
防止反射漏洞的方法:在私有构造器里设置:
public class SingletonDemo2 { ...... // 私有化构造器 private SingletonDemo6() { // 防止反射漏洞 if(instance!=null) { throw new RuntimeException(); } } ...... }
-
反序列化可以破解上面几种实现方式(不包含枚举)
例如针对上面的懒汉式单例模式(SingletonDemo2类)的破解:
对SingletonDemo2类修改,添加接口
public class SingletonDemo2 implements Serializable{ ...... }
package com.wzg.singleton; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; /** * 测试反序列化破解 * @author wang * */ public class Client2 { public static void main(String[] args) throws Exception{ SingletonDemo2 s1 = SingletonDemo2.getInstance(); SingletonDemo2 s2 = SingletonDemo2.getInstance(); // 下面两行输出显示为同一个对象 System.out.println(s1); System.out.println(s2); //通过反序列化的方式构造多个对象 FileOutputStream fos = new FileOutputStream("result.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s1); oos.close(); fos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("result.txt")); SingletonDemo2 s3 = (SingletonDemo2) ois.readObject(); // 下面输出显示一个新对象 System.out.println(s3); } }
防止反序列化的方法,在原函数SingletonDemo2中添加一个方法:
// 反序列化时,如果定义了readResolve()方法则直接返回此方法指定的对象,而不需要单独再创建新对象。 // 实际上是一种回调 private Object readResolve() throws ObjectStreamException { return instance; }
这时再执行破解类Client2时,3个对象相同。
-
-
单例模式各种实现方式的效率测试:
利用多线程的测试环境:
- 同步辅助类CountDownLatch,在完成一组或其他线程中执行的操作之前,它允许一个或多个线程一直等待
- countDown() 当前线程调用此方法,则计数减一(建议放在finally里执行)
- await() 调用此方法会一直阻塞当前线程,直到计时器的值为0
package com.wzg.singleton; import java.util.concurrent.CountDownLatch; /** * 单例模式五种实现方式的效率测试 * @author wang * */ public class Client3 { public static void main(String[] args) throws Exception{ long start = System.currentTimeMillis(); // 开启的线程数 int threadNum = 10; final CountDownLatch countDownLatch = new CountDownLatch(threadNum); for(int k=0;k<10;k++) { new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub for(int i=0;i<10000;i++) { Object o = SingletonDemo1.getInstance(); // Object o = SingletonDemo2.getInstance(); // Object o = SingletonDemo3.getInstance(); // Object o = SingletonDemo4.getInstance(); // Object o = SingletonDemo5.INSTANCE; } countDownLatch.countDown(); } }).start(); } countDownLatch.await();// main线程阻塞,直到计数器变为0,才会向下执行。 long end = System.currentTimeMillis(); System.out.println("总耗时"+(end-start)+"ms"); } }
我的测试结果:
实现方式 耗时 饿汉式 7ms 懒汉式 15ms 双重检测锁式 11ms 静态内部类式 9ms 枚举式 5ms - 同步辅助类CountDownLatch,在完成一组或其他线程中执行的操作之前,它允许一个或多个线程一直等待