• 手写单例模式


    手写单例模式

    为什么要有单例模式

    实际编程应用场景中,有一些对象其实我们只需要一个,比如线程池对象、缓存、系统全局配置对象等。这样可以就保证一个在全局使用的类不被频繁地创建与销毁,节省系统资源。

    实现单例模式的几个要点

    首先要确保全局只有一个类的实例。
    要保证这一点,至少类的构造器要私有化。
    单例的类只能自己创建自己的实例。
    因为,构造器私有了,但是还要有一个实例,只能自己创建咯!
    单例类必须能够提供自己的唯一实例给其他类
    就是要有一个公共的方法能返回该单例类的唯一实例。

    具体实现:

    1.饿汉式:

    天然线程安全:类加载时就创建了实例,占用内存空间

    public class Singleton{
        //static变量,全局唯一
        private static Singleton instance=new Singleton();
        //私有构造器:外部无法创建实例
        private Singleton(){};
        //公有的方法,对外提供唯一的实例
        public static Singleton getInstance(){
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.懒汉式

    为方法加上锁:线程安全,但是这种做法有一个缺点,不管是不是已经存在实例了,都会被锁阻塞。效率低

    public class Singleton{
        private static Singleton instance;
        private Singleton(){};
        public static synchronized Single getInstance(){
            if(instance==null){
                
                 instance=new Singleton();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    双重校验锁(DCL):线程安全,效率高

    public class Singleton{
       //volatile防止指令重排序,内存可见(缓存中的变化及时刷到主存,并且其他的内存失效,必须从主存获取)
        private volatile static Singleton instance;
        private Singleton(){
            //构造器必须私有  不然直接new就可以创建
        };
        public static Single getInstance(){
            //第一次判断,假设会有好多线程,如果instance没有被实例化,那么就会到下一步获取锁,只有一个能获取到,
            //如果已经实例化,那么直接返回了,减少除了初始化时之外的所有锁获取等待过程
            if(instance==null){
                synchronized(Singleton.class){
                    //第二次判断是因为假设有两个线程A、B,两个同时通过了第一个if,然后A获取了锁,进入然后判断instance是null,他就实例化了instance,然后他出了锁,
                    //这时候线程B经过等待A释放的锁,B获取锁了,如果没有第二个判断,那么他还是会去new Singleton(),再创建一个实例,所以为了防止这种情况,需要第二次判断
                    if(instance==null){
                        //下面这句代码其实分为三步:
                        //1.开辟内存分配给这个对象
                        //2.初始化对象
                        //3.将内存地址赋给虚拟机栈内存中的instance变量
                        //注意上面这三步,第2步和第3步的顺序是随机的,这是计算机指令重排序的问题
                        //假设有两个线程,其中一个线程执行下面这行代码,如果第三步先执行了,就会把没有初始化的内存赋值给instance
                        //然后恰好这时候有另一个线程执行了第一个判断if(instance == null),然后就会发现instance指向了一个内存地址
                        //这另一个线程就直接返回了这个没有初始化的内存,所以要防止第2步和第3步重排序
    
                       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

    从代码里可以看到,做了两重的instance == null的判断,中间还用了synchronized关键字,第一个instance == null的判断是为了避免线程串行化,如果为空,就进入synchronized代码块中,获取锁后再操作,如果不为空,直接就返回instance对象了,无需再进行锁竞争和等待了。而第二个instance == null的判断是为了防止有多个线程同时跳过第一个instance == null的判断,比如线程一先获取到锁,进入同步代码块中,发现instance实例还是null,就会做new操作,然后退出同步代码块并释放锁,这时一起跳过第一层instance == null的判断的还有线程二,这时线程一释放了锁,线程二就会获取到锁,如果没有第二层的instance == null这个判断挡着,那就会再创建一个instance实例,就违反了单例的约束了。

    DCL使用volatile关键字,是为了禁止指令重排序,避免返回还没完成初始化的instance对象,导致调用报错,也保证了线程的安全。

    了解下singleton = new Singleton()这段代码其实不是原子性的操作,它至少分为以下3个步骤:

    1. 给singleton对象分配内存空间
    2. 调用Singleton类的构造函数等,初始化singleton对象
    3. 将singleton对象指向分配的内存空间,这步一旦执行了,那singleton对象就不等于null了

    这里还需要知道一点,就是有时候JVM会为了优化,而做指令重排序的操作,这里的指令,指的是CPU层面的。

    正常情况下,singleton = new Singleton()的步骤是按照1->2->3这种步骤进行的,但是一旦JVM做了指令重排序,那么顺序很可能编程1->3->2,如果是这种顺序,可以发现,在3步骤执行完singleton对象就不等于null,但是它其实还没做步骤二的初始化工作,但是另一个线程进来时发现,singleton不等于null了,就这样把半成品的实例返回去,调用是会报错的。

    静态内部类实现:线程安全,高效

    public class Singleton {  
        private static class SingletonHolder {  
        	private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
        public static final Singleton getInstance() {  
        	return SingletonHolder.INSTANCE;  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。具体来说当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,使用INSTANCE的时候,才会导致虚拟机加载SingleTonHoler类。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

  • 相关阅读:
    助力数据中心双碳发展,存储如何变得越来越绿?
    【AI】自回归 (AR) 模型使预测和深度学习变得简单
    SUID提权教程
    CSP-J第二轮试题-2022年-1.2题
    合宙Air724UG LuatOS-Air lvgl7-lvgl(矢量字体)
    React 框架
    第三、四章-嵌入式系统程序设计、嵌入式最小系统
    springboot学习四:Spring Boot profile多环境配置、devtools热部署
    Vue插件
    html标签中crossOrigin、integrity属性详解
  • 原文地址:https://blog.csdn.net/qq_43600941/article/details/127643043