Java开发中为什么要使用单例模式详解

 更新时间:2021年06月01日 15:59:37   作者:鸭梨山大哎  
单例对于大家来说并不陌生,但是在什么时候用单例呢?为什么要用呢?本文就带大家了解一下为什么要使用单例,文中有非常详细的介绍,需要的朋友可以参考下

一、什么是单例模式?

单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

二、实战案例一:处理资源访问冲突

我们先来看第一个例子。在这个例子中,我们自定义实现了一个往文件中打印日志的 Logger 类。具体的代码实现如下所示:

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

// 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/wangzheng/log.txt 中。在 UserControllerOrderController 中,我们分别创建两个 Logger 对象。在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login()create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。

为什么会出现互相覆盖呢?我们可以这么类比着理解。在多线程环境下,如果两个线程同时给同一个共享变量加 1,因为共享变量是竞争资源,所以,共享变量最后的结果有可能并不是加了 2,而是只加了 1。同理,这里的 log.txt 文件也是竞争资源,两个线程同时往里面写数据,就有可能存在互相覆盖的情况

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

public class Logger {
  private FileWriter writer;

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

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

在这里插入图片描述

我这里稍微补充一下,在刚刚的讲解和给出的代码中,我故意“隐瞒”了一个事实:我们给 log() 函数加不加对象级别的锁,其实都没有关系。因为 FileWriter 本身就是线程安全的,它的内部实现中本身就加了对象级别的锁,因此,在外层调用 write() 函数的时候,再加对象级别的锁实际上是多此一举。因为不同的 Logger 对象不共享 FileWriter 对象,所以,FileWriter 对象级别的锁也解决不了数据写入互相覆盖的问题

那我们该怎么解决这个问题呢?实际上,要想解决这个问题也不难,我们只需要把对象级别的锁,换成类级别的锁就可以了。让所有的对象都共享同一把锁。这样就避免了不同对象之间同时调用 log() 函数,而导致的日志覆盖问题。具体的代码实现如下所示:

public class Logger {
  private FileWriter writer;

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

除了使用类级别锁之外,实际上,解决资源竞争问题的办法还有很多,分布式锁是最常听到的一种解决方案。不过,实现一个安全可靠、无 bug、高性能的分布式锁,并不是件容易的事情。除此之外,并发队列(比如 Java 中的 BlockingQueue)也可以解决这个问题:多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件。这种方式实现起来也稍微有点复杂。

相对于这两种解决方案,单例模式的解决思路就简单一些了。单例模式相对于之前类级别锁的好处是,不用创建那么多 Logger 对象,一方面节省内存空间,另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能随便浪费)

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

按照这个设计思路,我们实现了 Logger 单例类。具体代码如下所示:

public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();

  private Logger() {
    File file = new File("/Users/wangzheng/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());
  }
}

三、实战案例二:表示全局唯一类

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。再比如,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

import java.util.concurrent.atomic.AtomicLong;
public 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();
  }
}

// IdGenerator使用举例
long id = IdGenerator.getInstance().getId();

到此这篇关于Java开发中为什么要使用单例模式详解的文章就介绍到这了,更多相关Java单例模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅析Java迭代器Iterator和Iterable的区别

    浅析Java迭代器Iterator和Iterable的区别

    Java语言中,Iterator和Iterable都是用来遍历集合类数据结构的接口,虽然它们有很多相似的地方,但在具体实现中却有着一些不同之处,本文将详细分析它们的区别,并提供相应的代码示例,需要的朋友可以参考下
    2023-07-07
  • 谨慎使用Java8的默认方法

    谨慎使用Java8的默认方法

    为什么要谨慎使用Java8的默认方法?本文给出了为什么要慎用Java8默认方法的原因,解释的很详细,感兴趣的朋友可以参考一下
    2016-01-01
  • Spring websocket并发发送消息异常的解决

    Spring websocket并发发送消息异常的解决

    本文主要介绍了 Spring websocket并发发送消息异常的解决,当多个线程同时尝试通过 WebSocket 会话发送消息时,会抛出异常,下面就来解决一下,感兴趣的可以了解一下
    2023-09-09
  • Java源码解析ArrayList及ConcurrentModificationException

    Java源码解析ArrayList及ConcurrentModificationException

    今天小编就为大家分享一篇关于Java源码解析ArrayList及ConcurrentModificationException,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • 详解Java创建多线程的四种方式以及优缺点

    详解Java创建多线程的四种方式以及优缺点

    这篇文章主要介绍了Java创建多线程的四种方式以及优缺点,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • Netty中解码器的作用及实现详解

    Netty中解码器的作用及实现详解

    这篇文章主要介绍了Netty中解码器的作用及实现详解,本章我们只需要让客户端发送消息的时候循环发送100次,服务端不变,看看服务端是不是接收到了100条消息,需要的朋友可以参考下
    2023-12-12
  • java中List对象列表实现去重或取出及排序的方法

    java中List对象列表实现去重或取出及排序的方法

    这篇文章主要介绍了关于java中List对象列表实现去重或取出以及排序的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面跟着小编来一起学习学习吧。
    2017-08-08
  • 带你走进Maven的大门-最全Maven配置及集成idea工具总结

    带你走进Maven的大门-最全Maven配置及集成idea工具总结

    Maven项目对象模型(POM),是一个项目管理工具可以通过一小段描述信息来管理项目的构建,报告和文档的软件.那我们想要在IDEA中使用Maven得进行一些配置,接下来我们具体看一下是如何配置使用的,需要的朋友可以参考下
    2021-06-06
  • java实现猜字母游戏

    java实现猜字母游戏

    这篇文章主要为大家详细介绍了java实现猜字母小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-03-03
  • springboot连接不同数据库的写法详解

    springboot连接不同数据库的写法详解

    这篇文章主要介绍了springboot连接不同数据库的写法 ,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-04-04

最新评论