• 设计模式学习笔记(三):单例模式


    1 概述

    1.1 引言

    很多时候为了节约系统资源,需要确保系统中某个类只有一个唯一的实例,当这个唯一实例创建了之后,无法再创建一个同类型的其他对象,所有的操作只能基于这一个唯一实例。这是单例模式的动机所在。

    比如Windows的任务管理器,可以按Ctrl+Shift+Esc启动,而且启动一个,不能启动多个。

    1.2 定义

    单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

    单例模式是一种对象创建型模式。

    1.3 结构图

    在这里插入图片描述

    1.4 角色

    单例模式只有一个角色:

    • Singleton(单例角色):在单例类的内部只生成一个实例,同时它提供一个类似名叫getInstance的静态方法获取实例,同时为了防止外部生成新的实例化对象,构造方法可见性为private,在单例类内部定义了一个Singleton的静态对象,作为供外部访问的唯一实例

    2 典型实现

    2.1 步骤

    • 构造函数私有化:也就是禁止外部直接使用new等方式创建对象
    • 定义静态成员:定义一个私有静态成员保存实例
    • 增加公有静态方法:增加一个类似getInstance()的公有静态方法来获取实例

    2.2 单例角色

    单例角色通常实现如下:

    class Singleton
    {
    	//饿汉式实现
        private static Singleton instance = new Singleton();
        private Singleton(){}
        public static Singleton getInstance()
        {
            return instance;
        }
    }
    

    2.3 客户端

    客户端直接通过该类获取实例即可:

    Singleton singleton = Singleton.getInstance();
    

    3 实例

    某个软件需要使用一个全局唯一的负载均衡器,使用单例模式对其进行设计。

    代码如下:

    public class LoadBalancer
    {
        private static LoadBalancer instance = null;
    
        private LoadBalancer(){}
    
        public static LoadBalancer getInstance()
        {
            return instance == null ? instance = new LoadBalancer() : instance;
        }
    
        public static void main(String[] args) {
            LoadBalancer balancer1 = LoadBalancer.getInstance();
            LoadBalancer balancer2 = LoadBalancer.getInstance();
            System.out.println(balancer1 == balancer2);
        }
    }
    

    这是最简单的单例类的设计,获取实例时仅仅判断是否为null,没有考虑到线程问题。也就是说,多个线程同时获取实例时,还是有可能会产生多个实例,一般来说,常见的解决方式如下:

    • 饿汉式单例
    • 懒汉式单例
    • IoDH

    4 饿汉式单例

    饿汉式单例就是在普通的单例类基础上,在定义静态变量时就直接实例化,因此在类加载的时候就已经创建了单例对象,而且在获取实例时不需要进行判空操作直接返回实例即可:

    public class LoadBalancer
    {
        private static LoadBalancer instance = new LoadBalancer();
    
        private LoadBalancer(){}
    
        public static LoadBalancer getInstance()
        {
            return instance;
        }
    }
    

    当类被加载时,静态变量instance被初始化,类的私有构造方法将被调用,单例类的唯一实例将被创建。

    5 懒汉式单例

    懒汉式单例在类加载时不进行初始化,在需要的时候再初始化,加载实例,同时为了避免多个线程同时调用getInstance(),可以加上synchronized

    public class LoadBalancer
    {
        private static LoadBalancer instance = null;
    
        private LoadBalancer(){}
    
        synchronized public static LoadBalancer getInstance()
        {
            return instance == null ? instance = new LoadBalancer() : instance;
        }
    }
    

    这种技术又叫延迟加载技术,尽管解决了多个线程同时访问的问题,但是每次调用时都需要进行线程锁定判断,这样会降低效率。

    事实上,单例的核心在于instance = new LoadBalancer(),因此只需要锁定这行代码,优化如下:

    public static LoadBalancer getInstance()
    {
    	if(instance == null)
    	{
    		synchronized (LoadBalancer.class)
    		{
    			instance = new LoadBalancer();
    		}
    	}
    	return instance;
    }
    

    但是实际情况中还是有可能出现多个实例,因为如果A和B两个线程同时调用getInstance(),都通过了if(instance == null)的判断,假设线程A先获得锁,创建实例后,A释放锁,接着B获取锁,再次创建了一个实例,这样还是导致产生多个单例对象。

    因此,通常采用一种叫“双重检查锁定”的方式来确保不会产生多个实例,一个线程获取锁后再进行一次判空操作:

    private volatile static LoadBalancer instance = null;
    public static LoadBalancer getInstance()
    {
    	if(instance == null)
    	{
    		synchronized (LoadBalancer.class)
    		{
    			if(instance == null)
    			{
    				instance = new LoadBalancer();
    			}
    		}
    	}
    	return instance;
    }
    

    需要注意的是要使用volatile修饰变量,volatile可以保证可见性以及有序性。

    6 饿汉式与懒汉式的比较

    • 饿汉式在类加载时就已经初始化,优点在于无需考虑多线程访问问题,可以确保实例的唯一性
    • 从调用速度方面来说饿汉式会优于懒汉式,因为在类加载时就已经被创建
    • 从资源利用效率来说饿汉式会劣于懒汉式,因为无论是否需要使用都会加载单例对象,而且由于加载时需要创建实例会导致类加载时间变长
    • 懒汉式实现了延迟加载,无须一直占用系统资源
    • 懒汉式需要处理多线程并发访问问题,需要双重检查锁定,且通常来说初始化过程需要较长时间,会增大多个线程同时首次调用的几率,这会导致系统性能受一定影响

    7 IoDH

    为了克服饿汉式不能延迟加载以及懒汉式的线程安全控制繁琐问题,可以使用一种叫Initialization on Demand Holder(IoDH)的技术。实现IoDH时,需在单例类增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,代码如下:

    public class LoadBalancer
    {
    	private LoadBalancer(){}
    	private static class HolderClass
    	{
    		private static final LoadBalancer instance = new LoadBalancer();
    	}
    	
    	public static LoadBalancer getInstance()
    	{
    		return HolderClass.instance;
    	}
    }
    

    由于单例对象没有作为LoadBalancer的成员变量直接实例化,因此类加载时不会实例化instance。首次调用getInstance()时,会初始化instance,由JVM保证线程安全性,确保只能被初始化一次。另外相比起懒汉式单例,getInstance()没有线程锁定,因此性能不会有任何影响。

    通过IoDH既可以实现延迟加载,又可以保证线程安全,不影响系统性能,但是缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH。另外,还可能引发NoClassDefFoundError(当初始化失败时),例子可以戳这里

    8 枚举实现单例(推荐)

    其中,无论是饿汉式,还是懒汉式,还是IoDH,都有或多或少的问题,并且还可以通过反射以及序列化/反序列化方式去“强制”生成多个单例,有没有更优雅的解决方案呢?

    有!答案就是枚举。

    代码如下:

    public class Test
    {
        public static void main(String[] args) {
            LoadBalancer balancer1 = LoadBalancer.INSTANCE;
            LoadBalancer balancer2 = LoadBalancer.INSTANCE;
            System.out.println(balancer1 == balancer2);
        }
    }
    
    enum LoadBalancer{
        INSTANCE;
    }
    

    使用枚举实现单例优点如下:

    • 代码简洁不易出错
    • 无须像饿汉式一样直接在类加载时初始化
    • 也无须像懒汉式一样需要双重检查锁定
    • 也无须像IoDH一样添加一个静态内部类增加系统中类的数量
    • 由JVM保证线程安全
    • 不会因为序列化生成新实例
    • 也不会因为反射生产新实例

    9 主要优点

    • 唯一实例:单例模式提供了对唯一实例的受控访问,可以严格控制客户怎样以及何时访问它
    • 节约资源:由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式可以提高系统性能

    10 主要缺点

    • 扩展困难:没有抽象层,扩展困难
    • 职责过重:单例类职责过重,一定程度上违反了SRP,因为既提供了业务方法,也提供了创建对象方法,将对象创建以及对象本身的功能耦合在一起
    • GC导致重新实例化:很多语言提供了GC机制,实例化的对象长时间不使用将被回收,下次使用需要重新实例化,这回导致共享的单例对象状态丢失

    11 适用场景

    • 系统需要一个实例对象
    • 客户调用类的单个实例只允许使用一个公共访问点

    12 总结

    在这里插入图片描述

    如果觉得文章好看,欢迎点赞。

    同时欢迎关注微信公众号:氷泠之路。

    在这里插入图片描述

  • 相关阅读:
    专访京东孙海波:大牛架构师养成记及电商供应链中区块链技术的应用(转)
    Python3 使用 matplotlib 画折线图
    JavaSE(十)之Map总结 (转)
    SciPy 安装不上?
    AI 也开源:50 大开源 AI 项目 (转)
    RabbitMQ的应用场景以及基本原理介绍 【转】
    mysql的水平拆分和垂直拆分 (转)
    MySQL在线大表DDL操作 (转)
    如何在原生工程中引入Cordova工程-for iOS 【转】
    Android 与 js 简单互调
  • 原文地址:https://www.cnblogs.com/6b7b5fc3/p/13287731.html
Copyright © 2020-2023  润新知