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

1 概述

1.1 引言

不少时候为了节约系统资源,须要确保系统中某个类只有一个惟一的实例,当这个惟一实例建立了以后,没法再建立一个同类型的其余对象,全部的操做只能基于这一个惟一实例。这是单例模式的动机所在。java

好比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 总结

在这里插入图片描述

若是以为文章好看,欢迎点赞。

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

在这里插入图片描述

相关文章
相关标签/搜索