【Java设计模式 设计模式与范式】创建型模式 一:单例模式(下)

简介: 【Java设计模式 设计模式与范式】创建型模式 一:单例模式(下)

模式实践

单例模式的一些实际应用场景。处理资源访问冲突问题,处理全局唯一类问题。

设计一个文件写入无冲突的日志工具

当我们使用日志类去写日志时,自定义实现了一个往文件中打印日志的 Logger 类。具体的代码实现如下所示:

public class Logger {
  private FileWriter writer;
  public Logger() {
    File file = new File("/Users/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 username, String password) {
    // ...省略业务逻辑代码...
    logger.log(username + " logined!");
  }
}
public class OrderController {
  private Logger logger = new Logger();
  public void create(OrderVo order) {
    // ...省略业务逻辑代码...
    logger.log("Created an order: " + order.toString());
  }
}

所有的日志都写入到同一个文件 /Users/log.txt 中。在 UserController 和 OrderController 中,我们分别创建两个 Logger 对象。在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况

我们最先想到的就是通过加锁的方式:给 log() 函数加互斥锁(Java 中可以通过 synchronized 的关键字),同一时刻只允许一个线程调用执行 log() 函数。具体的代码实现如下所示

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

但是这并不能解决相互写入的问题,这种锁是一个对象级别的锁,一个对象在不同的线程下同时调用 log() 函数,会被强制要求顺序执行。但是,不同的对象之间并不共享同一把锁。在不同的线程下,通过不同的对象调用执行 log() 函数,锁并不会起作用,仍然有可能存在写入日志互相覆盖的问题

1 使用类锁解决资源冲突问题

要想解决这个问题有如下几种思路,首先就是将对象锁升级为类锁。

  • 给日志类的实现加类锁,让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用 log() 函数,而导致的日志覆盖问题
public class Logger {
  private FileWriter writer;
  public Logger() {
    File file = new File("/Users/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  public void log(String message) {
    synchronized(Logger.class) { // 类级别的锁
      writer.write(mesasge);
    }
  }
}
  • 使用分布式锁,但是实现一个安全可靠、无 bug、高性能的分布式锁,并不是件容易的事情
  • 并发队列(比如 Java 中的 BlockingQueue)也可以解决这个问题:多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件。这种方式实现起来也稍微有点复杂

2 使用单例模式解决资源冲突问题

抛开以上复杂的设计,最简单的实现方式就是单例模式,让Logger类只有一个对象,再加上 FileWriter 本身就是线程安全的,它的内部实现中本身就加了对象级别的锁

因为唯一的 Logger 对象有持有唯一的 FileWriter 对象,所以,FileWriter 对象级别的锁也解决了数据写入互相覆盖的问题。那么对象锁的作用就等同于类锁了:饿汉式单例实现

public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();
  private Logger() {
    File file = new File("/Users/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  public static Logger getInstance() {
    return instance;
  }
  public void log(String message) {
    writer.write(mesasge);
  }
}
// Logger类的应用示例:
public class UserController {
  public void login(String username, String password) {
    // ...省略业务逻辑代码...
    Logger.getInstance().log(username + " logined!");
  }
}
public class OrderController {  
  public void create(OrderVo order) {
    // ...省略业务逻辑代码...
    Logger.getInstance().log("Created a order: " + order.toString());
  }
}

从这我们也能看的出,单例模式并不能解决方法调用的并发问题,只是能保证一个类有一个实例,如果要解决并发问题,还需要配合对象锁使用,单例模式+对象锁=类锁,这很好理解,当一个类只有一个对象的时候,当然对象锁和类锁的作用就想同了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,一方面节省内存空间,另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能随便浪费)

设计一个网站访问数自增器

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。

再比如,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例,饿汉式单例实现

package com.example.springboot.test;
import java.util.concurrent.atomic.AtomicLong;
/**
 * * @Name MainTest
 * * @Description
 * * @author tianmaolin
 * * @Date 2022/4/7
 */
public class MainTest {
    public static void main(String[] args) {
        // IdGenerator使用举例
        long id = IdGenerator.getInstance().getId();
        System.out.println(id);
    }
}
class IdGenerator {
    // AtomicLong是一个Java并发库中提供的一个原子变量类型,
    // 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
    // 比如下面会用到的incrementAndGet().
    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();
    }
}

如果不是单例的,那么两个线程分别调用自增器对象的getId方法同时依据现有值自增,本来应该累加2,就成了累加1了。当只有一个自增器对象的时候,即使多个线程调用,因为自增方法是线程安全的原子操作,所以不会有冲突的情况。这和上边的日志覆写例子类似,单例保证只有一个对象,再加上对象操作方法本身是线程安全的。

当然全局唯一类也能通过:工厂模式IOC 容器(比如 Spring IOC 容器)来保证

模式对比

单例模式有几种全局唯一实现的替代方案:静态方法工厂模式IOC 容器(比如 Spring IOC 容器),我们重点关注静态方法和单例模式的对比

单例模式与静态方法

要想实现和单例模式一样的效果,实际上我们通过静态方法也可以,这样就不用考虑类实例的创建过程了,直接用类的静态方法。例如:

// 静态方法实现方式
public class IdGenerator {
  private static AtomicLong id = new AtomicLong(0);
  public static long getId() { 
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

其它的使用大同小异。有以下几点区别:

  • 单例模式某些实现方式(懒汉式单例)支持延迟加载,但静态方法不支持延迟加载。
  • 静态方法是面向过程的,而非面向对象的编程思想,单例是一个对象,能够实现接口或者继承一个父类,但类的静态方法不行。静态方法只能使用静态成员变量,单例可以动态加载一些内容,比如属性有其他类的对象(组合)。
  • 单例模式是利用唯一的实例保存系统的状态,提供的实例方法也是为了对这个唯一的实例进行操作,而静态方法多是一些工具方法,Math 类中的静态方法就是一个典型的例子,如果仅仅是想不自己创建类的实例就可以调用到某些方法来完成一定的操作,那完全没必要也不应该使用单例模式
  • 从生命周期上来看,静态方法的类会在代码编译的时候就被加载,静态方法中产生的对象句柄,会随着静态方法执行完毕而释放掉,对象也会在不再有引用的时候消失,而且执行类中的静态方法时,不会实例化静态方法所在的类。如果用单例模式, 产生的那一个唯一的实例,会一直在内存中,不会被GC清除的(原因是静态的属性变量不会被GC清除),除非整个应用退出了JVM

其实只要明确一点:静态方法是类方法,类方法面向过程;而单例模式是唯一实例对象,面向对象;以上的几点区别都基于此。所以静态方法一般适用于做一些Util类,例如JsonUtil,StringUtil。而单例适合做唯一实例管理器-就像简单工厂模式中提到的单例+简单工厂模式日志处理器自增序号生成器

模式扩展

扩展了解下单例模式的唯一性的范围以及多例模式的概念,理论层面上打开一下眼界。

单例唯一性范围

单例类中对象的唯一性的作用范围是进程唯一的。

  • 集群唯一指的是进程内唯一、进程间也唯一,进程内、进程间都唯一
  • 进程唯一指的是进程内唯一、进程间不唯一,线程内、线程间都唯一
  • 线程唯一指的是线程内唯一、线程间不唯一。

对于 Java 语言来说,单例类对象的唯一性的作用范围并非仅仅是进程,而是类加载器(Class Loader),因为不同的类加载器也会生成不同的单例类,进而生成不同的单例对象。

集群唯一单例实现方式

要想创建集群内唯一单例,我们需要通过借助外部文件的方式

  1. 把单例对象序列化并存储到外部共享存储区(比如文件)
  2. 进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用。一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁
  3. 使用完成之后还需要再存储回外部共享存储区

以上条件的伪代码实现如下:懒汉式单例实现

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略,比如文件地址*/);
  //分布式锁确保集群内单进程锁定对象
  private static DistributedLock lock = new DistributedLock();
  private IdGenerator() {}
  public synchronized static IdGenerator getInstance() 
    if (instance == null) {
      lock.lock();
      instance = storage.load(IdGenerator.class);
    }
    return instance;
  }
  public synchroinzed void freeInstance() {
    storage.save(this, IdGeneator.class);
    instance = null; //释放对象
    lock.unlock();
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}
// IdGenerator使用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
idGenerator.freeInstance();

线程唯一单例实现方式

我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例:饿汉式单例实现(+线程安全集合框架)

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static final ConcurrentHashMap<Long, IdGenerator> instances
          = new ConcurrentHashMap<>();
  private IdGenerator() {}
  //ConcurrentHashMap保证线程安全
  public static IdGenerator getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }
  public long getId() {
    return id.incrementAndGet();
  }
}

多例模式的概念

单例指的是一个类只能创建一个对象。对应地,多例指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象:饿汉式多例实现

public class BackendServer {
  private long serverNo;
  private String serverAddress;
  private static final int SERVER_COUNT = 3;
  private static final Map<Long, BackendServer> serverInstances = new HashMap<>();
  static {
    serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
    serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
    serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
  }
  private BackendServer(long serverNo, String serverAddress) {
    this.serverNo = serverNo;
    this.serverAddress = serverAddress;
  }
  public BackendServer getInstance(long serverNo) {
    return serverInstances.get(serverNo);
  }
  public BackendServer getRandomInstance() {
    Random r = new Random();
    int no = r.nextInt(SERVER_COUNT)+1;
    return serverInstances.get(no);
  }
}

这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象

总结一下

今天总结了一套学习设计模式的模版来不断套用学习模式,并且对历史学习的单例进行了一个整合性学习,加深了单例的概念内容探究。重新回顾了饿汉式单例实现、懒汉式单例实现、双检锁单例实现、静态内部类单例实现以及常规单例的反射和序列化破坏,最终祭出大杀器:枚举单例,算是一篇大汇总吧,同时也发散了一下自己的思维,单例的唯一性在什么限定范围下?多例模式是什么?很多看起来单例的应用场景是如何甄别为不适用于单例,单例的反模式反在哪里?以及单例的最佳应用场景:对于一个类判定其后续不扩展且不依赖外部并且试图全局唯一最好使用单例模式

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
|
5月前
|
设计模式 Java Spring
Java 设计模式之责任链模式:优雅处理请求的艺术
责任链模式通过构建处理者链,使请求沿链传递直至被处理,实现发送者与接收者的解耦。适用于审批流程、日志处理等多级处理场景,提升系统灵活性与可扩展性。
580 2
|
5月前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
499 0
|
7月前
|
设计模式 缓存 Java
Java设计模式(二):观察者模式与装饰器模式
本文深入讲解观察者模式与装饰器模式的核心概念及实现方式,涵盖从基础理论到实战应用的全面内容。观察者模式实现对象间松耦合通信,适用于事件通知机制;装饰器模式通过组合方式动态扩展对象功能,避免子类爆炸。文章通过Java示例展示两者在GUI、IO流、Web中间件等场景的应用,并提供常见陷阱与面试高频问题解析,助你写出灵活、可维护的代码。
|
5月前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
935 35
|
5月前
|
设计模式 消息中间件 传感器
Java 设计模式之观察者模式:构建松耦合的事件响应系统
观察者模式是Java中常用的行为型设计模式,用于构建松耦合的事件响应系统。当一个对象状态改变时,所有依赖它的观察者将自动收到通知并更新。该模式通过抽象耦合实现发布-订阅机制,广泛应用于GUI事件处理、消息通知、数据监控等场景,具有良好的可扩展性和维护性。
455 8
|
设计模式 缓存 安全
Java设计模式的单例模式应用场景
Java设计模式的单例模式应用场景
361 4
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
226 4
|
设计模式 存储 负载均衡
【五】设计模式~~~创建型模式~~~单例模式(Java)
文章详细介绍了单例模式(Singleton Pattern),这是一种确保一个类只有一个实例,并提供全局访问点的设计模式。文中通过Windows任务管理器的例子阐述了单例模式的动机,解释了如何通过私有构造函数、静态私有成员变量和公有静态方法实现单例模式。接着,通过负载均衡器的案例展示了单例模式的应用,并讨论了单例模式的优点、缺点以及适用场景。最后,文章还探讨了饿汉式和懒汉式单例的实现方式及其比较。
【五】设计模式~~~创建型模式~~~单例模式(Java)
|
设计模式 安全 Java
Java 编程中的设计模式:单例模式的深度解析
【9月更文挑战第22天】在Java的世界里,单例模式就像是一位老练的舞者,轻盈地穿梭在对象创建的舞台上。它确保了一个类仅有一个实例,并提供全局访问点。这不仅仅是代码优雅的体现,更是资源管理的高手。我们将一起探索单例模式的奥秘,从基础实现到高级应用,再到它与现代Java版本的舞蹈,让我们揭开单例模式的面纱,一探究竟。
133 11