深入设计模式08---单例模式

举报
老司机张师傅 发表于 2022/07/26 23:17:04 2022/07/26
【摘要】 前言今天看了一部电影《末代皇帝》,讲述的是溥仪的一生,也是一个时代的末尾,看的心中很有感触,人生只有一次,每个人都无法选择两个人生,所以我们都要努力的去过上精彩的人生,不给以后留遗憾,顺应时代,努力进步。今天要学习的是单例模式,单例模式是一种最简单的设计模式,我相信大家应该对这种模式是最了解的,它的核心是一个被称作单例类的特殊类,通过单例模式可以确保系统中的单例类只有一个实例,而且该实例可...

前言

今天看了一部电影《末代皇帝》,讲述的是溥仪的一生,也是一个时代的末尾,看的心中很有感触,人生只有一次,每个人都无法选择两个人生,所以我们都要努力的去过上精彩的人生,不给以后留遗憾,顺应时代,努力进步。

今天要学习的是单例模式,单例模式是一种最简单的设计模式,我相信大家应该对这种模式是最了解的,它的核心是一个被称作单例类的特殊类,通过单例模式可以确保系统中的单例类只有一个实例,而且该实例可以被外部访问以此来节省系统资源。
虽然单例模式的结构很简单,但是他其中的一些细节却非常值得注意,希望大家可以通过本篇博客对简单的单例模式有一个更加全面的认识。

正文

  • 概述

    确保一个类只有一个实例,并且提供一个全局访问点来访问这个唯一的实例。
    单例模式的设计思想如下:
    如何确保一个类只有一个实例呢?单例设计模式有三个要点:一是某个类只能有一个实例,二是它必须自行创建这实例,三是它必须自行向整个系统提供这个实例。

  • 结构与实现

    • 模式结构
      • 单例模式的结构很简单,它只包含一个单例类,在该单例类内部创建一个它自身的实例,为了防止外部对单例类实例化,可以私有化它的构造函数,在它的内部定义一个private的单例对象,然后提供一个公开的方法向外界提供该对象。
        案例说明:
        某公司承接了一个负载均衡(Load Balance)器软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态的删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,试用单例模式设计该负载均衡器。
        案例分析:
        单例的负载均衡器中需要包含一个容器,用来存储服务器,客户可以动态的添加或者删除服务器,并且可以获取服务器(分发)。
        目录结构:
        在这里插入图片描述

        LoadBalance: 负载均衡器(模拟负载)。

        public class LoadBalance {
        
            private static LoadBalance loadBalance = null;
        
            private List<String> serverList = null;
        
            private LoadBalance(){
                serverList = new ArrayList();
            }
        
            public static LoadBalance getLoadBalance(){
                if (null == loadBalance){
                    loadBalance = new LoadBalance();
                }
                return loadBalance;
            }
        
            public void addServer(String server){
                serverList.add(server);
            }
        
            public void removeServer(String server){
                serverList.remove(server);
            }
        
            // 随机获取服务器
            public String getServer(){
                Random random = new Random();
                int idx = random.nextInt(serverList.size());
                return serverList.get(idx);
            }
        
        }
        

        Client(客户端测试类): 模拟请求,负载进行分发(只贴出了main方法)。

         /**
         * 需求:
         *      某公司承接了一个负载均衡(Load Balance)器软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的
         *      多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态的删减,且客户端请求需要统一分发,因此需要确保
         *      负载均衡器的唯一性,试用单例模式设计该负载均衡器。
         * 分析:
         *      单例的负载均衡器中需要包含一个容器,用来存储服务器,客户可以动态的添加或者删除服务器,并且可以获取服务器(分发);
         * @param args
         */
        public static void main(String[] args) {
        	LoadBalance loadBalance1 = LoadBalance.getLoadBalance();
            LoadBalance loadBalance2 = LoadBalance.getLoadBalance();
            LoadBalance loadBalance3 = LoadBalance.getLoadBalance();
            if (loadBalance1 == loadBalance2 && loadBalance2== loadBalance3 && loadBalance1 == loadBalance3){
                System.out.println("负载均衡器是唯一的!!");
            }
        
            loadBalance1.addServer("Server 1");
            loadBalance2.addServer("Server 2");
            loadBalance3.addServer("Server 3");
        
            LoadBalance loadBalance4 = LoadBalance.getLoadBalance();
            for (int i = 0; i < 10; i++) {
                System.out.println("请求分发到服务器: "+loadBalance4.getServer());
            }
        }
        

        运行结果:
        在这里插入图片描述
        可以看出,负载均衡器在该系统中是单例的,不管获取几次获取到的始终都是同一个单例。

  • 深入单例的创建

    饿汉式单例: 饿汉式单例实现起来非常简单,在单例类中声明一个静态的变量,直接将单例对象实例化出来,然后提供方法向外界提供方法可以直接调用。
    饿汉式单例代码:

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

    在单例类被加载时,饿汉式单例会直接随着类的加载直接将单例类创建出来,但是这样会更加的小号系统资源,因为我们无论是否使用单例,在系统加载该类时都会创建单例对象,一定程度上会延长系统的加载速度。
    懒汉式单例: 懒汉式单例是使用比较频繁的一种方式,在加载类时我们不直接创建单例对象类,在真正要使用时我们才来创建单例对象,大家看下代码就明白了。
    懒汉式单例代码:

    public class LazySubgletion {
    
        // 使用volatile关键字保证多线程中都能正确处理(可以理解为被volatile修饰的属性直接属于主线程,通常说main的可见性)
        private static  LazySubgletion instance = null;
    
        private LazySubgletion(){};
    
        public static LazySubgletion getInstance(){  
             if (instance == null){
                 instance = new LazySubgletion();
             }
            return instance;
        }
    }
    

    在第一次使用单例对象时才创建,这种方式就是懒汉式(也被称为懒加载),
    但是这种方式需要注意线程安全,我们要避免多个线程同时调用getInstance方法,可以通过以下方式避免:

    public class LazySubgletion {
    
        // 使用volatile关键字保证多线程中都能正确处理(可以理解为被volatile修饰的属性直接属于主线程,通常说main的可见性)
        private volatile static  LazySubgletion instance = null;
    
        private LazySubgletion(){};
    
        /**
         * 注意需要保证线程安全,采用双重检查锁定的方式保证线程安全
         * @return
         */
        public static LazySubgletion getInstance(){
            // 第一重判断,当为null时进入
            if (instance == null){
                // 如果有一个线程进入,锁定,但是此时还没有创建instance,其它线程进入后会进行排队(有可能会创建多个)
                // 所以就需要有第二重判断,在进入synchronized代码块后再次判断是否为null,为null再创建
                synchronized (LazySubgletion.class){
                    if (instance == null){
                        instance = new LazySubgletion();
                    }
                }
            }
            return instance;
        }
    
    }
    

    这种方式叫做双重检查锁,为什么要使用双重检查锁来锁定呢?我们可以考虑一下,当A和B线程同时调用的getInstance()方法时,这是俩线程一看instance都是null,然后俩个线程都想进入,如果A线程先进来synchronized代码块,代码块被锁定,B线程进入了排队的状态,然后A线程创建出了单例对象,但是这时的B已经在等待synchronized代码块了,A执行完后B紧跟着进入了代码块,因为B不知道instance已经被创建了,所有他也创建了一次对象,这样就导致系统线程不安全了,因此我们需要双重检查锁(即在synchronized代码块中再进行一次判断),同时我们还要给单例对象instance加上volatile修饰符,加了该修饰符表示被修饰的变量对main线程直接可见,就是保证大家看到的该变量都是一致的,这样可以较为完美的解决线程安全的问题。

    补充

    静态内部类实现单例: 饿汉式不管单例对象用不用都会占用内存,而懒汉式虽然可以实现延迟加载,但是我们需要控制线程安全,过程较为繁琐,同样也会影响系统性能,那么有没有更好的方式来实现单例模式呢?
    当然!有一种静态内部类的方式可以更加完美的实现单例模式!
    我们先看一下代码:

// 使用静态内部类的方式实现(堪称完美的实现方案)
public class StaticSingletion {

    private StaticSingletion() {
    }

    private static class HolderClass{
        private final static StaticSingletion instance = new StaticSingletion();
    }

    /**
     * 在第一次加载时将调用内部类HolderClass且该类为静态内部类,只会被加载一次,在该类内部定义了一个static类型的StaticSingletion,
     * 由Java虚拟机保证其线程的安全性,由于getInstance不需要再被任何线程锁定,所以对性能不会造成影响。
     * @return
     */
    public static StaticSingletion getInstance(){
        return HolderClass.instance;
    }

    public static void main(String[] args) {
        StaticSingletion s1,s2;
        s1 = StaticSingletion.getInstance();
        s2 = StaticSingletion.getInstance();
        System.out.println(s1 == s2);
    }
}

在这种方式实现的单例下,我们将单例对象声明在了静态内部类中,单例对象会随着静态内部类的加载而被创建出来,我们在getInstance方法中调用静态内部类的instance实例,由于实例是被static修饰的,它直接属于内部类,会随着内部类的加载而加载,保证了单例的唯一性,然后我们在调用内部类加载时会被虚拟机加载,虚拟机控制了线程的安全性,这样对性能的影响是最小的,这种方式既可以实现延迟加载,又可以保证线程安全且不影响系统性能,它的缺点是这是Java语言的特性,有许多的其它面向对象语言并不支持该方式。

  • 优缺点分析

    • 优点
      • 单例模式提供了唯一实例的受控访问,因为单例类封装了它的唯一实例,索引它可以严格的控制用户怎样以及何时可以访问它;
      • 在系统内存中始终只存在一个对象,节省系统资源;
      • 允许可变数目的实例,可以基于单例类进行扩展,使用与控制单例相似的方式获取指定个数的实例。
    • 缺点
      • 单例模式没有抽象层,所以很难扩展;
      • 单例类的职责过重,违背了单一职责原则;
      • 由于虚拟机的垃圾自动回收技术,如果单例对象长时间不被利用,系统会把它当作辣鸡回收掉,会导致单例的已有的一些状态丢失。
    • 适用环境

      在以下情况下可以考虑使用原型模式:
      1. 系统只需要一个实例对象;
      2. 客户端调用类的单个实例只允许使用同一个公共访问点,不能通过其它方式进行访问。

  • 自练习习题

    • 某软件开发人员要创建一个数据库连接池,将指定个数的数据库连接对象存储在连接池中,客户端代码可以从池中随机获取一个连接对象来连接数据库 ,试通过单例类进行改造,设计一个能够自行提供指定个数实例对象的数据库连接类并用Java代码模拟编程。

    世界上再无第二个我,人生来孤独,努力让自己变的优秀起来!

    半原创博客,用以记录学习,希望可以帮到您,不喜可喷。
    在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。