【Java设计模式001单例模式

2023年 7月 25日 25.6k 0

前言

大家好,我们的gzh是朝阳三只大明白,满满全是干货,分享近期的学习知识以及个人总结(包括读研和IT),跪求一波关注,希望和大家一起努力、进步!!

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。如果我们需要让某一个类在整个程序生命周期内只能有一个实例,那么就要使用单例模式。

想要实现单例模式,必须满足三个必要条件:

  • 单例类的构造器是私有的,客户端无法通过 new 关键字创建实例;
  • 单例类必须自己创建自己的唯一实例;
  • 单例类必须给客户端提供一个方法以获取到唯一实例;
  • 实现

    单例设计模式一般分为如下两类:

    • 饿汉式:类加载就会导致该单实例对象被创建;
    • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会被创建。

    实现的基本思路如下:

  • 构造器私有化;
  • 类的内部创建对象;
  • 向外暴露静态 getInstance 方法;
  • 懒汉式-静态变量法

    静态变量法是最基本的一个实现,通过静态变量的方式创建唯一对象,其实现如下:

    public class Singleton1 {
    
        // 类的内部创建对象;
        private static Singleton1 instance = new Singleton1();
    
        // 构造器私有化
        private Singleton1() {
        }
    
        // 向外暴露静态方法
        public static Singleton1 getInstance() {
            return instance;
        }
    }
    

    除此之外还有一种使用静态代码块的构建方法

    public class Singleton2 {
        private static Singleton2 instance;
    
        // 使用静态代码块创建静态示例;
        static {
            instance = new Singleton2();
        }
    
        private Singleton2() {
        }
    
        public static Singleton2 getInstance() {
            return instance;
        }
    }
    
    • 优势:写法简单,避免了多线程的问题;
    • 劣势:没有实现懒加载的效果,这种方式基于classloder机制避免了多线程的同步问题,但会造成不必要的内存浪费;

    懒汉式-线程不安全实现

    为了解决饿汉式类加载造成的内存浪费,可以采用懒加载的方式创建实例,以下是懒汉式的最基本实现。

    public class Singleton3 {
        private static Singleton3 instance;
    
        private Singleton3() {
        }
    
        // 可能出现线程安全问题
        public static Singleton3 getInstance() {
            if (instance == null) {
                instance = new Singleton3();
            }
    
            return instance;
        }
    }
    

    这种方式是最基本的实现方式,其最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式不要求线程安全,在多线程不能正常工作。

    懒汉式-线程安全

    由于懒汉式基础实现有多线程安全问题,因此最简单的解决方案就是加锁,其实现如下:

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

    这种方式能够在多线程中很好的工作,同时能够实现懒加载。但是锁的粒度很大,同步效率低。

    懒汉式-双重检查

    public class Singleton5 {
        private static Singleton5 instance;
    
        private Singleton5() {
        }
    
        // 双重检查
        public static Singleton5 getInstance() {
            if (instance == null) {
                synchronized (Singleton5.class) {
                    if (instance == null) {
                        instance = new Singleton5();
                    }
                }
            }
    
            return instance;
        }
    }
    

    双重检查实现方式既能保证线程安全,也能实现懒加载,同时其效率较高,在多线程情况下能保持高性能。

    懒汉式-静态内部类实现

    使用静态内部类的优点在于:

  • 当一个类被加载的时候,静态内部类不会被加载;
  • classloader会保证静态内部类加载时线程安全。
  • 使用静态内部类可以天然的实现线程安全的懒加载单例类,其具体实现如下:

    public class Singleton6 {
    
        private static Singleton6 instance;
    
        private Singleton6() {
        }
    
        public static Singleton6 getInstance() {
            return InnerSingleton6.instance;
        }
    
        // 使用静态内部类实现单例
        public static final class InnerSingleton6 {
            private static Singleton6 instance = new Singleton6();
        }
    }
    

    类加载器加载 Singleton6时不定会加载 InnerSingleton6 只有显式的调用 getInstance 方法时才会加载内部类,从而实例化 instance。这种实现由 JVM 保证类加载时的线程安全,效率非常高。

    使用枚举类

    这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。其唯一的问题在于枚举类永远继承自 Enum,且无法被继承;

    public enum Singleton7 {
    
        INSTANCE;
    
    
        public void otherMethod() {
            // ....
            return;
        }
    }
    

    上面的这种实现是一种饿汉式实现,也可以将业务类放在一个枚举类中:

    public enum Singleton8 {
        INSTANCE;
        private User user;
    
        Singleton8() {
            user = new User();
        }
    
        public User getUser() {
            return user;
        }
    
        public static  class User {
    
        }
    }
    

    破坏单例模式

    序列化和反序列化

    可以使用序列化和反序列破坏单例模式,以下面单例类为例:

    public class Singleton1 implements Serializable {
    
        // 类的内部创建对象;
        private static Singleton1 instance = new Singleton1();
    
        // 构造器私有化
        private Singleton1() {
        }
    
        // 向外暴露静态方法
        public static Singleton1 getInstance() {
            return instance;
        }
    }
    

    测试类:

    public class Main {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            Singleton1 oriInstance = Singleton1.getInstance();
            // 创建对象输出流并输出对象
            ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("obj.txt")));
            oos.writeObject(oriInstance);
            // 释放资源
            oos.close();
    
            // 创建对象输入流并读取
            ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("obj.txt")));
            Singleton1 readInstance = (Singleton1) ois.readObject();
            // 释放资源
            ois.close();
    
    		// false
            System.out.println(oriInstance == readInstance);
        }
    }
    

    解决方案:在单例类中定义一个 readResolve 方法,该方法在反序列化时被调用;以 ObjectInputStream 源码为例:

    class ObjectInputStream{
        private Object readOrdinaryObject(boolean unshared)
            throws IOException
        {
            if (bin.readByte() != TC_OBJECT) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            desc.checkDeserialize();
    
            Class cl = desc.forClass();
            if (cl == String.class || cl == Class.class
                    || cl == ObjectStreamClass.class) {
                throw new InvalidClassException("invalid class descriptor");
            }
    
            Object obj;
            try {
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
    
            passHandle = handles.assign(unshared ? unsharedMarker : obj);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(passHandle, resolveEx);
            }
    
            if (desc.isExternalizable()) {
                readExternalData((Externalizable) obj, desc);
            } else {
                readSerialData(obj, desc);
            }
    
            handles.finish(passHandle);
    		
            // 如果有readResolve方法,则调用该方法
            if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
            {
                Object rep = desc.invokeReadResolve(obj);
                if (unshared && rep.getClass().isArray()) {
                    rep = cloneArray(rep);
                }
                if (rep != obj) {
                    // Filter the replacement object
                    if (rep != null) {
                        if (rep.getClass().isArray()) {
                            filterCheck(rep.getClass(), Array.getLength(rep));
                        } else {
                            filterCheck(rep.getClass(), -1);
                        }
                    }
                    handles.setObject(passHandle, obj = rep);
                }
            }
    
            return obj;
        }
    }
    

    反射

    通过反射同样可以破坏单例模式,其测试类如下:

    public class test {
        public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
            // 获取字节码文件
            Class singleton1Class = Singleton1.class;
            // 获取获取无参构造器
            Constructor constructor = singleton1Class.getDeclaredConstructor();
            // 设置访问权限
            constructor.setAccessible(true);
    
            Singleton1 singleton1 = constructor.newInstance();
            Singleton1 singleton2 = constructor.newInstance();
    
    		// false
            System.out.println(singleton1 == singleton2);
        }
    }
    

    解决方案:在私有构造器中加上一个判断,如果对象二次创建,则直接抛出异常。结合防止序列化和反序列化的方式,单例类的代码修改如下:

    public class Singleton1 implements Serializable {
    
        // 类的内部创建对象;
        private static Singleton1 instance = new Singleton1();
    
        // 放置反射破坏单例
        private static boolean uniqueInstance = false;
    
        // 构造器私有化
        private Singleton1() {
            if (uniqueInstance) {
                throw new RuntimeException("重复构建对象");
            }
    
            uniqueInstance = true;
        }
    
        // 向外暴露静态方法
        public static Singleton1 getInstance() {
            return instance;
        }
        
        // 放置序列化反序列化破坏单例
        public Object readResolve() {
            return instance;
        }
    }
    

    总结

    一般情况下,使用静态变量法实现饿汉式;如果有实现懒加载的需求时使用静态内部类的方式进行实现;如果涉及到反序列化创建对象时,可以尝试使用枚举方式。最后才考虑使用双重检查的方式实现单例。

    文中难免会出现一些描述不当之处(尽管我已反复检查多次),欢迎在留言区指正,相关的知识点也可进行分享,希望大家都能有所收获!!如果觉得我的文章写得还行,不妨支持一下。你的每一个转发、关注、点赞、评论都是对我最大的支持!

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论