我是靠谱客的博主 拼搏火龙果,最近开发中收集的这篇文章主要介绍设计模式之【单例模式】,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。

以心法为基础,以武器运用招式应对复杂的编程问题。

表妹:哥啊,为什么电脑上只能打开一个任务管理器呢?

我:首先,如果可以打开多个窗口,但这些窗口显示的内容是完全一致的,这是一种资源浪费;其次,如果这些窗口显示的内容不一致的话,就意味着某一瞬间系统有多个状态,这与实际不符,也会给用户带来误解。

表妹:原来是这样子~

我:这就用到了设计模式中的单例模式。


确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

为什么要使用单例模式?

避免资源访问冲突

比如,往文件中打印日志的Logger类:

public class Logger {
    private FileWriter writer;
    
    public Logger() {
        File file = new file("D:/Log/log.txt");
        writer = new FileWriter(file, true);  // true表示追加写入
    }
    
    public void log(String message) {
        writer.write(message);
    }
}

// Logger类的应用示例:
public class UserController {
    private Logger logger = new Logger();
    
    public void login(String name, String password) {
        // 省略业务逻辑代码
        logger.log(name + " logined!");
    }
}

public class OrderController {
    private Logger logger = new Logger();
    
    public void create(OrderVo order) {
        // 省略业务逻辑代码
        logger.log("Created an order: " + order.toString());
    }
}

从上面的代码中,我们可以看到,所有的日志都是写到同一个文件D:/Log/log.txt中的。很明显,在并发写入的时候,就会出现日志信息相互覆盖的情况。如下图所示:

在这里插入图片描述

可能有同学会说,可以加锁来解决并发问题。

是的,确实可以。但是,其实日志文件只有一份,没必要创建那么多Logger对象,毕竟系统的文件句柄是有限的资源。

所以,我们将Logger设计成一个单例类,程序中只允许创建一个Logger对象,所有的线程共享使用的Logger对象,共享一个FileWriter对象,而且FileWriter本身是对象级别线程安全的,也就避免了多线程情况下写日志会互相覆盖的问题。

public class Logger {
    private FileWriter writer;
    private static final Logger instance = new Logger();
    
    private Logger() {
        File file = new File("");
        writer = new FileWriter(file, true); // true表示追加写入
    }
    
    public static Logger getInstance() {
        return instance;
    }
    
    public void log(String message) {
        writer.write(message);
    }
}

// Logger单例类的应用示例:
public class UserController {
    public void login(String name, String password) {
        // 省略业务逻辑代码
        Logger.getInstance().log(name + " logined!");
    }
}

public class OrderController {
    private Logger logger = new Logger();
    
    public void create(OrderVo order) {
        // 省略业务逻辑代码
        Logger.getInstance().log("Created a order: " + order.toString());
    }
}

表示全局唯一

从业务概念上,有些数据在系统中只需要保存一份,这种就比较适合设计成单例类。

比如系统在运行的时候,需要加载一些配置和属性,这些配置和属性是共享的,且在整个生命周期内都存在的,所以只需要存在一份就够了。

如果每次都在需要用到的时候重新new一个出来,再进行赋值,显然是浪费资源的,再赋值也没有意义,所以应该将其设计成单例类,全局只维护一份。

单例模式的实现方式

理解了在开发中使用单例设计模式的原因以后,现在来看看到底如何实现单例模式。

要实现一个单例,我们需要关注的点有如下几个:

  1. 构造函数需要是private,这样才能避免外部通过new创建实例;
  2. 考虑对象创建时的线程安全问题;
  3. 考虑是否支持延迟加载;
  4. 考虑getInstance()性能是否高(是否加锁)。

还是以递增的ID生成器为例子:

  • 饿汉式

这种方式在类加载的时候,instance静态实例就已经创建并初始化好了,而不是在真正用到该类的时候,再创建实例。所以,这种方式的单例创建过程是线程安全的。

import java.util.concurrent.atomic.AtomicLong;
public class IDGenerator {
    private AtomicLong ID = new AtomicLong(0);
    private static final IDGenerator instance = new IDGenerator();
    private IDGenerator() {}
    
    public static IDGenerator getInstance() {
        return instance;
    }
    
    public long getID() {
        return ID.incrementAndGet();
    }
}

这种方式还有几个优点:

  1. 如果这个初始化操作比较耗时,将其提前到程序启动的时候完成,这样能避免在程序运行的时候,再去初始化导致的性能问题(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。
  2. 如果实例占用资源多,按照fail-fast的原则(有问题尽早暴露),那我们也希望在程序启动的时候就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错,可以立即去修复。避免在程序运行过程中触发报错,影响系统的可用性。
  • 懒汉式

可能有同学会说,饿汉式提前初始化实例,是一种浪费资源的行为,应该需要用到的时候,再去初始化。那么,懒汉式这种方式就支持延迟加载。

import java.util.concurrent.atomic.AtomicLong;
public class IDGenerator {
    private AtomicLong ID = new AtomicLong(0);
    // 类初始化时,不初始化这个对象(延时加载,真正用到的时候再创建)
    private static IDGenerator instance;
    private IDGenerator() {}
    // 方法同步,调用效率低
    public static synchronized IDGenerator getInstance() {
        if (instance == null) {
            instance = new IDGenerator();
        }
        return instance;
    }
    public long getID() {
        return ID.incrementAndGet();
    }
}

我们给getInstance()这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。如果这个单例类偶尔被调用到,该方式还是可以接受的。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

  • 双重检测

饿汉式不支持延迟加载,懒汉式的并发度较低。那我们来看一种既支持延迟加载,又避免并发度较低的单实例实现方式,也就是双重检测实现方式。

import java.util.concurrent.atomic.AtomicLong;
public class IDGenertor{
    private AtomicLong ID = new AtomicLong(0);
    // 这里加上volatile关键字的原因:
    // 禁止指令重排序优化,保证instance变量被赋值的时候对象已经是初始化过的。
    private static volatile IDGenerator instance = null;
    private IDGenerator() {}
    public static IDGenerator getInstance() {
        if (instance == null) {
            synchronized(IDGenerator.class) {  // 类级别锁
                if (instance == null) {
                    instance = new IDGenerator();
                }
            }
        }
        return instance;
    }
    public long getID() {
        return ID.incrementAndGet();
    }
}

这种实现方式,只要instance被创建以后,即便再调用getInstance()函数也不会再进入加锁逻辑中了。所以,避免了并发度较低的问题。

  • 静态内部类

这是一种比双重检测更加简单的实现方式。

import java.util.concurrent.atomic.AtomicLong;
public class IDGenerator {
    private AtomicLong ID = new AtomicLong(0);
    private IDGenerator() {}
    
    private static class SingletonHolder {
        private static final IDGenerator instance = new IDGenerator();
    }
    
    public static IDGenerator getInstance() {
        return SingletonHolder.instance;
    }
    
    public long getID() {
        return ID.incrementAndGet();
    }
}

SingletonHolder是一个静态内部类,当外部类IDGenerator被加载的时候,并不会创建SingletonHolder实例对象。只有当调用getInstance()方法的时候,SingletonHolder才会被加载,这个时候才会创建instance。

instance的唯一性、创建过程的线程安全性,都有JVM来保证。所以,这种方式既保证了线程安全,又能做到延迟加载。

  • 枚举

这是一种最简单的实现方式,通过Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

import java.util.concurrent.atomic.AtomicLong;
public enum IDGenerator {
    // 枚举元素本身就是单例
    INSTANCE;
    private AtomicLong ID = new AtomicLong(0);
    
    public long getID() {
        return ID.incrementAndGet();
    }
}

这种实现方式可以天然的防止反射和反序列化调用,但是不支持延迟加载。

以上几种实现方式,怎么选择呢?

  • 如果单例对象占用资源少,不需要延迟加载,那么,枚举好于饿汉式;
  • 如果单例对象占用资源多,需要延迟加载,静态内部类好于懒汉式。

单例模式的优缺点

设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路,也就是我们与“敌人”交手时用的“招式”。我们来看一下,单例设计模式有哪些优点:

  1. 单例模式在内存中只有一个实例,减少了内存开销。
  2. 对于一个对象需要频繁创建、销毁时,而且创建或销毁时性能又无法优化的时候,单例模式能够减少系统的性能开销。
  3. 单例模式可以避免对资源的多重占用,例如上面的Logger类,由于使用单例模式后,只有一个实例存在,避免对同一个资源文件的同时写操作。
  4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问,比如,可以设计一个单例类,负责所有数据表的映射处理。

虽然它是我们前辈们总结出来的经验,但并不代表该设计模式就是完美的。我们来看一下,单例设计模式的一些缺点:

1、单例设计模式对抽象、继承、多态都支持得不好。我们还是通过递增ID生成器的例子来解释一下:

public class Order {
    public void create(...) {
        // 省略一些业务逻辑代码
        long ID = IDGenerator.getInstance().getID();
        // 省略一些业务逻辑代码
    }
}

public class User {
    public void create(...) {
        // 省略一些业务逻辑代码
        long ID = IDGenerator.getInstance().getID();
        // 省略一些业务逻辑代码
    }
}

现在IDGenerator的使用方式是违背了基于接口而非实现编程的原则。假如有一天,我们希望订单ID和用户ID分别采用不同的ID生成方法,那么需要修改所有用到IDGenerator类的地方,这样的代码改动是比较大的。

public class Order {
    public void create(...) {
        // 省略一些业务逻辑代码
        long ID = IDGenerator.getInstance().getID();
        // 需要将上面一行代码,改成下面一行代码
        long ID = OrderIDGenerator.getInstance().getID();
        // 省略一些业务逻辑代码
    }
}

public class User {
    public void create(...) {
        // 省略一些业务逻辑代码
        long ID = IDGenerator.getInstance().getID();
        // 需要将上面一行代码,改成下面一行代码
        long ID = UserIDGenerator.getInstance().getID();
        // 省略一些业务逻辑代码
    }
}

你看,这不就违背了我们之前学习过的设计原则之【开放封闭原则】吗?所以,对扩展性不友好。

以上是针对抽象而言的,至于继承和多态,虽然单例类可以做到,但是会让代码的可读性降低。

2、单例会隐藏类之间的依赖关系。我们知道,如果通过构造函数、参数传递等方式声明的类之间的依赖关系,可以通过查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。在阅读代码的时候,就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。

3、单例对代码的可测试性不友好。如果单例类依赖比较重的外部资源,比如DB,我们在写单元测试的时候,希望能通过mock的方式将其替换掉,但是单例类这种硬编码的方式,导致无法实现mock替换;其次,单例类中如果持有成员变量的话(比如IDGenerator中的成员变量ID),它实际上就相当于一种可变的全局变量,这也会影响代码的可测试性的。

4、在一些情况下,单例模式可能与单一职责原则有冲突。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,使得单例类的职责过重。

尽管如此,并不是说,单例设计模式就是一种反模式,毕竟,设计模式是前辈们经过无数的实践总结出来的“招式”。这个“招式”适不适合应用在这里?应用在这里会不会违背设计原则?会不会破坏面向对象编程风格?又会不会降低代码质量?这些都是要相互权衡的,没有一种设计模式是完美的。

其实,类对象的全局唯一性还可以通过其他方式来保证,单例模式只是其中一种。

接下来,我们来看看,单例设计模式一般有哪些应用场景。

适用场景

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器。
  • 在整个项目中需要一个共享访问点或共享数据,比如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

总结

设计模式没有对错,关键看你怎么用。

参考

极客时间专栏《设计模式之美》

《设计模式之禅》

最后

以上就是拼搏火龙果为你收集整理的设计模式之【单例模式】的全部内容,希望文章能够帮你解决设计模式之【单例模式】所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(37)

评论列表共有 0 条评论

立即
投稿
返回
顶部