java中单例模式讲解
作者:看山 发布时间:2022-05-22 14:24:07
个人认为单例模式是设计模式中最简单也是最常用的一种,是对有限资源合理利用的一种方式。这个模式看似简单,但是其中蕴含了关于并发、类加载、序列化等一系列深层次的知识,如果理解不够深,就有可能在高并发时遇到难以预期的异常,或者会造成资源浪费。
所以本文会从将目前Java领域最常用的几种单例模式列出来,供大家参考。
WHAT
* 给出了解释、实现的思路以及应该注意的地方:
单例模式,也叫单子模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
类图是:
WHY
正如定义所说,单例模式就是整个内存模型中,只有一个实例。实例少了,内存占用就少。同时,只有一个实例,也就只需要构建一个对象,计算就少。对于构造过程中需要大量计算或者占用大量资源的对象,只创建一次,就减少了资源占用和内存占用。
HOW
饿汉式
饿汉式是最简单的一种实现,在类装载过程中,完成实例化,避免多线程问题。
实现一:静态实例参数与静态代码块
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
根据java的特性,饿汉式还可以变种写法,有的地方称为静态代码块方式:
public class EagerSingleton {
private static EagerSingleton INSTANCE = null;
static {
INSTANCE = new EagerSingleton();
}
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
这两种方式只是在写法上的区别,优缺点没有区别,只是借助Java语言特性的不同写法,所以归为一类。
饿汉式有两个明显的缺点:
1.类装载过程即完成实例化,如果整个应用生命周期内,实例没有使用,也就是浪费资源了。
2.因为没有办法向构造函数传递不同的参数,如果需要通过个性化参数定制实例时,这种方式就不支持了。
实现二:静态内部类
针对饿汉式第一个缺点,我们可以借助静态内部类的方式,将对象实例化的时间延后。
public class EagerSingleton {
private EagerSingleton() {
}
private static class EagerSingletonInstance {
private static final EagerSingleton INSTANCE = new EagerSingleton();
}
public static EagerSingleton getInstance() {
return EagerSingletonInstance.INSTANCE;
}
}
但是,依然不能很好的解决第二个缺点,如果需要根据不同的参数实现不同的实例,可以采用下面说的懒汉式实现。
懒汉式
懒汉式比饿汉式的一个优点,就是能够在使用的时候再进行实例化。但是,馅饼总是要伴随着陷阱,懒汉式写法有更多的坑,一不小心就摔着了。
错误一:单线程实现
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
之所以定义为单线程实现,是因为INSTANCE == null
这个判断,一个线程通过这个判断,开始进行对象实例化,但是还没有实例化完成,另一个线程又来了,这个时候,对象还没有实例化,就也会开始进行实例化,造成不必要的浪费。
错误二:同步方法
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
这种方式解决了多线程的问题,但是也引入了新的性能问题:太慢。synchronized
把整个方法包起来,也就是每个线程进入的时候,都需要等待其他线程结束调用,才能拿到实例,在性能敏感的场景,是比较致命的。
错误三:同步代码块之单次检查
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazySingleton.class) {
INSTANCE = new LazySingleton();
}
}
return INSTANCE;
}
}
这种写法看似将同步代码缩小,但也缩小了多线程保障,也犯了第一种写法的错误,属于没有对多线程有基本了解写出的低级错误代码。
错误四:同步代码块之双重检查
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
}
}
return INSTANCE;
}
}
这种写法在一定程度上属于正确的写法,双重判断可以很好的实现线程安全和延迟加载。如果到这里就结束,那就是谬以千里的毫厘之差。
双重检查和同步代码块都没有问题,问题出在INSTANCE = new LazySingleton()
这句话。在JVM中,为了充分利用CPU计算能力,会进行重排序优化,INSTANCE = new LazySingleton()
做了三件事:
1.为 INSTANCE 初始化栈空间
2.为 LazySingleton 分配内存空间,实例化对象
3.INSTANCE 指向 LazySingleton 实例分配的内存空间
因为重排序优化的存在,真正执行的过程中,可能会出现1-2-3的顺序,也可能出现1-3-2的顺序。如果是1-3-2,INSTANCE 指向了 LazySingleton 实例分配的内存空间后,就不是null,另外一个线程进入判断null时,就会直接返回 INSTANCE,但是这个时候 LazySingleton 实例化还没有完成,就可能出现意想不到的异常。
正确:双重检查+阻止重排序
public class LazySingleton {
private static volatile LazySingleton INSTANCE = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
}
}
return INSTANCE;
}
}
这种写法比上面那种,就差在volatile
这个关键字。
枚举
懒汉式和饿汉式都能够适用于多线程并发场景,但是通过反序列化或反射可以实例化对象,这样依然不能满足单例模式的要求,所以可以借助枚举实现,枚举可以完美避免多线程并发问题,而且可以防止反序列化和反射创建新对象。第一次看到这样定义单例模式,是在《Effective Java》中,多读经典书还是挺好的。
public enum EnumSingleton {
INSTANCE;
public void method1() {
// do something
}
public Object method2() {
// do something and return something else
return new Object();
}
}
在开发实践中,枚举可以满足绝大部分场景,而且写法简单,定义单例的逻辑只需要三行代码,简洁而不简单,三行代码可以保证线程安全。同时枚举的反序列化只是通过name查找对象,不会产生新的对象;根据JVM规范,通过反射创建枚举对象时,会抛出IllegalArgumentException
异常。这样,相当于通过语法糖防止反序列化和反射破坏单例。
场景
1.无状态工具类:这种工具类不需要记录状态,只保证正确的应用就行,可以通过单例模式来定义。
2.数据共享:即多个不相关的两个线程或者进程之间实现通信。因为是一个实例,如果它的属性或者变量值被修改,所有引用都是同时修改的,当然需要 volatile 来定义变量。比如网站的计数器。
3.日志应用:通常应用会向日志文件写日志信息,为了实时向文件写,通常会使用单例模式,保证有一个实例持有文件,然后进行操作。
4.数据库连接池:数据库连接是一种数据库资源,使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,通过单例模式来维护,就可以大大降低这种损耗。
5.Web应用的配置对象:读取文件需要消耗时间,如果读取大文件,消耗的时间和资源更久,所以通过单例模式可以大大降低消耗。
来源:https://blog.csdn.net/liuxinghao/article/details/104084369


猜你喜欢
- (一)springboot web项目打jar包1、打包两种打包方式maven命令打包切换目录到工程根下,pom.xml所在位置,运行mav
- Broadcast Receiver简介 Broadcast Receiver是Android的五大组件之一,使用频率也很高。 用于异步接收
- Java停止线程的逻辑(协同、通知)在Java程序中,我们想要停止一个线程可以通过interrupt方法进行停止。但是当我们调用interr
- 今天写这篇文章的缘由是前一段时间一个网友在我的博客上面留言,想要实现在GridLayout(相当于九宫格)中点击每项可左右滑动显
- 准备过程:在电脑桌面 右键点击 “此电脑”的“属性”选项选择“高级系统设置”选项点击下面的“环境变量”选项配置过程:点击“系统变量”下面的”
- 不啰嗦,上菜 QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.se
- 这里我们通过Apache Commons CLI来完成目标功能,废话不多说直接上代码所需的maven依赖<dependency>
- 最近“全网域(Web Scale)”一词被炒得火热,人们也正在通过扩展他们的应用程序架构来使他们的系统变得更加“全网域”。但是究竟什么是全网
- 在项目中有使用到延时队列的场景,做个简单的记录说明;首先DelayQueue实现了BlockingQueue,加入其中的元素必须实现Dela
- 一、继承1、继承的概念继承机制:是面向对象程序设计是代码可以复用的最重要手段,允许程序员在保持原有类特性的基础上进行扩展,增加新的功能,产生
- 前言前段时间一直使用到word文档转pdf或者pdf转word,寻思着用Java应该是可以实现的,于是花了点时间写了个文件转换工具源码wel
- 前言枚举为我看日常开发的可读性提供的非常好的支持,但是有时在使用枚举类型时,我们需要取名称和值,甚至有时候还需要取枚举类型的描述。通过反射,
- 高快省的排序算法有没有既不浪费空间又可以快一点的排序算法呢?那就是“快速排序”啦!光听这个名字是不是就觉得很高端呢。假设我们现在对“6 1
- 单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径。在弄清楚如何求算单源最短路径问题之前,必须弄清楚最短路径的最优子
- 网络通信协议中的UDP通信是无连接通信,客户端在发送数据前无需与服务器端建立连接,即使服务器端不在线也可以发送,但是不能保证服务器端可以收到
- 创建一个TextHello类 TextHello类的代码如下@Controller@RequestMapping("/h
- 开发环境:android4.1.1实验功能:在第一个Hello World!为标签的activity中显示good,该界面中有一个名为Nex
- 有时候一些项目并不需要提供 Web 服务,例如跑定时任务的项目,如果都按照 Web 项目启动未免画蛇添足浪费资源为了达到非 Web 运行的效
- 简介最近学了java基础后对以前不会写的作业深有感触,想起以前各种在网上找资料找别人的代码参考,所以今天特地写了了简单的基于控制台的学生信息
- 功能需求:为软件设定一个使用有效期,当超过指定时间后,程序无法运行。实现思路:定义一个常量,用于记录一个时间,我们称之为标记时间,使用当前时