• 设计模式-单例


    什么是单例

    单例,顾名思义,就是单个实例,也就是说,某个类如果实现了单例模式,那这个类就只能生成一个实例。

    为什么使用单例

    在一些应用中,可能会有一些工具类,这些类为其他类服务,本身没有太多数据要保存。如果使用这样的类的时候,每次都用new创建一个对象的话,会增加系统的开销。在实际设计时,这种只需要一个实例对象的类就应该设计为单例模式。
    另外,比如:线程池、缓存、网络请求等。当这类对象有多个实例时,程序就可能会出现异常,比如:程序出现异常行为、得到的结果不一致等,这时候也应该使用单例模式。

    如何实现单例

    关键点:

    • 构造函数设置为 private ,这避免外部通过 new 创建实例。
    • 通过一个静态方法或者枚举返回单例类对象。
    • 考虑对象创建时的线程安全问题,确保单例类的对象有且仅有一个,尤其是在多线程环境下。
    • 确保单例类对象在反序列化时不会重新构建对象。
    • 考虑是否支持延迟加载。

    几种常见单例模式的实现方式

    饿汉式

    在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。

    public class Singleton {
        // 静态字段引用唯一实例:
        private static final Singleton INSTANCE = new Singleton();
    
        // 通过静态方法返回实例:
        public static Singleton getInstance() {
            return INSTANCE;
        }
    
        // private构造方法保证外部无法实例化:
        private Singleton() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    或者 直接把 static变量暴露给外部:

    public class Singleton {
        // 静态字段引用唯一实例:
        public static final Singleton INSTANCE = new Singleton();
    
        // private构造方法保证外部无法实例化:
        private Singleton() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    懒汉式

    懒汉式相对于饿汉式的优势是支持延迟加载。但它的缺点也很明显,getInstance 使用了 synchronize 实现线程同步,导致这个方法的并发很低,每次调用都会频繁的枷锁、释放锁,会导致性能瓶颈。

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

    双重检测

    饿汉式不能延时加载,懒汉式有性能问题,而双重检测方式既支持延迟加载、又支持高并发的单例实现方式。

    当 instance 对象被创建后,再次调用 getInstance 方法不再会进入 synchronize 加锁的代码之中。

    它的优点是:资源利用率高,第一次执行 getInstance 时才会被实例化,效率高。缺点是:第一次加载反应稍慢。

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

    有时候,面试官会问这种实现方式有什么问题。他们指的就是指令重排序。

    instance = new Singleton(); 并不是一个原子操作, 这句代码实际执行了三件事。

    1、 给 Singleton 的实例分配内存;
    2、调用 Singleton 的构造函数,初始化成员变量;
    3、将 instance 的对象指向分配的内存空间。

    因为 Java 编译器允许处理器乱序执行,2、3的顺序是无法保证的。如果是 1-3-2 执行的顺序,当执行完 3 、2未执行之前,被切换到 B 线程,此时 instance 已经非空,B 会直接取走 instance,在使用时就会出错。

    这就是指令重排。

    解决办法也很简单:只需要给 instance 成员变量加上 volatile 关键字,就可以禁止指令重排序。

    其实这个问题在高版本的 java 中已经被解决了,解决方式也很简单,就是把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序。

    静态内部类

    使用 Java 的静态内部类也能够实现。

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

    当第一次加载 Singleton 类时并不会初始化 instance,只有在第一次调用 Singleton 的 getInstance 方法时才会导致 instance 被初始化。
    第一次调用 getInstance 方法时会导致虚拟机加载 Instance 类,这种方式不仅能保证线程安全,也能够保证单例对象唯一,同时也延迟了单例的实例化。

    枚举

    另一种实现Singleton的方式是利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:

    public enum World {
        // 唯一枚举:
    	INSTANCE;
    
    	private String name = "world";
    
    	public String getName() {
    		return this.name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:
    String name = World.INSTANCE.getName();

    使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

    什么时候使用单例

    那我们什么时候应该用Singleton呢?实际上,很多程序,尤其是Web程序,大部分服务类都应该被视作Singleton,如果全部按Singleton的写法写,会非常麻烦,所以,通常是通过约定让框架(例如Spring)来实例化这些类,保证只有一个实例,调用方自觉通过框架获取实例而不是new操作符:

    @Component // 表示一个单例组件
    public class MyService {
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    Mac上多版本JDK安装和管理
    C# Onnx GFPGAN GPEN-BFR 人像修复
    如何使用Python将PDF转为图片
    在 VMware vSphere 中构建 Kubernetes 存储环境
    绿色低碳,数字为先:万应低代码推动能源资产管理优化
    【微信自动化】使用c#实现微信自动化
    【Jmeter+Influxdb+Grafana性能监控平台安装与部署】
    基于JAVA医患辅助系统计算机毕业设计源码+系统+数据库+lw文档+部署
    implementation file-视频系统
    什么是RDB和AOF?
  • 原文地址:https://blog.csdn.net/quyingzhe0217/article/details/134437672