zoukankan      html  css  js  c++  java
  • 单例模式

    一、单例模式介绍

    1、定义与类型

    定义:保证一个类仅有一个实例,并提供一个全局访问点
    类型:创建型

    2、适用场景

    想确保任何情况下都绝对只有一个实例

    3、优点

    在内存里只有一个实例,减少了内存开销
    可以避免对资源的多重占用
    设置全局访问点,严格控制访问

    4、缺点

    没有接口,扩展困难

    5、重点

    私有构造器:禁止从单例类外部构造对象
    线程安全
    延迟加载:使用时才创建
    序列化和反序列化安全:序列化和反序列化会对单例模式进行破坏
    反射:防御反射攻击

    二、代码示例

    1、懒汉式及多线程

    注重延迟加载:

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
        private LazySingleton(){
        }
        public static LazySingleton getInstance(){
            if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    但是存在线程安全问题,所以可以增加synchronized:

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
        private LazySingleton(){
        }
        public synchronized static LazySingleton getInstance(){
            if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    2、Double Check双重检查

    但是 synchronized 对性能存在影响,所以可以使用Double Check双重检查:

    public class LazyDoubleCheckSingleton {
        private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
        private LazyDoubleCheckSingleton(){
        }
        public static LazyDoubleCheckSingleton getInstance(){
            if(lazyDoubleCheckSingleton == null){
                synchronized (LazyDoubleCheckSingleton.class){
                    if(lazyDoubleCheckSingleton == null){
                        lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }
            return lazyDoubleCheckSingleton;
        }
    }
    

    其中

    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
    

    这一句代码包含三个步骤:
    1.分配内存给这个对象
    2.初始化对象
    3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
    在java语言规范中 允许在单线程内,不会改变单线程执行结果的重排序。
    所以 2和3步可能会存在指令重排序,在单线程中,不会影响执行结果:

    此时在多线程中:

    此时线程1访问对象,但是对象在线程0中还没有初始化完成,可能就会报异常。
    解决方案:
    方案1、不允许2、3步骤重排序:
    使用volatile关键字:

    public class LazyDoubleCheckSingleton {
        private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
        private LazyDoubleCheckSingleton(){
        }
        public static LazyDoubleCheckSingleton getInstance(){
            if(lazyDoubleCheckSingleton == null){
                synchronized (LazyDoubleCheckSingleton.class){
                    if(lazyDoubleCheckSingleton == null){
                        lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }
            return lazyDoubleCheckSingleton;
        }
    }
    

    使用了volatile后,所有线程都可以看到共享内存的最新状态,保证了内存的可见性。用volatile关键字修饰的变量,在进行写操作时,会多出一些汇编代码,将当前处理器缓存行的数据写回到内存,其中涉及到缓存一致性协议。

    方案2、允许重排序,但不允许其他线程看到这个重排序,即静态内部类

    3、静态内部类

    基于类初始化的延迟加载解决方案

    public class StaticInnerClassSingleton {
        private static class InnerClass{
            private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
        }
        public static StaticInnerClassSingleton getInstance(){
            return InnerClass.staticInnerClassSingleton;
        }
        private StaticInnerClassSingleton(){
        }
    }
    

    原理:存在Class对象的初始化锁,并且非构造线程,是看不到指令重排序的。
    线程0初始化Class,线程1看不到初始化过程。所以静态内部类这种方法的核心在于InnerClass这个类的对象初始化锁

    补充:类在以下几种情况下被初始化,1.实例被创建(new、反射、序列化),2.静态方法被调用,3.静态成员被赋值,4.非常量静态成员被使用,5.顶级类中有嵌套的断言语句,6.子类被初始化

    4、饿汉式

    最简单的写法:

    public class HungrySingleton {
        private final static HungrySingleton hungrySingleton;
        static{
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton(){
        }
        public static HungrySingleton getInstance(){
            return hungrySingleton;
        }
    }
    

    优点是类加载的时候就完成了初始化,避免了线程同步的问题
    缺点是没有延迟加载的效果,可能造成累成内存浪费
    饿汉与懒汉之间最大的区别就是延迟加载:饿汉式很饿,一上来就想吃东西,马上就把对象创建好了;而懒汉式非常懒,不用它的时候都不会创建这个对象。

    5、序列化破坏单例模式

    以下序列化和反序列化 将会破坏单例模式:

    // 实现序列化接口
    public class HungrySingleton implements Serializable {
        private final static HungrySingleton hungrySingleton;
        static{
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton(){
        }
        public static HungrySingleton getInstance(){
            return hungrySingleton;
        }
    }
    

    测试类:

    public class Test {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            HungrySingleton instance = HungrySingleton.getInstance();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(instance);
            File file = new File("singleton_file");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
            HungrySingleton newInstance = (HungrySingleton) ois.readObject();
            // 将会输出两个不同的内存地址
            System.out.println(instance);
            System.out.println(newInstance);
        }
    }
    

    解决方法:反序列化是通过反射生成对象,在这个过程中,会判断是否存在并调用readResolve方法

    所以可通过增加readResolve方法防止反序列化:

    public class HungrySingleton implements Serializable{
        private final static HungrySingleton hungrySingleton;
        static{
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton(){
        }
        public static HungrySingleton getInstance(){
            return hungrySingleton;
        }
        private Object reaResolve(){
            // 返回单例对象
            return hungrySingleton;
        }
    }
    

    但是在这个过程中,仍然被创建了新的对象,只是最后没有返回而已。

    6、反射攻击

    public class Test {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
            Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            HungrySingleton instance = HungrySingleton.getInstance();
            HungrySingleton newInstance = declaredConstructor.newInstance();
    
            System.out.println(instance);
            System.out.println(newInstance);
            // 输出false
            System.out.println(instance == newInstance);
        }
    }
    

    对于饿汉式单例、静态内部类单例,因为是在类初始化时就创建了对象,所以可在构造器中进行反射防御:

    public class HungrySingleton implements Serializable{
        private final static HungrySingleton hungrySingleton;
        static{
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton(){
            // 反射防御,当类在初始化时,单例就会被初始化,为第一次调用;反射时,为第二次调用就会报错	
            if(hungrySingleton != null){
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
        public static HungrySingleton getInstance(){
            return hungrySingleton;
        }
        private Object readResolve(){
            // 返回单例对象
            return hungrySingleton;
        }
    }
    

    而对于不是在类初始化时创建对象的单例模式,则无法防御反射攻击,例如懒汉式单例模式:

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
        private LazySingleton(){
            if(lazySingleton != null){
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
        public synchronized static LazySingleton getInstance(){
            if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    因为在被反射攻击的时候,单例可能还没有被创建,所以会产生不同实例,测试类:

    public class Test {
        public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
            // 反射攻击
            Class<LazySingleton> lazySingletonClass = LazySingleton.class;
            Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            // 先反射
            LazySingleton newInstance = declaredConstructor.newInstance();
            // 后取单例,因为类中的实例仍为null,所以构造器的判断没有起到想要的作用
            LazySingleton instance = LazySingleton.getInstance();
    
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }
    

    可以增加信号量进行控制:

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
        private static boolean flag = true;
        private LazySingleton(){
            if (flag){
                flag = false;
            } else {
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
        public synchronized static LazySingleton getInstance(){
            if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    但是信号量仍然可以被修改,以达到反射攻击:

    public class Test {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, NoSuchFieldException, InvocationTargetException {
            Class objectClass = LazySingleton.class;
            Constructor c = objectClass.getDeclaredConstructor();
            c.setAccessible(true);
    
            LazySingleton o1 = LazySingleton.getInstance();
    
            Field flag = o1.getClass().getDeclaredField("flag");
            flag.setAccessible(true);
            // 修改信号量
            flag.set(o1,true);
    
            LazySingleton o2 = (LazySingleton) c.newInstance();
    
            System.out.println(o1);
            System.out.println(o2);
            // 返回false
            System.out.println(o1==o2);
        }
    }
    

    7、Enum枚举单例

    枚举类型天然的可序列化机制,能够强有力得保证不会多次实例化的情况。即使在复杂的序列化或者反射攻击下,枚举模式都没有问题。

    public enum EnumInstance {
        INSTANCE{
            protected  void printTest(){
                System.out.println("Geely Print Test");
            }
        };
        protected abstract void printTest();
        private Object data;
        public Object getData() {
            return data;
        }
        public void setData(Object data) {
            this.data = data;
        }
        public static EnumInstance getInstance(){
            return INSTANCE;
        }
    }
    

    在ObjectInputStream中,对于枚举类型,是通过枚举类直接获得唯一的枚举常量,没有创建新的对象,维护了枚举的单例属性:

    而对于反射,在调用
    objectClass.getDeclaredConstructor();
    时会直接报错:
    java.lang.NoSuchMethodException
    原因在于Enum本身就只有一个构造器:

    而如果调用

    Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
    constructor.setAccessible(true);
    EnumInstance instance = (EnumInstance) constructor.newInstance("11",22);
    

    也会直接报错:java.lang.IllegalArgumentException: Cannot reflectively create enum objects

    如果通过jad反编译枚举类,可以看到:1.class类为final的;2.构造器为private;3.声明的枚举对象是static和final的;4.枚举对象在static代码块中实例化
    所以枚举单例是最安全的单例模式

    8、容器单例

    public class ContainerSingleton {
    
        private ContainerSingleton(){
        }
        private static Map<String,Object> singletonMap = new HashMap<String,Object>();
    
        public static void putInstance(String key,Object instance){
            if(StringUtils.isNotBlank(key) && instance != null){
                if(!singletonMap.containsKey(key)){
                    singletonMap.put(key,instance);
                }
            }
        }
        public static Object getInstance(String key){
            return singletonMap.get(key);
        }
    }
    

    容器单例与享元模式相似
    优点:统一管理,节省资源,相当于缓存
    缺点:存在线程安全问题

    9、ThreadLocal线程单例

    public class ThreadLocalInstance {
        private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
                 = new ThreadLocal<ThreadLocalInstance>(){
            @Override
            protected ThreadLocalInstance initialValue() {
                return new ThreadLocalInstance();
            }
        };
        private ThreadLocalInstance(){
    
        }
        public static ThreadLocalInstance getInstance(){
            return threadLocalInstanceThreadLocal.get();
        }
    }
    

    这个单例 并不能保证整个应用全局唯一,但能保存线程唯一。
    ThreadLocal会为每一个线程提供一个变量副本,本身是基于ThreaLocalMap实现的,维持了线程间的隔离。原理是以空间换时间的方式,会创建很多对象,在一个线程里会创建唯一的一个对象。在多线程访问的时候,彼此不会相互影响。

    三、源码示例

    1、JDK中的Runtime:饿汉式

    2、JDK中的Desktop:懒汉式+容器式+线程安全控制

    3、spring

    4、mybatis:ThreadLocal

  • 相关阅读:
    浮动float 摆放位置
    边框(border)宽度样式颜色 和基本属性
    调用css文件,进行调色
    deque_01
    iterator_教程中的讲解
    vector_01
    VS2013_CodeLens
    Qt for Embedded Linux
    jsjl_for_ubuntu12.04
    VC6_导入lib库
  • 原文地址:https://www.cnblogs.com/weixk/p/12905508.html
Copyright © 2011-2022 走看看