确保对象的唯一性----单例模式(上)

69 阅读8分钟

开启掘金成长之旅!这是我参加「掘金日新计划·12月更文挑战」的第十八天,点击查看活动详情

单例模式的动机

对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子--Windows任务管理器,如图所示,我们可以做一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”,看能否打开多个任务管理器窗口?如果你的桌面出现多个任务管理器,我请你吃饭,微笑(注:电脑中毒或私自修改Windows内核者除外)。通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。为什么要这样设计呢?我们可以从以下两个方面来分析:其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源,任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等,浪费是可耻的,而且根本没有必要显示多个内容完全相同的窗口;其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?这纯属“调戏”用户,偷笑,给用户带来误解,更不可取。由此可见,确保Windows任务管理器在系统中有且仅有一个非常重要。

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

单例模式概述

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

class TaskManager{
    public TaskManager(){......} //初始化窗口
    public void displayProcesses(){......} //显示进程
    public void displayServices(){......} //显示服务
    ......
}    

为了实现Windows任务管理器的唯一性,我们通过如下三步来对该类进行重构:

(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对象,因为构造函数是私有的。 

思考

为什么要将成员变量tm定义为静态变量?

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

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

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

上述代码也是单例模式的一种最典型实现方式,有了以上基础,理解单例模式的定义和结构就非常容易了。单例模式定义如下: 单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。单例模式结构如图3-2所示:

确保对象的唯一性——单例模式 (一) - 图2 单例模式结构图中只包含一个单例角色:

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

负载均衡器的设计与实现

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

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

确保对象的唯一性——单例模式 (二) - 图1

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

import java.util.*;

//负载均衡器LoadBalancer:单例类,真实环境下该类将非常复杂,包括大量初始化的工作和业务方法,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class LoadBalancer{
    //私有静态成员变量,存储唯一实例
    private static LoadBalancer instance = null;
    //服务器集合
    private List serverList = null;
    
    //私有构造函数
    private LoadBlancer(){
        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);
    }
}

编写如下客户测试代码:

class Client{
    public static void main(String args[]){
        //创建四个LoadBalancer对象
        LoadBalancer balancer1,balancer2,balancer3,balancer4;
        balancer1 = LoadBalancer.getLoadBalancer();
        balancer2 = LoadBalancer.getLoadBalancer();
        balancer3 = LoadBalancer.getLoadBalancer();
        balancer3 = 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");
        
        //模拟客户端请求的分发
        for(int i = 0; i < 10; i++){
            String server = balancer1.getServer();
            System.out.println("分发请求至服务器: " + server);
        }
    }
}    

编译并运行程序,输出结果如下:

服务器负载均衡器具有唯一性!

分发请求至服务器: Server 1
分发请求至服务器: Server 3
分发请求至服务器: Server 4
分发请求至服务器: Server 2
分发请求至服务器: Server 3
分发请求至服务器: Server 2
分发请求至服务器: Server 3
分发请求至服务器: Server 4
分发请求至服务器: Server 4
分发请求至服务器: Server 1

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