• 设计模式(二)-创建者模式(1)-单例模式


    概述

    为何需要单例模式(Singleton Pattern)?

    在程序运行当中,我们只希望一个类只能创建一个对象,在多个地方可以公用这个唯一的对象。

    特点: 必须保证类只能创建一个对象。

    单例模式可分为:饿汉式和懒汉式

    1)饿汉式
    特点: 类加载时(程序一开始运行时),该单例对象就被创建。
    (所谓饿,饿肚子就马上干饭,所以程序一运行,就会被创建。)
    优点: 没有出现线程安全问题
    缺点: 浪费内存空间。如果创建一个非常大的实例,到程序结束都没有被使用到,就浪费了资源。同时也会导致程序运行很慢,尤其在程序启动的时候。

    2)懒汉式:
    特点: 类加载不会创建单例对象,而是在程序运行中首次使用到单例对象时,才会开始被创建。
    (所谓懒,不需要时不会创建。需要时才去检查有没有实例化,如果有就会返回实例对象,没有则会创建一个。)
    缺点: 线程不安全。在程序运行中,可能会存在多个线程轮询干活。如果有多个线程同时判断是否有单例对象,如果都判断没有,就同时创建了多个对象。

    1、饿汉式(静态变量)
        public class Singleton
        {
            private Singleton() { }
            private static Singleton instance = new Singleton();
            public static Singleton getInstance(){ return instance;}
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    2、饿汉式(静态代码块)
    //通过静态代码块(C# 不支持)
        public class Singleton
        {
            private Singleton() { }
            private static Singleton instance;
            static { instance = new Singleton();
            public static Singleton geetInstance() { return instance; }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    3、懒汉式(线程不安全)
    public class Singleeton
        {
            private Singleeton() { }
            private static Singleeton instance;
            public static Singleeton getInstance
            {
                get
                {
                    if (instance == null)
                    {
                        instance = new Singleeton();
                    }
                    return instance;
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    缺点: 如果有多个线程同时访问单例对象,会可能存在上一个线程还没有创建好对象时,而下一个线程因为判断该对象为空就重复创建一个对象。

    4、懒汉式(线程安全)
       public class Singleeton
        {
            private Singleeton() { }
            private static Singleeton instance;
            private static object lockobj = new object();
    
            public static Singleeton getInstance()
            {
            //加锁的作用:保证创建唯一的单例对象
            //缺点:当单例对象创建好后,仍会导致其他线程进行等待抢锁,而不是直接返回该对象。
                lock(lockobj)
                {
                    if (instance == null)
                    {
                        instance = new Singleeton();
                    }
                }
                return instance;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    优点: 解决了多个线程重复创建对象的问题。因为在单例对象判断是否存在前加了把锁,说明需要等待上一个线程创建好单例对象了,才能把执行权分配给下一个线程去执行 if 代码块。

    缺点: 影响性能。如果单例对象已经创建好,其他线程为了使用该单例对象时,仍然会进行排队抢锁,从而增加了抢锁的时间,而不是直接返回该对象。

    5、懒汉式(双重检查锁)
        public class Singleeton
        {
            private Singleeton() { }
            private static Singleeton instance;
            private static object lockobj = new object();
    
            public static Singleeton getInstance()
            {
            //如果存在单例对象就直接返回该对象,避免浪费排队强锁的时间。
                if (instance == null)
                {
                    lock (lockobj)
                    {
                        if (instance == null)
                        {
                            instance = new Singleeton();
                        }
                    }
                }
    
                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

    还存在缺点: 在代码执行过程中,会存在指令的执行顺序问题。即指令的执行顺序并不一定会按照我们编写的顺序执行。也就说,系统可能会存在对指令重排序问题。所以这会导致程序不一定能及时拿到单例对象变量(instance)的最新值。这样即使能拿到不为空的 instance 变量,也不确保这个变量是否完全被创建好。

    6、懒汉式(双重检查锁改进)
     public class Singleeton
        {
            private Singleeton() { }
            //改进:在双重检查锁的代码中,只在 instance 变量中加了修饰符 volatile.
            private static volatile Singleeton instance;
            private static object lockobj = new object();
            //....
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    为何 instance 变量 需要使用 volatile 修饰符?

    如果 instance 变量去掉修饰符 volatile 的话,就不能保证该代码执行的正确性。因为instance = new SIngleton();这行代码并不是原子操作。(原子操作:不会被线程调度机制打断的操作,中间的执行不会切换到另一个线程来操作,也就是new 单例变量和单例对象具体创建的过程并不是由同一个线程执行的)。

    打个比方:骑手送外卖。他一般工作的顺序就是送餐到达地点后,然后联系顾客,最后把餐丢到顾客嘴里。但是有时候呢,他可能因为其他事情如赶时间等原因,还没到送达点,就先联系顾客说外卖到了。结果就是顾客开门没看到外卖,就一直张着嘴,结果吃的是西北风。
    如果顾客在APP上设置了通知功能,当骑手真的到送达点了就马上通知顾客。

    这个例子,就相当于:

    • 多线程:顾客拿餐、骑手送外卖过程、外卖中途有其他事情等。
    • instance = new instance():顾客用餐。
    • 创建Instance:骑手送餐、联系顾客,送达这一过程。
    • 指令重排序:骑手中途先联系顾客,再到送达点。
    • volatile:一旦骑手到了送达点,APP就会马上通知顾客。否则,就继续等待用餐。

    初始化对象的顺序问题
    Java 有JVM,同样的C#也会存在相似的运行环境,即 CLR。
    在运行环境中,对类对象创建时会存在三个阶段执行:

    • 1)为变量分配内存
    • 2)初始化变量
    • 3)将变量指向分配的内存空间

    如果系统存在重排序,就可能会出现执行顺序问题。也就是说,系统可能存在先执行第三步后执行第二步,也可能会正常按顺序执行。前者的执行顺序,会导致变量还没有初始化完成,其他线程就已经判断了该变量值不为空,然后返回一个没有初始化完成的单例对象。

    volatile的作用
    (1)保证并发编程的可见性(不保证原子性)
    单线程时重排序无影响,但是多线程就有可能会读取到脏数据,这就需要用 volatile。使用 volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值。

    (2)禁止指令重排序
    用 volatile 修饰保证执行顺序。

  • 相关阅读:
    【深度学习 论文篇 02-1 】YOLOv1论文精读
    HR常用的人才测评工具,测什么,怎么测?
    Git命令图
    Jmeter —— jmeter利用取样器中http发送请求
    java-php-python-ssm校园驿站计算机毕业设计
    尚硅谷SpringBoot3笔记
    java计算机毕业设计租房管理系统源码+数据库+系统+lw文档+mybatis+运行部署
    使用纯 CSS 实现超酷炫的粘性气泡效果
    【Python】Numpy生成坐标网格
    C++之分水岭——类和对象【上】
  • 原文地址:https://blog.csdn.net/chen1083376511/article/details/134489335