单例模式——对象创建型模式

引入——任务管理器:
动机:

对于一个软件系统的某些类而言,我们无须创建多个实例

举个大家都熟知的例子——Windows任务管理器:通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。

在实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。

模拟:

下面我们来模拟实现Windows任务管理器,假设任务管理器的类名为TaskManager,在TaskManager类中包含了大量的成员方法,例如构造函数TaskManager(),显示进程的方法displayProcesses(),显示服务的方法displayServices()等,该类的示意代码如下:

class TaskManager {
    public TaskManager() {……} //初始化窗口
    public void displayProcesses() {……} //显示进程
    public void displayServices() {……} //显示服务}
按照单例模式进行重构:
  1. 由于每次使用new关键字来实例化TaskManager类时都将产生一个新对象,为了确保TaskManager实例的唯一性,我们需要禁止类的外部直接使用new来创建对象,因此需要将TaskManager的构造函数的可见性改为private,如下代码所示:

    private TaskManager() {}
    
  2. 将构造函数改为private修饰后,该如何创建对象呢?不要着急,虽然类的外部无法再使用new来创建对象,但是在TaskManager的内部还是可以创建的,可见性只对类外有效。因此,我们可以在TaskManager中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在TaskManager中定义一个静态的TaskManager类型的私有成员变量,如下代码所示:

    private static TaskManager tm = null;
    
  3. 为了保证成员变量的封装性,我们将TaskManager类型的tm对象的可见性设置为private,但外界该如何使用该成员变量并何时实例化该成员变量呢?答案是增加一个公有的静态方法,如下代码所示:

    public static TaskManager getInstance () {
        if (tm == null) {
            tm = new TaskManager();
        }
        return tm;
    }
    

    在getInstance()方法中首先判断tm对象是否存在,如果不存在(即tm == null),则使用new关键字创建一个新的TaskManager类型的tm对象,再返回新创建的tm对象;否则直接返回已有的tm对象。

需要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便供外界其他对象使用,其次它使用了static关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无须创建TaskManager对象,事实上在类外也无法创建TaskManager对象,因为构造函数是私有的。

通过以上三个步骤,我们完成了一个最简单的单例类的设计,其完整代码如下:

class TaskManager {
    private static TaskManager tm = null;
    private TaskManager() {……} //初始化窗口
    public void displayProcesses() {……} //显示进程
    public void displayServices() {……} //显示服务
    public static TaskManager getInstance () {
        if (tm == null) {
            tm = new TaskManager();
        }
        return tm;
    }}

在类外我们无法直接创建新的TaskManager对象,但可以通过代码TaskManager.getInstance()来访问实例对象,第一次调用getInstance()方法时将创建唯一实例,再次调用时将返回第一次创建的实例,从而确保实例对象的唯一性。

定义:

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

要点:
  • 某个类只能有一个实例
  • 它必须自行创建这个实例
  • 它必须自行向整个系统提供这个实例
结构图:

在这里插入图片描述

总结:

单例模式结构图中只包含一个单例角色——Singleton(单例):

  • 在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance()工厂方法,让客户可以访问它的唯一实例;
  • 为了防止在外部对其实例化,将其构造函数设计为私有;
  • 在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一 实例。
应用——负载均衡器的设计与实现:
背景:

Sunny软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键。

Sunny公司开发人员通过分析和权衡,决定使用单例模式来设计该负载均衡器,结构图如下:

在这里插入图片描述

代码实现:

将负载均衡器LoadBalancer设计为单例类,其中包含一个存储服务器信息的集合 serverList,每次在serverList中随机选择一台服务器来响应客户端的请求,实现代码如下:

package singleton;
import java.util.*;

public class LoadBalancer {
    //私有静态成员变量,存储唯一实例
    private static LoadBalancer instance = null;
    //服务器集合
    private List serverList = null;
    //私有构造函数
    private LoadBalancer() {
        serverList = new ArrayList();
    }
    //公有静态成员方法,返回唯一实例
    public static LoadBalancer getLoadBalancer() {
        if (instance == null) {
            instance = new LoadBalancer();
        }
        return instance;
    }
    //增加服务器
    public void addServer(String server) {
        serverList.add(server);
    }
    //删除服务器
    public void removeServer(String server) {
        serverList.remove(server);
    }
    //使用Random类随机获取服务器
    public String getServer() {
        Random random = new Random();
        int i = random.nextInt(serverList.size());
        return (String)serverList.get(i);
    }
}

编写如下客户端测试代码:

package singleton;

public class Client {
    public static void main (String[] args) {
        //创建四个LoadBalancer对象
        LoadBalancer balancer1,balancer2,balancer3,balancer4;
        balancer1 = LoadBalancer.getLoadBalancer();
        balancer2 = LoadBalancer.getLoadBalancer();
        balancer3 = LoadBalancer.getLoadBalancer();
        balancer4 = LoadBalancer.getLoadBalancer();
        //判断服务器负载均衡器是否相同
        if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4) {
            System.out.println("服务器负载均衡器具有唯一性!");
        }
        //增加服务器
        balancer1.addServer("Server 1");
        balancer1.addServer("Server 2");
        balancer1.addServer("Server 3");
        balancer1.addServer("Server 4");
        balancer2.removeServer("Server 4");
        //模拟客户端请求的分发
        for (int i = 0; i < 10; i++) {
            String server = balancer1.getServer();
            System.out.println("分发请求至服务器: " + server);
        }
    }
}
输出结果:
总览:

在这里插入图片描述

结果:

在这里插入图片描述

结果分析:

虽然创建了四个LoadBalancer对象,但是它们实际上是同一个对象,因此,通过使用单例模式 可以确保LoadBalancer对象的唯一性。

实现方式1——饿汉式:

饿汉式单例类是实现起来最简单的单例类,饿汉式单例类结构图如下:

在这里插入图片描述

由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象,代码如下所示:

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

当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。如果使用饿汉式单例来实现负载均衡器LoadBalancer类的设计,则不会出现 创建多个单例对象的情况,可确保单例对象的唯一性。

实现方式2——懒汉式:
第一种——对 getInstance() 使用 synchronized:
结构图:

在这里插入图片描述

懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自 行实例化,这种技术又称为延迟加载(Lazy Load)技术,即需要的时候再加载实例,为了避免 多个线程同时调用getInstance()方法,我们可以使用关键字synchronized,代码如下所示:

class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton() { }
    synchronized public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
第二种——对 “instance = new LazySingleton(); ”锁定:

上述代码虽然解决了线程安全问题,但是每次调用getInstance()时 都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何 既解决线程安全问题又不影响系统性能呢?我们继续对懒汉式单例进行改进。事实上,我们 无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:

public static LazySingleton getInstance() {
    if (instance == null) {
        synchronized (LazySingleton.class) {
            instance = new LazySingleton();
        }
    }
    return instance;
}
第三种——双重检查锁:

问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。

原因如下: 假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通 过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代 码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入 synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在 synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:

class LazySingleton {
    private volatile static LazySingleton instance = null;
    private LazySingleton() { }
    public static LazySingleton getInstance() {
        //第一重判断
        if (instance == null) {
            //锁定代码块
            synchronized (LazySingleton.class) {
                //第二重判断
                if (instance == null) {
                    instance = new LazySingleton(); //创建单例实例
                }
            }
        }
        return instance;
    }
}

需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之 前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代 码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。

饿汉式单例类与懒汉式单例类比较:

饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以 确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因 此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该 对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统 加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。

懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处 理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源 初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机 率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

实现方式3——Initialization Demand Holder (IoDH)

在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将 该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:

//Initialization on Demand Holder
class Singleton {
    private Singleton() {}
    private static class HolderClass {
        private final static Singleton instance = new Singleton();
    }
    public static Singleton getInstance() {
        return HolderClass.instance;
    }
    public static void main(String args[]) {
        Singleton s1, s2;
        s1 = Singleton.getInstance();
        s2 = Singleton.getInstance();
        System.out.println(s1==s2);
    }
}

编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单 例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次 调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量 instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员 变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影 响。

通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象 语言不支持IoDH)。

单例模式——主要优点:
  • 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严 格控制客户怎样以及何时访问它。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销 毁的对象单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获 得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
单例模式——主要缺点:
  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角 色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的 本身的功能融合到一起。
  • 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如 果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次 利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
单例模式——适用场景:
  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

相关推荐

  1. 创建--模式

    2024-03-20 09:10:05       63 阅读
  2. 创建模式 | 模式

    2024-03-20 09:10:05       58 阅读
  3. 【设计模式 创建模式

    2024-03-20 09:10:05       62 阅读
  4. 创建模式模式

    2024-03-20 09:10:05       53 阅读
  5. 1.创建模式--模式

    2024-03-20 09:10:05       37 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-20 09:10:05       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-20 09:10:05       101 阅读
  3. 在Django里面运行非项目文件

    2024-03-20 09:10:05       82 阅读
  4. Python语言-面向对象

    2024-03-20 09:10:05       91 阅读

热门阅读

  1. opencv | 编译opencv卡在ADE: Download: v0.1.1f.zip

    2024-03-20 09:10:05       42 阅读
  2. PHP与Spring Boot在实现功能上的比较

    2024-03-20 09:10:05       45 阅读
  3. 关于sftp限制登录默认目录若干问题

    2024-03-20 09:10:05       32 阅读
  4. Go语言学习12-反射和Unsafe

    2024-03-20 09:10:05       46 阅读
  5. go 解决货币计算的难题:避免浮点数陷阱

    2024-03-20 09:10:05       31 阅读
  6. Rust 的 PhantomData

    2024-03-20 09:10:05       42 阅读
  7. ES进程除了kill之外,有什么优雅关闭的方式吗?

    2024-03-20 09:10:05       42 阅读
  8. R语言Scale函数与normalize.quantiles()函数的异同

    2024-03-20 09:10:05       45 阅读
  9. Linux Shell 管道基本介绍

    2024-03-20 09:10:05       44 阅读
  10. 红魔馆的馆主

    2024-03-20 09:10:05       38 阅读
  11. SQL-存储过程介绍

    2024-03-20 09:10:05       48 阅读
  12. 未来之路:Python PDF处理技术的革新

    2024-03-20 09:10:05       43 阅读