设计模式-单例模式

单例模式

其它创建型模式链接:

  1. 设计模式-简单工厂模式
  2. 设计模式-工厂方法模式
  3. 设计模式-抽象工厂模式
  4. 设计模式-原型模式
  5. 设计模式-建造者模式

1.单例模式概述

​ 对于一个软件系统的某些类而言,只有一个实例很重要,例如一个系统只能有一个窗口管理器或文件系统,只能有一个计时工具等。

​ 如何保证一个类只有一个实例并且这个实例易于被访问?定义一个统一的全局变量可以确保对象随时都可以被访问,但不能防止创建多个对象。一个更好的解决办法是让该类自身负责创建和保存唯一的实例,并保证不能创建其他实例,还要提供一个访问该实例的方法,这就是单例模式的动机。

​ 单例模式定义如下:

单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

单例模式的三个要点:

  1. 某个类只能有一个实例
  2. 它必须自行创建这个实例
  3. 它必须自行向整个系统提供这个实例

2.单例模式结构

单例模式只包含一个单例角色,也就是Singleton。

image-20210518110732141

在单例类内部创建唯一实例,并通过getInstance方法让客户端可以使用这个实例,为防止在外部实例化,将构造函数可见性设置为private。

3.案例

某公司承接了一个服务器负载均衡软件的开发工作,该软件运行在一个负载均衡服务器上。由于集群中的服务器需要动态删减,并且客户端请求需要统一分发,因此需要确保负载均衡服务器的唯一性。使用单例模式设计负载均衡服务器。

3.1结构图

image-20210518111558222

3.2代码实现

单例类
public class LoadBalancer {
    //存储唯一实例
    private static LoadBalancer instance = null;
    //服务器集合
    private List<String> 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 serverList.get(i);
    }

}
客户端
public class Demo {
    public static void main(String[] args) {
        LoadBalancer balancer1,balancer2;
        balancer1=LoadBalancer.getLoadBalancer();
        balancer2=LoadBalancer.getLoadBalancer();

        if (balancer1==balancer2){
            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);
        }
    }
}

3.3效果展示

服务器负载均衡具有唯一性
请求发送至服务器 : Server 2
请求发送至服务器 : Server 1
请求发送至服务器 : Server 4
请求发送至服务器 : Server 2
请求发送至服务器 : Server 3
请求发送至服务器 : Server 4
请求发送至服务器 : Server 1
请求发送至服务器 : Server 3
请求发送至服务器 : Server 2
请求发送至服务器 : Server 2

4.饿汉式单例和懒汉式单例

4.1饿汉式单例

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

image-20210518113216502

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

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return instance;
    }
}

当类被加载时,静态变量instance会被初始化,相当于类加载时单例对象就已创建。

4.2懒汉式单例与双检查锁定

与饿汉式相同的是构造函数也是私有的,但不同的是,懒汉式单例在第一次被引用时将自己实例化。

结构如下:

image-20210518115248019

public class LazySingleton {
    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    synchronized public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

懒汉式单例在第一次调用getInstance()方法时,进行实例化,在类加载时并不实例化,这种技术又称为延迟加载技术,即需要时再加载实例。为了避免多个线程同时调用,可以使用synchronized关键字。

在上述模式中,虽然加了关键字进行线程锁定,解决了线程安全问题,但是每次调用都需要对线程进行锁定,在多线程高并发环境中会导致系统性能的大大降低。因此可以对其进行改造,只对关键代码进行锁定。

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

问题貌似得到解决,但事实并非如此,如果使用上述代码实现,还是会存在单例对象不唯一的情况。原因如下:

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

改进后的实现如下:

public 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修饰的成员变量可以确保多个线程能够正常处理。由于volatile关键字会屏蔽java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此双重检查锁定来实现单例模式也不是一种完美的实现方式。

4.3两种方式的比较

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

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

4.4使用静态内部类实现单例模式

饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制繁琐,而且性能受影响,可见,无论是饿汉式还是懒汉式都存在一些问题。为了客服这些问题,在Java语言中可以通过Initialization on Demand Holder(IoDH)技术来实现单例模式。

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

public class Singleton {
    private Singleton() {
    }

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

    public static Singleton getInstance() {
        return HolderClass.instance;
    }
}

在上述代码中由于没有静态单例对象作为成员变量,因此类加载时不会进行实例化,第一次调用getInstance()时将加载内部静态类HolderClass,在该内部类中定义instance,此时会首先初始化这个成员变量,有Java虚拟机来保证其线程安全性,确保该变量只能初始化一次。

通过使用IoDH,皆可以实现延迟加载,又可以保证线程安全,不影响性能,不失为一种最好的Java语言单例模式实现方式;其缺点是与编程语言自身特性有关,很对面向对象语言并不支持IoDH。

5.单例模式优缺点与适用环境

单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中的使用频率相当之高,在很多应用软件和框架中都得到广泛应用

5.1单例模式优点

单例模式的优点主要如下:

  1. 单例模式提供了对唯一实例的受控访问。因为单例模式封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  2. 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  3. 允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例相似方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。

5.2单例模式缺点

单例模式的缺点主要如下:

  1. 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。‘
  2. 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法,将对象的创建和对象本身的功能耦合在一次。
  3. 现在很多面向对象语言的运行环境都提供了自动垃圾回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

5.3单例模式适用环境

在以下情况可以考虑使用单例模式:

  1. 系统只需要一个实例对象
    单例模式中没有抽象层,因此单例类的扩展有很大的困难。‘
  2. 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法,将对象的创建和对象本身的功能耦合在一次。
  3. 现在很多面向对象语言的运行环境都提供了自动垃圾回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

5.3单例模式适用环境

在以下情况可以考虑使用单例模式:

  1. 系统只需要一个实例对象
  2. 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其它途径访问该实例。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值