Java开发中为什么要使用单例模式详解
作者:鸭梨山大哎 发布时间:2023-12-24 21:56:40
一、什么是单例模式?
单例设计模式(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
中。在 UserController
和 OrderController
中,我们分别创建两个 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();
来源:https://blog.csdn.net/u010711495/article/details/117401445
猜你喜欢
- 一、三方库介绍地址:https://github.com/youth5201314/banner介绍:从学习 Android 开始到现在工作
- Android Studio 打包 jar 及 aar 包创建工程 New -> Module -> Library在gradl
- 常见Bean后处理器的作用:public static void main(String[] args) { &
- RecyclerView 滑动时的优化处理,在滑动时停止加载图片,在滑动停止时开始加载图片,这里用了Glide.pause 和Glide.r
- 本文实例展示的是一个自定义的定时器组件,有别于.NET Framework里面提供的几个Timer。首先说说该组件开发背景,发现现在手头上的
- 本文实例为大家分享了Unity实现弧形移动效果的具体代码,供大家参考,具体内容如下一、实现效果二、第一种实现方法——弧形插值using Un
- 本文实例讲述了Android编程开发之TextView控件用法。分享给大家供大家参考,具体如下:这里我们会讲讲常用控件的使用。在今后的大多数
- 因重定向无法正常goBack()解决方案首先说下问题,初始页面为A,点击某个链接跳转到B(http://xxx.com.cn/),B页面重定
- 前言ConcurrentHashMap是Java 5中支持高并发、高吞吐量的线程安全HashMap实现。我们知道,ConcurrentHas
- 1.两种取值方式的差异mapper.xml映射文件<select id="selectEmployeeByCondition
- 目录前言1、什么叫循环依赖呢2、具体出现循环依赖的代码逻辑3、解决循环依赖的代码实现总结前言本文基于springboot版本2.5.1 &n
- 学习大佬们开发安全小工具,打包jar解决错误: 找不到或无法加载主类 main1 Maven方式遇到报错”找不到或无法加载主类 main“解
- SpringBoot2之PUT请求接收不了参数的解决办法,这个问题,关乎两个Filter过滤器,是spring3和3.5之后提供的,目的就是
- 概览1. 基于链表的可选有界阻塞队列。根据FIFO的出入队顺序,从队列头部检索和获取元素,在队列尾部插入新元素。2. 当作为有界阻塞队列,在
- Invoke Phing targets这个插件主要是读取xml形式包括自动化测试打包部署的配置文件,然后根据流程走下来。用ph
- C#限速下载网络文件的方法,具体如下:using System;using System.Collections.Concurrent;us
- 在Android开发过程中,如果Android系统自带的属性不能满足我们日常开发的需求,那么就需要我们给系统控件添加额外的属性了。假如有个需
- 本篇我们讲解下使用spring创建bean的几种方式,创建bean,也可以叫组件注册,就是把单例bean放到spring容器中。我们定义如下
- 曾经做过一个项目,其中登录界面的交互令人印象深刻。交互设计师给出了一个非常作的设计,要求做出包含根据情况可变色的下划线,左侧有可变图标,右侧
- 前言《英文猜词游戏》代码行数没有超过200行,是之前为了背英语单词,特意研发的小游戏。主要设计1.事先准备单词文本。2.为了让玩家能与程序互