• 多线程应用——单例模式


    单例模式

    一.什么是单例模式

    单例模式(Singleton Pattern)顾名思义,在程序中一个类只有一个对象实例。例如我们在JDBC编程中,我们创建了一个简单类DataSource,只要从DataSource中获取数据库连接即可,不用创建多个DataSource对象。

    单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

    二.如何实现

    1.口头实现

    2.利用语法特性

    • 本质上就是利用编程语言自身的特性,强行限制某个类不能创建多个实例
    • static修饰一个变量后,这个变量就从一个普通的成员变量属性变成了类对象的成员变量
    • 在JVM中一个类只要一个类对象,从而保证了static变量的唯一性

    三.实现方式(饿汉式+懒汉式)

    1.饿汉式

    public class SingletonHungry{
        //类的成员变量
        private static Singleton instance=new Singleton();
        //私有化构造方法
        private SingletonHungry(){ }
        
        /**
         * 对外获取类成员方法
         * @return
         */
        public static SingletonHungry getInstance(){
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    饿汉式:需要急迫的创建这个实例,类在加载的过程中就创建出来了

    描述:这种方式比较常见,但容易产生垃圾对象

    • 优点:没有加锁,执行效率高
    • 缺点:类加载时就初始化,浪费内存

    2.懒汉式

    public class SingletonLazy{
        //类的成员变量
        private static Singleton instance=null;
        //私有化构造方法
        private Singleton(){ }
        
        /**
         * 对外获取类成员方法
         * @return
         */
        public static Singleton getInstance(){
            //判断一个需要返回的对象是否为空
            if (instance==null){
                //创建对象
                instance=new SingletonLazy();
            }
            //返回单例对象
            return instance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    懒汉式:什么时候用什么时候才去创建,不要程序启动的时候创建,从而节省了程序启动时的开销

    3.线程安全的单例模式

    在多线程中,饿汉式只是获取变量而不是修改变量;而懒汉式是修改共享变量,因此存在线程安全问题。

    我们用上面的代码做一测试

    public class Demo_SingletonLazy {
        public static void main(String[] args) {
            //多个线程获取单例对象
            for (int i = 0; i < 10; i++) {
                Thread thread = new Thread(() -> {
                    SingletonLazy instance = SingletonLazy.getInstance();
                    System.out.println(instance);
                });
                thread.start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    image-20230830183534647

    我们知道造成线程安全问题的原因有 原子性、内存可见性、有序性

    image-20230830185305419

    通过上图分析得出问题:不满足原子性,那该如何解决呢,当然是加锁。

    public class SingletonLazy{
        //类的成员变量
        private static Singleton instance=null;
        //私有化构造方法
        private Singleton(){ }
        
        /**
         * 对外获取类成员方法
         * @return
         */
        public static Singleton getInstance(){
            synchronized(SingletonLazy.class){
                //判断一个需要返回的对象是否为空
            	if (instance==null){
                	//创建对象
                	instance=new SingletonLazy();
            	}
            }
            //返回单例对象
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    image-20230830185941493

    加锁之后,我们看到问题也解决了,但此时还有一个非常严重的问题:效率问题

    1. 当变量没有初始化时,第一次创建可能会出现线程问题,因为多个线程可能创建实例
    2. 当实例变量被创建后,new操作将永远不会执行了,因为获取到的实例不为null了
    3. 那么synchronized的锁就没有必要加了,因为实例已经创建好了,之后线程拿到锁之后只是判断一下实例是否为空,不会去new了,如果不为null就什么也不干就把锁释放了,这样一来锁白加了,资源也白白浪费了

    synchronizeed看上去是一个关键字,可能会涉及到用户态–>内核态之间的切换,这个成本是比较高的,我们为了保证程序正确执行的基础可以承担这个成本,但是没有必要做无用的消耗

    4.双重检查锁

    既然在第一次创建完实例后加锁是为了判断实例是否为空,那么不如将判断为空放到加锁之前,避免因为上述原因而造成资源浪费

    public class SingletonDCL {
    
        //定义一个类的成员变量
        private static SingletonDCL instance=null;
    
        private SingletonDCL(){}
    
        public static SingletonDCL getInstance(){
            //第一层判断是否需要加锁
            if (instance==null){
                synchronized (SingletonDCL.class){
                    //第二层加锁判断是否需要创建对象
                    if (instance==null){
                        //创建对象
                        instance=new SingletonDCL();
                    }
                }
            }
            //返回单例对象
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    5.禁止指令重排序

    上述代码还存在一个严重问题,那就是指令重排序问题

    假设一个线程在调用getInstande()方法时,拿到了锁,进入了第二层开始new对象:

    new对象本质分为三步:

    1. 申请内存空间
    2. 调用构造方法,初始化实例
    3. 把内存首地址赋给对象的引用

    可以看出1和3有逻辑关系,2是在这个内存空间里填充数据

    如果这里指令重排序,造成执行顺序为1 3 2 那么这个时候又有一个线程执行到第一层的判断,这里的instance就不为空了,返回一个没有完成初始化的对象。这种情况也是很危险的

    为了防止指令重排序,给变量加入关键字volatile

    public class SingletonDCL {
    
        //定义一个类的成员变量
        private static volatile SingletonDCL instance=null;//禁止指令重排序,也保证了在对共享变量修改时的内存可见性
    
        private SingletonDCL(){}
    
        public static SingletonDCL getInstance(){
            //第一层判断是否需要加锁
            if (instance==null){
                synchronized (SingletonDCL.class){
                    //第二层加锁判断是否需要创建对象
                    if (instance==null){
                        //创建对象
                        instance=new SingletonDCL();
                    }
                }
            }
            //返回单例对象
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    看完留个三连吧

  • 相关阅读:
    计算机毕业设计Java客户关系管理平台(源码+mysql数据库+系统+lw文档)
    学习笔记4--自动驾驶汽车感知系统
    【进阶版】机器学习之EM经典算法原理+代码(11)
    SpringMVC学习篇(十一)
    An error occurred.Faithfully yours, nginx
    算法之美阅读笔记
    ESP8266-Arduino编程实例-74HC595位移寄存驱动
    两地三中心部署
    前端技术(16) : 插件集合
    Java面试题之——异常和错误
  • 原文地址:https://blog.csdn.net/weixin_60781793/article/details/132589075