单例模式【JavaEE初阶】

news2024/9/28 21:19:45

一、单例模式的概念

        单例模式是一种常见的设计模式 。单例模式希望:有些对象,在一个程序中应该只有唯一一个实例,就可以使用单例模式 。换句话说,在单例模式下,对象的实例化被限制了,只能创建一个,多了也报错创建不了 

单例模式的设计:类似于棋谱,是 前辈们已经总结好了的一些固定套路,照着棋谱来下棋,棋就不会下的太差,这就提高了下限 

二、单例模式的简单实现

在 Java 里面的单例模式,有很多种实现方式,在这里主要介绍两个大类:饿汉模式、懒汉模式 

饿汉模式 和 懒汉模式 两种模式,描述了创建实例的时间。

比如说,想要了解某个资料(大文件 10G,存放在硬盘中),那么 此时使用某个编辑器,打开文件,就会出现两种情况:

  • 饿汉:把 10G 都读到内存中,读取完毕之后 再允许用户进行查看和修改~
  • 懒汉:只读取一点点(当前屏幕能显示出的范围),随着用户翻页,继续再读后续内容~

        所以说,如果是 饿汉模式,那么显示所需要的时间就会比较多;如果是 懒汉模式,那么显示的时间就会比较低,效率就会比较高(也有可能 用户打开文件以后,只看了两眼就关了,后面的大部分都没有读,那么内存读了那么多也没有意义) 

所以,通常我们都认为,懒汉模式 要比 饿汉模式 更高效 

像 刷抖音、看小说、看微信、上网浏览内容等 都是借鉴了 懒汉模式 

2.1 饿汉模式

饿汉模式 的意思是,程序一旦启动,就会立刻创建实例 。

这就好比,一个饿了的人,看到一张饼,就会迫不及待的往嘴里塞,我们把它叫做 "饿汉" 

static关键字 的来龙去脉:

static 名字叫做 "静态",但是实际上和字面意思没有任何的关系,这是一个历史遗留的问题~

实际表示的含义是 "类属性 / 类方法",同样的,我们把 不是静态的普通的成员叫做 "实例属性 / 实例方法" 

Java 里面叫做 "静态" 是因为 C++ 里面表示 "类属性",就是用 static,Java 是从 C++ 那里抄来的;而 C++ 则是因为 引入面向对象之后。需要搞一个方式来定义类属性,就需要引入一个关键字,但是引入新的关键字 成本极高,所以关键字设计者的大佬们 目光就盯住了 旧的关键字 

于是,static 就中招了,static 原来表示的是 变量放到静态内存区,但是随着时间的推移,系统的进化,已经没有 "静态内存区" 这个说法了,但是 static关键字 还在,于是 "旧瓶装新酒",就用来表示 "类属性 / 类方法" 了~

此时,"类属性 / 类方法" 和 静不静态 字面意思上没有啥关系,只是 随便找一个之前旧的关键字,现在没啥用了,赋予一个新的功能,仅此而已~

引入新的关键字成本极高的原因:

写代码的时候,变量名不能和关键字一样,当引入新的关键字的时候,不可以确定 其他人是不是已经引入了 新的关键字 作为变量名 

更大的可能是 新的关键字一引入,就会导致已有的一些代码 编译就会失败 

然后这把火就会烧到了 关键字设计者 的身上~而类属性就长在类对象上,类对象在整个程序中只有唯一一个实例(JVM保证的) ,所以说 类的静态成员就只有唯一一个了 

package thread;
//单例模式:饿汉模式
class Singleton { 
    //在此处,先把实例给创建出来了
    private static Singleton instance = new Singleton();

    //后续如果需要这个实例,就需要统一基于 getInstance 方法来获取
    public static Singleton getInstance() {
        return instance;
    }
    //为了避免Singleton类不小心被实例化,要把构造方法设为私有
    //此时在类外面,其他类想来 new 就不可以了 (通过 编译器的规则来确保只有一个实例对象)
    //
    private Singleton() {
    }
}


public class Demo19 {
    public static void main(String[] args) {
        //饿汉模式 的调用
        Singleton instance = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        Singleton instance3 = new Singleton();
        System.out.println(instance == instance2);  //true
    }
}

运行结果:

如果不小心 想创建另一个实例,那么就会编译报错了:

总结:

static的作用:

  1. static让当前instance属性成为类属性,类属性是长在类对象上的,类对象又是唯一实例的(只在类加载阶段被创建出一个实例),那么就保证这个实例的唯一性
  2. 保证这个实例确实在一定时机中被创建出来

构造方法设为private,那么除自己类,外面的类都无法new

使用 静态成员表示实例(唯一性) + 让构造方法设为私有(堵住了 new 创建新实例的口子) 

按照上面的代码,当 Singleton类 被加载的时候,就会执行到 实例化操作,此时 实例化的时机非常早(非常迫切的感觉),我们把它称为 饿汉模式 

对于饿汉模式来说,在多线程的情况下,多次调用的是 getInstance() 方法, 而 这个方法只是一个 读操作,对于多线程读操作来说,是线程安全的 

2.2 懒汉模式

懒汉模式 的意思是,程序启动,先不着急创建实例,等到真正用的时候,再创建实例 

这个也很形象,比较 "懒",不想干活,等到必要的时候再去干活 

//懒汉模式的实现
class SingletLazy {
    //此处没有立即创建实例
    private static SingletLazy instance = null;
 
    //当首次调用 getInstance() 的时候,才会创建实例
    public static SingletLazy getInstance() {
        if (instance == null) {
            instance = new SingletLazy();
        }
        return instance;
    }
 
    //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    private SingletLazy() { }
}

在多线程的情况下,懒汉模式,多次调用 getInstance() 方法,而且涉及到了 两次读操作(读出 instance 是否为空,读出 返回的instance值)和 一次写操作(修改 instance 变量的值),这是线程不安全的 

当然,一旦实例创建好了以后,后续 if 条件语句就进不去了,此时也就是 全是读操作了,也就线程安全了 

既然已经明确了,懒汉模式 是线程不安全的,那么 如何解决懒汉模式线程不安全的问题呢?

办法就是 需要加锁!!!

通过 加锁 来保证 "判断" 和 "修改" 这组操作是原子的(这里的线程安全本质是读和写这三个操作不是原子的,导致t2读到的数据可能是t1还没来得及写的(脏读))

//懒汉模式的实现
class SingletLazy {
    //此处没有立即创建实例
    private static SingletLazy instance = null;
 
    //当首次调用 getInstance() 的时候,才会创建实例
    public static SingletLazy getInstance() {
        synchronized (SingletLazy.class) {
            if (instance == null) {
                instance = new SingletLazy();
            }
        }
        return instance;
    }
 
    //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    private SingletLazy() { }
}

懒汉模式,只是在初始情况下才会有线程不安全的问题,一旦实例创建好了以后,此时就安全了.

所以说,在后续调用 getInstance 的时候就不应该尝试加锁了 

如果使用上述的代码,无论 instance 是否为空(是否初始化),都会进行加锁,使得锁竞争加剧,消耗一些没有必要消耗的资源,就会很影响效率了 

在加锁之前,还需要进行判断 instance 是否为空(是否初始化),如果为空才会进行加锁: 

//懒汉模式的实现
class SingletLazy {
    //此处没有立即创建实例
    private static SingletLazy instance = null;
 
    //当首次调用 getInstance() 的时候,才会创建实例
    public static SingletLazy getInstance() {
        if (instance == null) {
            synchronized (SingletLazy.class) {
                if (instance == null) {
                    instance = new SingletLazy();
                }
            }
        }
        return instance;
    }
 
    //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    private SingletLazy() { }
}
 

分析:

外层 if 判定当前是否已经初始化好,如果未初始化好,就尝试加锁;如果已经初始化好,那么就接着往下走~

里层 if 是在多个线程尝试初始化,产生了锁竞争,这些参与竞争的线程 拿到锁之后,再进一步确认,是否真的要初始化~

当然,上面的代码操作还是有一些问题的 —— 有的线程在读,有的线程在写~

这就联想起了 —— 内存可见性问题~

其实,这里的情况 和 之前的情况还不一样,每一个线程都有自己的上下文,都有自己的寄存器内容,按理来说 是不应该会出现优化的~

但是,实际上也不好说,也并不能保证 编译器优化 是啥样的过程~

因此,给 instance 加上 volatile 是更加稳健的做法~

如果不加 volatile 不一定会有问题,但是 稳妥起见,还是加上更好 

这里的volatile有两个功能:

  1. 解决内存可见性
  2. 禁止指令重排序
//懒汉模式的实现
class SingletLazy {
    //此处没有立即创建实例
    volatile private static SingletLazy instance = null;
 
    //当首次调用 getInstance() 的时候,才会创建实例
    public static SingletLazy getInstance() {
        if (instance == null) {
            synchronized (SingletLazy.class) {
                if (instance == null) {
                    instance = new SingletLazy();
                }
            }
        }
        return instance;
    }
 
    //同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
    private SingletLazy() { }
}

总结:

懒汉模式 线程的三个要点:

  1. 加锁
  2. 双重 if
  3. volatile(不加可能是错的,但加了一定是正确的)

关于 单例模式 的内容就先介绍到这里了,当然,在 Java 中也有许多其他的方式 也可以实现单例模式,如 基于枚举、基于内部类 等等 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/55682.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mybatis源码解析(七):Mapper代理原理

Mybatis源码系列文章 手写源码(了解源码整体流程及重要组件) Mybatis源码解析(一):环境搭建 Mybatis源码解析(二):全局配置文件的解析 Mybatis源码解析(三):映射配置文件的解析 Mybatis源码解析(四):s…

使用R语言对S&P500股票指数进行ARIMA + GARCH交易策略

在本文中,我想向您展示如何应用S&P500股票市场指数的交易策略。最近我们被客户要求撰写关于交易策略的研究报告,包括一些图形和统计输出。 通过组合ARIMA GARCH模型,从长期来看,我们可以超过“买入并持有”方法。 相…

【MySQL基础】常用指令详解

如果看不清未来,就走好当下的路,做你此刻该去做的事。——《冰雪奇缘2》 目录 1、进入和退出mysql 1.1进入mysql 1.2退出mysql 2、查看mysql中有哪些数据库 2.2.创建数据库 3、使用数据库 3.1开始使用数据库 3.2展示数据库中的表 4、查看表中的…

跨境电商面临“寒冬”考验,如何转型升级入局新赛道(Starday)

近几年随着互联网和高新技术的飞速发展,加之疫情下各国海外贸易政策的管理,跨境贸易模式不断地创新升级,现今的跨境贸易模式已经从线下交易上升为线上交易,各种基于互联网商务网站的电子商务业务和网络公司开始不断地涌现&#xf…

WebDAV之葫芦儿•派盘+FolderSync

FolderSync 支持WebDAV方式连接葫芦儿派盘。 随着业务发展,文件数据增长,如文档更新、资料下载、拍照录像等。如何更好的管理这些资料,不出现丢失的问题就成为了一个很大的问题。也正是有了类似的需求,现在网络上出现了很多的文件同步备份软件。那么,文件同步备份软件哪…

Thread类的start()方法创建线程的底层分析

在Java中通过如下简单代码就可以创建一个新线程 Thread thread new Thread(new Runnable() {Overridepublic void run() {//do something} }); thread.start(); 在start()中又调用了start0(),它才是真正创建线程的方法。 public synchronized void start() {gro…

安全机制(security) - 加解密算法 - 对称加密 - 加解密模式

说明 大部分对称加密算法支持多种加密模式,每种模式的运算结果也不相同。加解密模式是分组加密算法通用的机制,不同算法可能支持相同的加密模式,不同算法支持的加密模式也可能不同。加密和解密需要使用相同的模式才能得到正确的结果。不同的…

CANOE功能介绍

1.CANoe主界面 当计算机安装完CANoe后,用户只需选择“开始”→“所有程序 ”→Vector CANoe 11.0→CANoe 11.0 系 统 菜 单 命 令 即 可 启 动CANoe。 为了快速熟悉CANoe的常用功能,我们可以打开Vector官方的自带例程,一边学习一边实践相关功…

超算/先进计算如何改变现如今对的生活

算力作为新一代的“石油”,与超算/先进计算有着不可分割的紧密联系。 通俗而言,算力泛指计算能力,即数据处理能力。算力大小代表数据处理能力的强弱。从远古的结绳计算到近代的机械式计算,再到现代的数字电子计算,特别…

Ajax学习:设置CROS响应头实现跨域(跨域资源共享)

CROS:跨域资源共享、是官方的跨域解决方案&#xff0c;特点不需要在客户端做任何特殊的操作&#xff0c;完全在服务器中处理&#xff08;支持get post 等&#xff09; 客户端做ajax请求&#xff0c;服务端做相应头设置就可以实现跨域&#xff1a; <!DOCTYPE html> <h…

如何快速构建研发效能度量的指标体系?

本月初&#xff0c;没毛病软件公司的研发总监 Kevin 在参加完公司管理层月度例会后&#xff0c;心情非常糟糕...... 刚才会议中&#xff0c;老板很严肃地问研发总监 Kevin&#xff1a;“我在会议前接到了客户的投诉电话&#xff0c;说产品出现了 Bug&#xff0c;这已经不是第一…

.net-----Windows 窗体应用程序包括控件,对话框,多重窗体,绘制图形,菜单和工具栏

目录前言Windows窗体应用程序概述&#xff1b;窗体和大部分控件常用的事件创建Windows窗体应用程序使用Visual Studio集成开发环境实现Hello World程序使用常用Windows窗体控件&#xff1b;Label、TextBox、RichTextBox、Button应用示例单选按钮、复选框和分组【例】RadioButto…

(附源码)springboot物流配货管理系统 毕业设计 250858

基于springboot物流配货管理系统的设计与实现 摘 要 信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题.针对物流配货等问题,对物流配货进行研究分析,然后…

电力系统机组组合优化调度(IEEE14节点、IEEE30节点、IEEE118节点)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f4dd;目前更新&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;电力系统相关知识&#xff0c;期刊论文&…

数云融合丨知识图谱在烟草零售数字化转型中的应用

一、知识图谱的趋势 随着互联网、云计算、大数据、人工智能等信息数据技术的快速发展&#xff0c;计算机的智能化程度也越来越高&#xff0c;知识图谱作为人工智能的核心技术&#xff0c;其在数据集成、语义表示和逻辑推理等方面存在着得天独厚的优势。 2021年&#xf…

Java并发-交替打印的四种方法。

1 前言 如下图所示&#xff0c;现在有两个线程A,B&#xff1b;A打印12345&#xff0c;B打印abcde&#xff0c;结果为1a2b3c4d5e交替输出。 1.1 采用wait和notify 【分析】我们要求线程A始终先打印&#xff0c;因此在线程B先获得CPU使用时间时也应该阻塞。 细节 线程A应该打印…

【人工智能/算法】搜索求解(Solving Problemsby Searching)

文章目录一、求解与搜索二、盲目式搜索1. 深度优先搜索&#xff08;Depth First Search, DFS&#xff09;回溯搜索&#xff08;Backtracking Search&#xff09;2. 广度优先搜索&#xff08;Breadth First Search, BFS&#xff09;一致代价搜索&#xff08;Uniform-cost Search…

TLog轻量级分布式日志标记追踪神器

文章目录TLog简介项目特性安装TLogspringboot依赖spring native依赖日志框架适配方式(举例Log4j框架适配器)任务框架支持(举例XXL-JOB框架)TLog架构图TLog简介 TLog通过对日志打标签完成企业级微服务的日志追踪。它不收集日志&#xff0c;使用简单&#xff0c; 产生全局唯一的…

Actor 生命周期

一&#xff0c;一览图 二&#xff0c; 大致流程 三&#xff0c;细节 从磁盘加载 已位于关卡中的 Actor 使用此路径&#xff0c;如 LoadMap 发生时、或 AddToWorld&#xff08;从流关卡或子关卡&#xff09;被调用时。 包/关卡中的 Actor 从磁盘中进行加载。 PostLoad - 在序…

支持向量机(SVM)—— 详细推导及案例应用可视化

支持向量机&#xff08;SVM&#xff09; 1. 什么是支持向量机&#xff1f; 在上图中&#xff0c;我们想在二维平面中通过画出一条分界线将黑点与白点分开&#xff0c;很明显&#xff0c;我们认为在上面所画的三条分界线中H3H_3H3​是最好的&#xff0c;因为H1H_1H1​压根就没有…