单例模式
单例模式定义:不管在什么场景下只能生成一个类实例;
单例模式主要注意以下几个问题:
1. 线程安全问题;解决方法:sychronized、静态内部类、双重检测时用volatile;
2. 反射破坏单例模式;解决方法:单例构造函数中加对象非空校验;
3. 序列化反序列化破坏单例模式;解决方法:重写readResolve方法或者用枚举单例;
懒汉和饿汉的本质区别,就是实例化对象的时机,即是什么时候将对象创建起来
饿汉式:在加载类的时候就创建单例对象,所以调用时肯定线程安全 缺点是:不管用不用,都会创建对象,消耗内存对象,可能造成浪费(占着茅坑不拉屎) @ThreadSafe public class Hungry { private Hungry() {} private static final Hungry instance = new Hungry(); public static Hungry getInstance() { return instance; } }
懒汉式单例模式建议使用静态内部类,JVM能够确保线程安全问题,但是要解决反射攻击和序列化反序列化攻击,还得通过代码来实现:
解决反射攻击的问题:需要在构造函数中控制私有静态成员变量的重复创建;
解决序列化反序列化攻击的问题:重写readResolve()方法
/** * Created by marcopan on 2018/9/14. * 有反射攻击和序列化攻击的问题 * * 反射攻击解决方法,在构造函数中判断单例是否为空 */ public class LazySingleton implements Serializable { private LazySingleton() { // 防止通过反射来破坏单例 if (LazySingletonHolder.singleton != null) { throw new RuntimeException("不允许创建多个实例"); } } public static final LazySingleton getInstance() { return LazySingletonHolder.singleton; } private static class LazySingletonHolder { private static final LazySingleton singleton = new LazySingleton(); } // public static void main(String[] args) { // try { // //很无聊的情况下,进行破坏 // Class<?> clazz = LazySingleton.class; // //通过反射拿到私有的构造方法 // Constructor c = clazz.getDeclaredConstructor(null); // //强制访问,强吻,不愿意也要吻 // c.setAccessible(true); // // //暴力初始化 // Object o1 = c.newInstance(); // //调用了两次构造方法,相当于new了两次,犯了原则性问题, // Object o2 = LazySingleton.getInstance();//c.newInstance(); // // System.out.println(o1 == o2); // } catch (Exception e) { // e.printStackTrace(); // } // } private Object readResolve() { return LazySingletonHolder.singleton; } public static void main(String[] args) { try { LazySingleton instance1 = LazySingleton.getInstance(); FileOutputStream fos = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); LazySingleton instance2 = (LazySingleton) ois.readObject(); ois.close(); System.out.println(instance1 == instance2); } catch (Exception e) { e.printStackTrace(); } } }
双重校验机制需要添加volatile关键字,防止指令重排序,双重校验机制同样无法解决反射攻击和序列化反序列化的问题。
public class RegisterSingleton { /** volatile防止指令重排序 * 有反射和序列化攻击的问题 */ private volatile static RegisterSingleton lazy = null; private RegisterSingleton() { } public static RegisterSingleton getInstance() { if (lazy == null) { synchronized (RegisterSingleton.class) { if (lazy == null) { lazy = new RegisterSingleton(); //1.分配内存给这个对象 //2.初始化对象 //3.设置lazy指向刚分配的内存地址 //4.初次访问对象 } } } return lazy; } }
枚举单例模式能够完美解决反射攻击和序列化反序列化的问题。
public enum EnumSingleton { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; } public static void main(String[] args) { try { EnumSingleton instance1 = EnumSingleton.getInstance(); instance1.setData(new Object()); FileOutputStream fos = new FileOutputStream("EnumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("EnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); EnumSingleton instance2 = (EnumSingleton) ois.readObject(); ois.close(); System.out.println(instance1.getData()); System.out.println(instance2.getData()); System.out.println(instance1.getData() == instance2.getData()); } catch (Exception e) { e.printStackTrace(); } } }
其中关于枚举的可以参考这篇文章,写得挺透彻的http://www.manongjc.com/article/1597.html
这里还要强调一点关于双重检查锁的情况,双重检查锁之所以是线程不安全的,原因在于CPU处理器会对指令进行重排序,有线程可能会拿到没有完成初始化的对象。双重检查锁要使用volatile变量避免双重检查锁线程不安全的问题