• 多线程下的单例设计模式(新手必看!!!)


    在项目中为了避免创建大量的对象,频繁出现gc的问题,单例设计模式闪亮登场。

    一、饿汉式

    1.1饿汉式

    顾名思义就是我们比较饿,每次想吃的时候,都提前为我们创建好。其实我记了好久也没分清楚饿汉式和懒汉式的区别。这里给出我的一个记忆方法:懒汉式就是懒加载,什么是懒加载呢?就是我们需要的时候给创建对象就行,稍后介绍懒汉式的时候你会发现这个现象。

    1.2饿汉式的特点

    线程安全,但是如果一个项目需要创建大量的对象的时候,当项目运行的时候,会创建大量我们暂时用不到的对象。

    1.3饿汉式代码
    package singletonModel;
    public class HungrySingleton {
        public static HungrySingleton instance=new HungrySingleton();
        private HungrySingleton(){}
        public static HungrySingleton getInstance(){
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1.4多线程下测试
    package Test;
    import singletonModel.DoubleLockSingleton;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicReference;
    public class SingletonTest {
        public static void main(String[] args) {
            // 使用AtomicReference来存储第一次获取到的LazySingleton实例
            AtomicReference<DoubleLockSingleton> singletonInstance = new AtomicReference<>();
            // 我们将启动大量线程来尝试突破单例的线程安全性
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            // 用于发现多个实例创建的标志
            AtomicReference<Boolean> flag = new AtomicReference<>(false);
            // 提交多个任务到线程池,尝试并发地获取单例实例
            for (int i = 0; i < 100; i++) {
                executorService.submit(() -> {
                    DoubleLockSingleton instance = DoubleLockSingleton.getInstance();
                    // 如果原子引用为空,我们设置当前实例
                    if (singletonInstance.get() == null) {
                        singletonInstance.set(instance);
                    } else if (singletonInstance.get() != instance) {
                        // 如果原子引用中的实例与当前获取的实例不同,说明存在多个实例
                        flag.set(true);
                        System.out.println("Detected multiple instances!");
                    }
                });
            }
            executorService.shutdown();
            // 等待所有任务完成
            while (!executorService.isTerminated()) {
                // 等待所有线程执行完毕
            }
    
            if (flag.get().equals(false)) {
                System.out.println("No multiple instances detected!");
            }
        }
    }
    
    
    • 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
    • 39
    • 40
    1.5运行结果

    在这里插入图片描述

    通过实验证明,饿汉式在多线程环境下是线程安全的!

    二、懒汉式

    2.1懒汉式

    顾名思义比较懒,叫我们的时候,我们在穿衣服去干活,即完成对象的创建的过程。

    2.2懒汉式的特点

    需要的时候,才为我们创建,能够避免在项目启动的时候,创建大量的无用对象,减少GC。缺点就是多线程操作下线程不安全!

    2.3懒汉式代码
    package singletonModel;
    public class LazySingleton {
        private static LazySingleton lazyInstance;
        private LazySingleton(){}
        public static LazySingleton getInstance(){
            if(lazyInstance==null){
                lazyInstance= new LazySingleton();
            }
            return lazyInstance;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    2.4多线程下测试
    package Test;
    import singletonModel.LazySingleton;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicReference;
    public class SingletonTest {
        public static void main(String[] args) {
            // 使用AtomicReference来存储第一次获取到的LazySingleton实例
            AtomicReference<LazySingleton> singletonInstance = new AtomicReference<>();
            // 我们将启动大量线程来尝试突破单例的线程安全性
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            // 用于发现多个实例创建的标志
            AtomicReference<Boolean> flag = new AtomicReference<>(false);
            // 提交多个任务到线程池,尝试并发地获取单例实例
            for (int i = 0; i < 100; i++) {
                executorService.submit(() -> {
                    LazySingleton instance = LazySingleton.getInstance();
                    // 如果原子引用为空,我们设置当前实例
                    if (singletonInstance.get() == null) {
                        singletonInstance.set(instance);
                    } else if (singletonInstance.get() != instance) {
                        // 如果原子引用中的实例与当前获取的实例不同,说明存在多个实例
                        flag.set(true);
                        System.out.println("Detected multiple instances!");
                    }
                });
            }
            executorService.shutdown();
            // 等待所有任务完成
            while (!executorService.isTerminated()) {
                // 等待所有线程执行完毕
            }
            if (flag.get().equals(false)) {
                System.out.println("No multiple instances detected!");
            }
        }
    }
    
    • 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

    上述代码需要多次测试,就能够测试出线程不安全的!

    2.5测试结果

    在这里插入图片描述
    测试证明懒汉式在多线程操作下是线程不安全的!

    2.6具体原因

    具体的原因就是发生在下图的位置:即多线程环境下,不知线程哪个执行快慢,即存在两个线程A,B,线程A在进入if语句的时候,判断为空,然后完成对象的创建,但是对象的创建也需要一定时间,这个时候线程B也进入if判断,当前线程A还没有创建好,则判断为null,同时也完成对象的创建,这时候线程A,B创建的对象就不是同一个对象了。也就是线程不安全的了,即不满足原子性,可见性,有序性。
    在这里插入图片描述

    三、懒汉式方案修补方案一

    为了保证线程安全,即满足原子性,可见性,有序性。我们首先想到的就是加锁!

    由于getInstance方法为static修饰的方式,我们加了synchronized后,锁住的是当前的类,即加的类锁。即多线程操作该类的时候,只有1个线程操作成功!

    3.1代码
    package singletonModel;
    
    public class RLazySingleton {
        static RLazySingleton instance;
        private RLazySingleton(){
    
        }
       synchronized public static RLazySingleton getInstance(){
            if(instance==null){
                instance=new RLazySingleton();
            }
            return instance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    3.2多线程测试代码
    package Test;
    import singletonModel.RLazySingleton;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicReference;
    
    public class SingletonTest {
        public static void main(String[] args) {
            // 使用AtomicReference来存储第一次获取到的LazySingleton实例
            AtomicReference<RLazySingleton> singletonInstance = new AtomicReference<>();
            // 我们将启动大量线程来尝试突破单例的线程安全性
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            // 用于发现多个实例创建的标志
            AtomicReference<Boolean> flag = new AtomicReference<>(false);
            // 提交多个任务到线程池,尝试并发地获取单例实例
            for (int i = 0; i < 100; i++) {
                executorService.submit(() -> {
                    RLazySingleton instance = RLazySingleton.getInstance();
                    // 如果原子引用为空,我们设置当前实例
                    if (singletonInstance.get() == null) {
                        singletonInstance.set(instance);
                    } else if (singletonInstance.get() != instance) {
                        // 如果原子引用中的实例与当前获取的实例不同,说明存在多个实例
                        flag.set(true);
                        System.out.println("Detected multiple instances!");
                    }
                });
            }
            executorService.shutdown();
            // 等待所有任务完成
            while (!executorService.isTerminated()) {
                // 等待所有线程执行完毕
            }
    
            if (flag.get().equals(false)) {
                System.out.println("No multiple instances detected!");
            }
        }
    }
    
    
    
    • 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
    • 39
    • 40
    • 41
    • 42
    3.3测试结果

    在这里插入图片描述

    实验结果证明:这种测试代码也是线程安全的!

    3.4存在的问题

    通过在getInstance()方法上添加synchronized关键字,可以强制每次只有一个线程能够访问方法,从而避免竞态条件。但这样做会影响性能,因为每次访问都需要进行同步。

    四、双重锁检测方案

    解决每次访问都需要进行同步的问题。

    4.1代码
    package singletonModel;
    public class DoubleLockSingleton {
        private static DoubleLockSingleton instance;
        private DoubleLockSingleton(){
        }
        public  static  DoubleLockSingleton getInstance(){
            if(instance==null){
                synchronized (DoubleLockSingleton.class){
                    if(instance==null){
                        instance=new DoubleLockSingleton();
                    }
                }
            }
            return instance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    4.2测试代码
    package Test;
    import singletonModel.DoubleLockSingleton;
    import singletonModel.RLazySingleton;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicReference;
    public class SingletonTest {
        public static void main(String[] args) {
            // 使用AtomicReference来存储第一次获取到的LazySingleton实例
            AtomicReference<RLazySingleton> singletonInstance = new AtomicReference<>();
            // 我们将启动大量线程来尝试突破单例的线程安全性
            ExecutorService executorService = Executors.newFixedThreadPool(100);
            // 用于发现多个实例创建的标志
            AtomicReference<Boolean> flag = new AtomicReference<>(false);
            // 提交多个任务到线程池,尝试并发地获取单例实例
            for (int i = 0; i < 100; i++) {
                executorService.submit(() -> {
                    RLazySingleton instance = RLazySingleton.getInstance();
                    // 如果原子引用为空,我们设置当前实例
                    if (singletonInstance.get() == null) {
                        singletonInstance.set(instance);
                    } else if (singletonInstance.get() != instance) {
                        // 如果原子引用中的实例与当前获取的实例不同,说明存在多个实例
                        flag.set(true);
                        System.out.println("Detected multiple instances!");
                    }
                });
            }
            executorService.shutdown();
            // 等待所有任务完成
            while (!executorService.isTerminated()) {
                // 等待所有线程执行完毕
            }
    
            if (flag.get().equals(false)) {
                System.out.println("No multiple instances detected!");
            }
        }
    }
    
    
    • 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
    • 39
    • 40
    测试结果

    实验结果也是线程安全的。
    在这里插入图片描述

    五、其他线程安全的写法

    5.1静态内部类
    public class StaticInnerClassSingleton {
        private static class LazyHolder {
            private static final StaticInnerClass INSTANCE = new StaticInnerClass();
        }
    
        private StaticInnerClass(){}
    
        public static StaticInnerClass getInstance(){
            return LazyHolder.INSTANCE;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    5.2枚举类
    package singletonModel;
    
    public enum EnumSingleton {
        Instance;
        public void getInstance(){
            System.out.println("枚举类创建对象");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    六、总结

    在Java中,使用枚举(enum)实现的单例模式是唯一能够抵御反射攻击的方式,因为枚举类型没有构造方法(在字节码层面是有私有构造器的,但这是由编译器自己添加的),所以无法通过反射来实例化枚举类型。
    枚举攻击!!!

    import java.lang.reflect.Constructor;
    
    public class ReflectionSingletonAttack {
        public static void main(String[] args) {
            Singleton instanceOne = Singleton.getInstance();
            Singleton instanceTwo = null;
    
            try {
                // 获取Singleton类的构造函数
                Constructor[] constructors = Singleton.class.getDeclaredConstructors();
                for (Constructor constructor : constructors) {
                    // 设置构造函数的访问权限为可访问
                    constructor.setAccessible(true);
                    // 使用构造函数创建一个新的Singleton实例
                    instanceTwo = (Singleton) constructor.newInstance();
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 打印两个实例的哈希码
            System.out.println("Instance 1 hash:" + instanceOne.hashCode());
            System.out.println("Instance 2 hash:" + instanceTwo.hashCode());
        }
    }
    
    
    • 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

    枚举类单例模式抵挡枚举攻击

    import java.lang.reflect.Constructor;
    
    public class EnumReflectionAttack {
        public static void main(String[] args) {
            EnumSingleton instanceOne = EnumSingleton.INSTANCE;
            EnumSingleton instanceTwo = null;
            try {
                Constructor[] constructors = EnumSingleton.class.getDeclaredConstructors();
                for (Constructor constructor : constructors) {
                    constructor.setAccessible(true);
                    instanceTwo = (EnumSingleton) constructor.newInstance();
                    break;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            System.out.println("Instance 1 hash:" + instanceOne.hashCode());
            System.out.println("Instance 2 hash:" + (instanceTwo != null ? instanceTwo.hashCode() : "instance creation failed"));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在运行此代码时,您会收到类似以下的异常:

    java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    
    • 1

    因此,使用枚举的方式创建单例是安全的,它有效地防止了反射攻击以及解决了序列化问题。这也是为什么很多推荐使用枚举方式来实现单例模式的原因之一。

  • 相关阅读:
    0分钟!搞懂计算机内存实现原理
    【爬虫】实验项目三:验证码处理与识别
    详解 DES加密技术 | 凯撒密码 | 栅栏密码
    Redis分布式锁故障,我忍不住想爆粗...
    vscode调教配置:快捷修复和格式化代码
    ChatGPT API 学习
    如何在 DigitalOcean Kubernetes 上设置 NGINX 入口控制器
    伦敦金K线图头部怎样看?
    2023年:我成了半个外包
    LeetCode 69. x 的平方根
  • 原文地址:https://blog.csdn.net/qq_42627388/article/details/133896498