zoukankan      html  css  js  c++  java
  • 设计模式:如何优雅地手写单例模式

    单例模式是一种常用的设计模式,该模式提供了一种创建对象的方法,确保在程序中一个类最多只有一个实例。

    单例有什么用处?

    有一些对象其实我们只需要一个,比如线程池、缓存、对话框、处理偏好设置和注册表的对象、日志对象,充当打印机、显示等设备的驱动程序对象。其实,这类对象只能有一个实例,如果制造出来多个实例,就会导致许多问题,如:程序的行为异常、资源使用过量,或者是不一致的结果。

    Singleton通常用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统。

    在Java中实现单例模式,需要一个静态变量、一个静态方法和私有的构造器。

    经典的单例模式实现

    对于一个简单的单例模式,可以这样实现:

    1. 定义一个私有的静态变量uniqueInstance;

    2. 定义私有的构造方法。这样别处的代码无法通过调用该类的构造函数来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例;

    3. 提供一个getInstance()方法,该方法中判断是否已经存在该类的实例,如果存在直接返回,不存在则新建一个再返回。代码如下:

    public class Singleton{
        private static Singleton uniqueInstance;//私有静态变量
        
        //私有的构造器。这样别处的代码无法通过调用该类的构造函数来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
        private Singleton(){}
        
        //静态方法
        public static Singleton getInstance(){
            //如果不存在,利用私有构造器产生一个Singleton实例并赋值到uniqueInstance静态变量中。
            //如果我们不需要这个实例,他就永远不会产生。这叫做“延迟实例化(懒加载)“
            if(uniqueInstance == null){
                uniqueInstance = new Singleton();
            }
            return uniqueInstance;
        }
    }
    

    这段代码使用了延迟实例化,在单线程中没有任何问题。但是在多线程环境下,当有多个线程并行调用 getInstance(),都认为uniqueInstance为null的时候,就会调用uniqueInstance = new Singleton();,这样就会创建多个Singleton实例,无法保证单例。

    解决多线程环境下的线程安全问题,主要有以下几种写法:

    同步getInstance()方法

    关键字synchronized可以保证在他同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。

    同步getInstance()方法是处理多线程最直接的做法。只要把getInstance()变成同步(synchronized)方法,就可以解决并发问题了。

    public class Singleton{
        private static Singleton uniqueInstance;//私有静态变量
    
        //私有构造器
        private Singleton() {}
        
        //synchronized同步方法
        public static synchronized Singleton getInstance(){
            if(uniqueInstance == null){
                uniqueInstance = new Singleton();
            }
            return uniqueInstance;
        }
    }
    

    但是,同步的效率低,会降低性能。只有第一次执行此方法的时候,才真正需要同步。也就是说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种累赘。同步getInstance()方法既简单又有效。如果说对性能要求不高,这样就可以满足要求。

    “急切”实例化

    之前的实现采用的是懒加载方式,也就是说,当真正用到的时候才会创建;如果没被使用到,就一直不会创建。

    懒加载方式在第一次使用的时候, 需要进行初始化操作,可能会比较耗时。

    如果确定一个对象一定会使用的话,可以采用“急切”地实例化,事先准备好这个对象,需要的时候直接使用就行了。这种方式也叫做饿汉模式。具体代码:

    public class Singleton{
        //在静态初始化器中创建单例,保证了线程安全性
        private static Singleton uniqueInstance = new Singleton();
        
        private Singleton() {}
        
        public static Singleton getInstance(){
            return uniqueInstance;
        }
    }
    

    饿汉模式是如何保证线程安全的?

    饿汉模式中的静态变量是随着类加载时被初始化的。static关键字保证了该变量是类级别的,也就是说这个类被加载的时候被初始化一次。注意与对象级别和方法级别进行区分。

    因为类的初始化是由类加载器完成的,这其实是利用了类加载器的线程安全机制。类加载器的loadClass方法在加载类的时候使用了synchronized关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。

    双重检查加锁

    杀鸡用牛刀。实现单例模式可以利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,“才”进行同步。这样,只有第一次会同步。

    public class Singleton{
        //使用volatile关键字,确保当uniqueInstance变量被初始化成为Singleton实例时,多线程可以正确地处理uniqueInstance变量。
        private volatile static Singleton uniqueInstance;
        
        private Singleton() {}
        
        public static Singleton getInstance() {
            if(uniqueInstance == null){//第一次检查
                synchronized(Singleton.class){
                    if(uniqueInstance == null){//第二次检查
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
        
    }
    

    如果性能是关注的重点,双重检查加锁可以大幅减少getInstance()的时间消耗成本。

    在Java 1.5发行版本之前,双重检查模式的功能很不稳定,因为volatile修饰符的语义不够强,难以支持它。Java 1.5发行版本中引入的内存模式解决了这个问题,如今,双重检查模式是延迟初始化的一个实例域的方法。

    为什么要进行双重检查?只检查一次不行吗?

    解答:只检查一次不行。只检查一次的代码如下:

         if(uniqueInstance == null){//第一次检查
                synchronized(Singleton.class){
                        uniqueInstance = new Singleton();
                }
            }
    

    当两个线程同时判断uniqueInstance == null的时候,都会去获得Singleton.class的锁对象,由于两个线程拥有的锁对象是同一个Singleton.class,两个线程先后执行,也就是两个线程都会进入同步代码块创建一个新的对象,造成返回的uniqueInstance 并不是唯一的,这样也就不符合单例模式了。

    最佳方法

    从Java 1.5发行版本起,实现Singleton只需要编写一个包含单个元素的枚举类型:

    public enum Singleton {  
        INSTANCE;  
    }  
    
    

    使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。注意:如果Singleton必须拓展一个超类,而不是扩展Enum的时候,则不宜使用这个方法。

    参考

    1. Eric Freeman;ElElisabeth Freeman.HeadFirst设计模式[M]. 北京:中国电力出版社, 2007.
    2. Joshua Bloch.Effective Java中文版(原书第3版)[M]. 北京:机械工业出版社, 2018.
    3. 漫话:如何给女朋友解释什么是单例模式?
  • 相关阅读:
    【视频开发】【CUDA开发】英伟达CUVID硬解,并通过FFmpeg读取文件
    【视频开发】【CUDA开发】英伟达CUVID硬解,并通过FFmpeg读取文件
    【视频开发】ffmpeg 的编译选项
    【视频开发】ffmpeg 的编译选项
    【视频开发】Nvidia硬解码总结
    【视频开发】 ffmpeg支持的硬解码接口
    【视频开发】 ffmpeg支持的硬解码接口
    【视频开发】【CUDA开发】FFMPEG硬件加速-nvidia方案
    【视频开发】【CUDA开发】FFMPEG硬件加速-nvidia方案
    【视频开发】【CUDA开发】ffmpeg nvenc编码
  • 原文地址:https://www.cnblogs.com/sgh1023/p/10752592.html
Copyright © 2011-2022 走看看