设计模式——单例模式
HeadFirst中对单例模式的定义:单例模式确保一个类只有一个实例,并只提供一个全局访问点。
单例模式的应用:任务管理器、回收站、项目的配置文件、日志文件等等
单例模式的特点:单例模式只有一个实例,减少了系统的开销,当一个对象的产生需要很多资源时,就可以通过在启动时来创建一个实例永久的驻存。
可以在全局设置访问点,优化资源的访问。
单例模式的常见实现方式:
饿汉式:线程安全,效率高,不能延时加载。
懒汉式:线程安全,效率低,可以延时加载。
双重检测锁:线程安全,效率高,可以延时加载,但是只有在java1.5之后才支持,并且由于JVM底层模型的原因容易出问题。
静态内部类:线程安全,效率高,可以延时加载。
枚举:线程安全,效率高,不能延时加载,可以天然的防止反射和反序列化漏洞。
一.饿汉式
1.静态初始化是天然的线程安全的。
2.效率比较高
3.一开始就创建,没有延时加载,如果一直没有用到这个单例对象的话就浪费了资源。
package com.wxisme.singleton; /** * 饿汉式单例模式 * @author wxisme * */ public class SingletonOne { //静态初始化时就new出单例对象 private static final SingletonOne instance = new SingletonOne(); //私有构造器 private SingletonOne() { } //返回单例对象 public static SingletonOne getInstance() { return instance; } }
二.懒汉式
1.线程安全,但是效率较低。
2.延时加载,不会造成资源的浪费。
package com.wxisme.singleton; import java.io.Serializable; /** * 懒汉式实现单例模式 * @author wxisme * */ public class SingletonTow implements Serializable { //在获取的时候创建此处不能加final private static SingletonTow instance; private SingletonTow() { //防止反射破解 /* if(instance != null) { throw new RuntimeException(); } */ } //必须要手动加锁,达到线程安全的目的。 public synchronized static SingletonTow getInstance() { if(instance == null) { instance = new SingletonTow(); } return instance; } //防止反序列化破解 /* private Object readResolve() { return instance; } */ //防止反射破解 /* private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if(classLoader == null) classLoader = SingletonTow.class.getClassLoader(); return (classLoader.loadClass(classname)); } */ }
三.双重检验锁
1.对懒汉式进行改进,只需要在第一次调用getInstance()方法的时候枷锁,提高了效率。
2.由于JVM底层模型问题,这种方式偶尔会出问题。在JDK1.5之后才能支持。
volatile 用来确保线程安全。
package com.wxisme.singleton; /** * 双重检验锁实现单例模式 * @author wxisme * */ public class SingletonThree { //volatile指令关键字确保实例是线程安全的 private volatile static SingletonThree instance; private SingletonThree() { } //双重检验锁实现 线程安全&延时加载 public static SingletonThree getInstance() { if(instance == null) { synchronized (SingletonThree.class) { if(instance == null) { instance = new SingletonThree(); } } } return instance; } }
四.静态内部类
1.静态初始化,天然的线程安全,效率高。
2.实现延时加载
3.但是能用反射机制和序列化破解
package com.wxisme.singleton; /** * 静态内部类实现单例模式 * @author wxisme * */ public class SingletonFour { //静态内部类 private static class Inner { private static final SingletonFour instance = new SingletonFour(); } private SingletonFour() {} //只有显示调用getInstance方法时才会加载内部类 public static final SingletonFour getInstance() { return Inner.instance; } }
五.枚举
1.天然的线程安全,效率高
2.代码简洁。
3.防止反射和反序列化破解
package com.wxisme.singleton; /** * 枚举实现单例模式 * @author wxisme * */ public enum SingletonFive { INSTANCE; public void getInstance() { } }
存在的问题:以上方法中除了枚举的方式之外,都可以通过反射和反序列化的方式来破解。
来破解一下。
package com.wxisme.singleton; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; /** * 反射和反序列化破解单例模式(除枚举之外的都可以破解) * @author wxisme * */ @SuppressWarnings("all") public class SingletonBreak { /* * 通过反射破解,以懒汉式为例 * 应对策略:在私有构造器中手动抛出异常 */ public static void test1() throws Exception { SingletonTow st1 = SingletonTow.getInstance(); SingletonTow st2 = SingletonTow.getInstance(); System.out.println(st1==st2); //反射破解 Class<SingletonTow> clazz = (Class<SingletonTow>) Class.forName("com.wxisme.singleton.SingletonTow"); //获取构造器 Constructor<SingletonTow> c = clazz.getDeclaredConstructor(null); c.setAccessible(true);//跳过权限的检查,可以访问私有构造器 SingletonTow st3 = c.newInstance(); SingletonTow st4 = c.newInstance(); System.out.println(st3==st4); } /* * 反序列化破解 以懒汉式为例 (被反序列化的类必须实现Serializable接口) * 应对策略:在类中定义一个readResolve()方法,当反序列化时直接返回已经存在的对象 */ public static void test2() throws IOException, ClassNotFoundException { SingletonTow st1 = SingletonTow.getInstance(); SingletonTow st2 = SingletonTow.getInstance(); System.out.println(st1==st2); //序列化st1对象 FileOutputStream fos = new FileOutputStream("e:/a.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(st1); oos.close(); fos.close(); //反序列化创建对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/a.txt")); SingletonTow st3 = (SingletonTow) ois.readObject(); ois.close(); System.out.println(st1==st3); } public static void main(String[] args) throws Exception { test1(); System.out.println("---------------"); test2(); } }
不过这种破解可以防止,但是比较繁琐。
防止反射破解的方法:
在私有构造器中手动抛出异常
private SingletonTow() { //防止反射破解 if(instance != null) { throw new RuntimeException(); } }
添加一个getClass()方法
private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if(classLoader == null) classLoader = SingletonTow.class.getClassLoader(); return (classLoader.loadClass(classname));
防止反序列化破解的方法:
添加一个readRsolve()方法
private Object readResolve() { return instance; }
总结:通过以上可以得出结论:如果需要延时加载,静态内部类好于懒汉式,不需要延时加载则枚举好于饿汉式。双重检验锁是对饿汉式的优化但是不推荐使用。如果没有特别的安全要求静态内部类式是最好的,如果需要还可以防止破解。懒汉式也不错。
PS.在多线程环境下测试每种方式的执行效率。(感谢高淇老师的视频:))
必须在除main线程执行其他所有的线程执行完之后才能计时,用到了CountDownLatch类来控制。
Demo:
package com.wxisme.singleton; import java.util.concurrent.CountDownLatch; /** * 在多线程环境下测试单例模式的效率 * @author wxisme * */ public class TestEfficiency { public static void main(String[] args) { long start = System.currentTimeMillis(); int thread = 0; //内部类方法生命周期和全局变量不一致,需要加final final CountDownLatch count = new CountDownLatch(thread); for(int i=0; i<100; i++) { new Thread(new Runnable() { @Override public void run() { for(int j=0; j<10000; j++) { Object o = SingletonOne.getInstance(); } count.countDown();//一个线程执行完计数器减一。 } }).start(); } try { count.await(); } catch (InterruptedException e) { e.printStackTrace(); }//阻塞main线程,直到所有的线程执行完,线程计数器减为零。 long end = System.currentTimeMillis(); } }