单例模式

单例模式定义:不管在什么场景下只能生成一个类实例;

单例模式主要注意以下几个问题:

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变量避免双重检查锁线程不安全的问题

posted @ 2017-10-14 22:42  不停的奋斗  阅读(151)  评论(0编辑  收藏  举报