• 单例设计模式--创建者模式


    单例模式

    单例模式Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

    这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

    单例模式是保证系统实例唯一性的重要手段。单例模式首先是要通过将类的实例化方法先私有化来防止程序通过其他方式创建该类的实例,然后通过提供一个全局唯一获取该类实例的方法帮助用户获取类的实例,用户只需要通过调用该方法获取类的实例。

    单例模式的设计保证了一个类再整个系统中同一时刻只有一个实例的存在,主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。同时,单例模式为系统资源的优化提供了很好的思路,频繁的创建和销毁对象都会增加系统的资源消耗,而单例模式下保障了整个系统只有一个对象能被使用,很好地节约了资源。

    单例模式的实现是很简单的,每次在获取对象前都会首先判断系统是否已经有了这个单例对象,有就返回0,没有的话就创建。
    在这需要注意的是:单例模式的类构造函数是私有的,只能由自身去创建和销毁对象,不允许除了该类以外的其他程序使用 new 关键字去创建对象。

    在单例模式中常见的写法分为两种:

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

    饿汉式方式

    1. 静态变量方式

    该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

    public class Singleton {
        //私有构造方法
        private Singleton() {}
    
        //在成员位置创建该类的对象
        private static Singleton instance = new Singleton();
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 静态代码块方式

    该方式在成员位置声明 Singleton 类型的静态变量,而对象的创建是在静态代码块中,也是在类的加载时而创建。所以和上面的基本上一样,当然该方式也存在内存浪费问题。

    public class Singleton {
    
        //私有构造方法
        private Singleton() {}
    
        //在成员位置创建该类的对象
        private static Singleton instance;// null
    
        static {
            instance = new Singleton();
        }
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 枚举方式

    枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

    public enum Singleton {
        INSTANCE;
    }
    
    • 1
    • 2
    • 3

    懒汉式方式

    1. 不做任何处理,是线程不安全的。

    这个方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,当调用 getInstance() 方法获取 Singleton 类的对象的时候才创建Singleton 类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

    public class Singleton {
        //私有构造方法
        private Singleton() {}
    
        //在成员位置创建该类的对象
        private static Singleton instance;
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            if(instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 加 synchronized(线程安全的)

    在 getInstance() 方法上添加了 synchronized 关键字,导致该方法的执行效果特别低。(但是它是安全的)。

    其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

    public class Singleton {
        //私有构造方法
        private Singleton() {}
    
        //在成员位置创建该类的对象
        private static Singleton instance;
    
        //对外提供静态方法获取该对象
        public static synchronized Singleton getInstance() {
    
            if(instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    1. 双重检查锁

    对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。
    由此也产生了一种新的实现模式:双重检查锁模式

    public class Singleton { 
    
        //私有构造方法
        private Singleton() {}
    
        private static Singleton instance;
    
       //对外提供静态方法获取该对象
        public static Singleton getInstance() {
    		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
            if(instance == null) {
                synchronized (Singleton.class) {
                    //抢到锁之后再次判断是否为null
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候会进行优化和指令重排序操作。

    出现这个问题,我们也问题不大,只需要加上 volatile 关键字, volatile 关键字可以保证可见性和有序性。(具体volatile 是什么,看 JVM。哈哈,在这我们就只需知道这个关键字可以防止指令重排的问题)

    public class Singleton {
    
        //私有构造方法
        private Singleton() {}
    
        private static volatile Singleton instance;
    
       //对外提供静态方法获取该对象
        public static Singleton getInstance() {
    		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
            if(instance == null) {
                synchronized (Singleton.class) {
                    //抢到锁之后再次判断是否为空
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

    1. 静态内部类方式

    静态内部类单例模式中实例是由内部类创建的,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

    public class Singleton {
    
        //私有构造方法
        private Singleton() {}
    
    	// 定义一个静态的内部类
        private static class SingletonHolder {
        	// 在内部类中声明并初始化外部类的对象
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    第一次加载 Singleton 类时不会去初始化 INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder时并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

    静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

    打破单例模式以及问题解决

    1. 序列化和反序列化的过程会打破单例模式
    public class Singleton implements Serializable {
    
        //私有构造方法
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    public class Test {
        public static void main(String[] args) throws Exception {
            //往文件中写对象
            //writeObject2File();
            //从文件中读取对象
            Singleton s1 = readObjectFromFile();
            Singleton s2 = readObjectFromFile();
    
            //判断两个反序列化后的对象是否是同一个对象
            System.out.println(s1 == s2);
        }
    	// 从文件中读取数据
        private static Singleton readObjectFromFile() throws Exception {
            //创建对象输入流对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\demo.txt"));
            //第一个读取Singleton对象
            Singleton instance = (Singleton) ois.readObject();
    		//释放资源
            ois.close();
            return instance;
        }
    	// 向文件中写入数据
        public static void writeObject2File() throws Exception {
            //获取Singleton类的对象
            Singleton instance = Singleton.getInstance();
            //创建对象输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\demo.txt"));
            //将instance对象写出到文件中
            oos.writeObject(instance);
            //释放资源
            oos.close();
        }
    }
    
    • 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

    返回的false。

    想要解决这个问题只需要在 Singleton 类中添加 readResolve() 方法。在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

    public class Singleton implements Serializable {
    
        //私有构造方法
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
        
    
        // 下面是为了解决序列化反序列化破解单例模式
        // 当进行反序列化时,会自动调用这个方法,将该方法的返回值直接返回
        private Object readResolve() {
            return SingletonHolder.INSTANCE;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 反射打破单例模式
    public class Singleton {
    
        //私有构造方法
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    public class Test {
        public static void main(String[] args) throws Exception {
            //获取Singleton类的字节码对象
            Class clazz = Singleton.class;
            //获取Singleton类的私有无参构造方法对象
            Constructor constructor = clazz.getDeclaredConstructor();
            //取消访问检查
            constructor.setAccessible(true);
    
            //创建Singleton类的对象s1
            Singleton s1 = (Singleton) constructor.newInstance();
            //创建Singleton类的对象s2
            Singleton s2 = (Singleton) constructor.newInstance();
    
            //判断通过反射创建的两个Singleton对象是否是同一个对象
            System.out.println(s1 == s2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    返回的false。打破了单例模式

    解决这个问题:

    public class Singleton {
    	private static boolean flag = false;
    	// 加锁,为了并发安全
    	synchronized(Singleton.class) {
    	    //私有构造方法
    	    private Singleton() {
    	        // 反射破解单例模式需要添加的代码
    	        if(flag) {
    	            throw new RuntimeException("不能创建多个对象");
    	        }
    	        flag = true;
    	    }
    	 }
        
        private static volatile Singleton instance;
    
        //对外提供静态方法获取该对象
        public static Singleton getInstance() {
    
            if(instance != null) {
                return instance;
            }
    
            synchronized (Singleton.class) {
                if(instance != null) {
                    return instance;
                }
                instance = new Singleton();
                return instance;
            }
        }
    }
    
    • 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

    在Java 当中 Runtime 就使用了单例模式。Runtime 类使用的是饿汉式(静态属性)方式来实现单例模式的。

  • 相关阅读:
    让AI玩《我的世界》
    JVM入门
    [绝对有效]axios的CORS跨域限制问题解决方法
    MySQL数据的基础语法
    java-net-php-python-jsp班导师助理业务管理信息系统计算机毕业设计程序
    Multitor:一款带有负载均衡功能的多Tor实例创建工具
    【2023,学点儿新Java-12】小结:阶段性复习 | Java学习书籍推荐(小白该读哪类Java书籍?有一定基础后,再去读哪类书籍?)
    Linux cp 命令使用介绍
    skywalking
    《Star Atlas》 Game Jam 来啦!
  • 原文地址:https://blog.csdn.net/weixin_45970271/article/details/126311305