• 创建型设计模式之单例模式


    单例模式

    概述

    单例模式保证一个对象在JVM中只能有一个实例,减少内存开销,避免对资源的多重占用。单例模式属于创建型模式。

    在单例模式中,单例类只能有一个实例,必须由自己创建自己的唯一实例,给所有其他对象提供这一实例。

    常见单例:

    懒汉式:就是需要的才会去实例化,线程不安全。
    
    饿汉式:就是当class文件被加载的时候,初始化,天生线程安全。
    
    • 1
    • 2
    • 3

    比如:ServletContext、ServletContextConfig、ApplicationContext等都是单例形式

    优缺点

    优点:

    1.在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
    
    2.避免对资源的多重占用。
    
    • 1
    • 2
    • 3

    缺点:

    1.没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
    
    • 1

    懒汉式

    懒汉式单例模式是对象要在被使用时才会初始化,能解决饿汉式单例可能带来的内存浪费问题

    注意:该方式在多线程中使用存在线程安全问题。

    public class Singleton {
        //需要时才会被实例化
        private static Singleton singleton;
    
        private Singleton() {}
        
    	public static Singleton getSingleton() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    使用synchronized关键字使方法变成线程同步方法。

    这种方法在线程数量较多情况下,如果CPU压力过大,则会导致大批线程阻塞,从而导致程序性能大幅下降。

    public class Singleton {
        //需要时才会被实例化
        private static Singleton singleton;
    
        private Singleton() {
    
        }
        synchronized public static Singleton getSingleton() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    
        public static void main(String[] args) {
            Singleton sl1 = Singleton.getSingleton();
            Singleton sl2 = Singleton.getSingleton();
            System.out.println(sl1 == sl2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    使用双重检查锁的的单例模式可进一步提升性能。

    public class Singleton {
        //需要时才会被实例化
        private static Singleton singleton;
        private Singleton() {}
        
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    //检查是否要重新创建实例
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    饿汉式

    饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

    适用于单例对象较少的情况。如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。

    优点:能保证绝对线程安全、执行效率比较高

    缺点:所有对象类加载的时候就实例化

    public class Singleton{
        //当class文件被加载初始化
        private static Singleton singleton = new Singleton();
    
        private Singleton() {}
    
        public static Singleton getSingleton() {
            return singleton;
        }
    }
    
      public static void main(String[] args) {
            Singleton singleton1 = Singleton.getSingleton();
            Singleton singleton2 = Singleton.getSingleton();
            System.out.println(singleton1 == singleton2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上述是标准写法,还有另外一种写法,利用静态代码块的机制

    public class Singleton{
        //当class文件被加载初始化
        private static Singleton singleton;
    
    	static{
    	 singleton = new Singleton()
    	}
    
        private Singleton() {}
    
        public static Singleton getSingleton() {
            return singleton;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    其他实现方式

    静态内部类实现单例

    由于饿汉式单例模式存在内存浪费问题,懒汉式单例模式有synchronized性能问题。

    实际上可以从类的初始化角度,采用静态内部类的方式,利用内部类一定是要在方法调用之前初始化的特点,巧妙避免线程安全问题

    public class Singleton {
    
        private Singleton() {
            if (LazySingleton.INSTANCE != null) {
                throw new RuntimeException("非法访问");
            }
        }
    
        /**
         * 在返回结果前先加载内部类
         */
        private static Singleton getInstance() {
            return LazySingleton.INSTANCE;
        }
    
        /**
         * 默认不加载
         */
        private static class LazySingleton {
            private static final Singleton INSTANCE = new Singleton();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ThreadLocal实现线程单例

    ThreadLocal不能保证其创建的对象是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的

    单例模式为了线程安全会给方法加锁,以时间换空间。ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间实现线程隔离。

    public class ThreadLocalSingleton {
        private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
                new ThreadLocal<ThreadLocalSingleton>() {
                    @Override
                    protected ThreadLocalSingleton initialValue() {
                        return new ThreadLocalSingleton();
                    }
                };
    
        private ThreadLocalSingleton() {
        }
    
        public static ThreadLocalSingleton getInstance() {
            return threadLocaLInstance.get();
        }
    
        public static void main(String[] args) {
            ThreadLocalSingleton threadLocalSingleton1 = ThreadLocalSingleton.getInstance();
            ThreadLocalSingleton threadLocalSingleton2 = ThreadLocalSingleton.getInstance();
            System.out.println(threadLocalSingleton1 == threadLocalSingleton2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    注册式单例模式

    注册式单例模式又称为登记式单例模式,将每一个实例都登记到某一个地方,使用唯一的标识获取实例。

    注册式单例模式有两种:枚举式单例模式容器式单例模式

    枚举式单例模式

    枚举式单例模式是比较推荐的一种单例模式实现,写法优雅

    注意:与饿汉式类似,在类加载时就将所有对象初始化,不适合大量创建单例对象的场景

    public enum EnumSingleton {
        INSTANCE;
        
        public static EnumSingleton getInstance() {
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            EnumSingleton instance1 = EnumSingleton.getInstance();
            EnumSingleton instance2 = EnumSingleton.getInstance();
            System.out.println(instance1 == instance2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    容器式单例模式

    容器式单例模式适用于需要大量创建单例对象的场景,便于管理。注意:它是非线程安全的。

    public class ContainerSingleton {
    
        private ContainerSingleton() {
        }
    
        private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
    
        public static Object getInstance(String className) {
            Object instance = null;
            if (!ioc.containsKey(className)) {
                try {
                    instance = Class.forName(className).newInstance();
                    ioc.put(className, instance);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return instance;
            } else {
                return ioc.get(className);
            }
        }
    
        public static void main(String[] args) {
            Object instance1 = ContainerSingleton.getInstance("cn.ybzy.demo.ContainerSingleton");
            Object instance2 = ContainerSingleton.getInstance("cn.ybzy.demo.ContainerSingleton");
            System.out.println(instance1 == instance2);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    单例模式的破坏

    反射破坏

    如果一个单例模式的私有构造不做任何处理,就可以通过反射破坏单例模式

    private Singleton() {}
    
    • 1
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
            Class<Singleton> singletonClass = Singleton.class;
            // 反射强制获取私有构造函数
            Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor(null);
            // 强制访问私有构造函数
            declaredConstructor.setAccessible(true);
            // 暴力调用私有构造函数进行初始化
            Singleton singleton1 = declaredConstructor.newInstance();
            Singleton singleton2 = declaredConstructor.newInstance();
    
            System.out.println(singleton1 == singleton2);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在私有构造函数中添加限制处理,防止实例多次重复创建。

        private Singleton() {
            if (LazySingleton.INSTANCE != null) {
                throw new RuntimeException("非法访问");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    序列化破坏

    序列化是将一个创建好的对象写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。由于反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就会破坏单例

    public class SeriableSingleton implements Serializable {
        public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    
        private SeriableSingleton() {
        }
    
        public static SeriableSingleton getInstance() {
            return INSTANCE;
        }
    
    //    private Object readResolve(){ return INSTANCE;}
    
    
        public static void main(String[] args) {
    
            SeriableSingleton s1 = SeriableSingleton.getInstance();
            SeriableSingleton s2 = null;
            FileOutputStream fos = null;
            try {
    
                fos = new FileOutputStream("SeriableSingleton.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(s1);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                s2 = (SeriableSingleton) ois.readObject();
                ois.close();
    
                System.out.println(s1 == s2);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    只需要增加一个叫readResolve方法返回实例就可以解决单例模式被序列化破坏的问题。

    关键核心在于ObjectInputStreamreadObject(Class type)方法调用readObject0方法中的switch (tc) 判断处理。

    注意:实际上还是会实例化多次,只不过新创建的对象没有被返回而已。如果创建对象的发生频率加快,则内存分配开销也会随之增大

    private Object readResolve(){ return INSTANCE;}
    
    • 1
  • 相关阅读:
    吴恩达的机器学习,属实牛逼
    智驾发展的前世今生|千寻NSSR,从安全和自主可控出发
    Cesium 修改鼠标样式
    每 日 练 习
    Text ‘10/03/2023 14:25:49‘ could not be parsed at index 0
    linux下二进制安装docker最新版docker-24.0.6
    稳定性总结
    一文概括AxureRP的优缺点和替代软件
    Linux中设置git的代理
    RocketMQ NameServer如何保证数据最终一致
  • 原文地址:https://blog.csdn.net/qq_38628046/article/details/126072579