《Java-SE-第二十三章》之单例模式

文章目录

  • 单例模式概述
    • 饿汉模式
    • 懒汉模式单线程版
    • 懒汉单例多线程版
    • 枚举实现单例

单例模式概述

单例模式是设计模式中的一种,其作用能保证某个类在程序中只存在唯一一份实例,而不会创建多份实例。单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.。饿汉模式中的饿不并不是真饿了,而是说提前把单例类更创建好。懒汉模式中的懒则是当需要使用到单例类的时候才创建单例对象。这就类似于,每次吃饭的时候,已经提前把碗洗了有碗用,这就是"饿汉"。"懒汉"则是要用碗赶紧去把碗洗了。单例模式中的单例类有且只有一份,static修饰的成员变量是属于类的,也是只有一份,所以我们使用static修饰的成员变量保存 到实例对象的引用变量。

饿汉模式

在单例模式中一个类只有一个实例对象,所以避免外部通过构造方法创建对象,所以需要才构造方法私有,防止创建出多个对象。又因为我们是使用static来保存实例对象的引用,所以需要提供一个静态方法获取唯一的对象。

示例代码

public class HungrySingleton {
    /**
     * 类加载的时候创建出HungrySingleton对象,并用静态差成员变量保存。
     */
    private static HungrySingleton singleton = new HungrySingleton();

    private HungrySingleton() {
    }

    /**
     * 提供静态方法获取单例
     * @return
     */
    public static HungrySingleton getSingleton() {
        return singleton;
    }
}

上述的代码多线程环境下依旧是安全的,因为 getSingleton()方法中的return语句只涉及到读而不涉及修改。

懒汉模式单线程版

懒汉模式和饿汉模式相比就是创建实例的时机不一样,懒汉不是在类加载的时候创建而是在需要使用的时候创建。

示例代码

public class SlackerSingleton {
 
    private static SlackerSingleton singleton = null;
    private SlackerSingleton() {
    }

    /**
     * 提供静态方法获取单例
     *
     * @return
     */
    public static SlackerSingleton getSingleton() {
        //需要使用的时候创建对象
        if (singleton == null) {
            singleton = new SlackerSingleton();//真正创建的时机,创建好了,后续是直接返回。
        }
        return singleton;
    }
}

多线程的环境下,当t1线程进行比较singleton是否为空时,比较完之后,t1线程还没有创建实例。此时t2线程立马进来再次判断此时singleton依旧为空,导致t2线程也进行new操作,最终导致创建了多份实例。造成线程不安全的代码就是if语句以及实例的创建操作,所以我们需要对这段代码加锁。

懒汉单例多线程版

加锁后的代码

public class SlackerSingleton {

    private static SlackerSingleton singleton = null;
    private SlackerSingleton() {
    }

    /**
     * 提供静态方法获取单例
     *
     * @return
     */
    public static SlackerSingleton getSingleton() {
        //需要使用的时候创建对象
        synchronized (SlackerSingleton.class) {
            if (singleton == null) {
                singleton = new SlackerSingleton();
            }
        }
        return singleton;
    }
}

上述代码仅仅是针对首次创建实例的情况,如果singleton已经创建好了,if语句就啥用了,但是第二次进入该方法会去获取锁,而获取锁海外释放锁的是耗时耗力的。所以可以在加锁之前进行判断该实例是否已经创建好了,没有则获取锁,创建好了直接返回。

双重if的代码

public class SlackerSingleton {

    private static SlackerSingleton singleton = null;

    private SlackerSingleton() {
    }

    /**
     * 提供静态方法获取单例
     *
     * @return
     */
    public static SlackerSingleton getSingleton() {
        //需要使用的时候创建对象
        if (singleton == null) {//判断是否需要加锁
            synchronized (SlackerSingleton.class) {
                if (singleton == null) {//判断是否需要创建实例
                    singleton = new SlackerSingleton();
                }
            }
        }
        return singleton;
    }
}

优化到这里依旧是存在问题的,和这个锅就是编译器的了。上述代码需要判断isingleton == null,这个操作在多线程的情况下是非常频繁的,导致CPU频繁的从内存读取(Load)到寄存器然后进行比较(CMP),此时就会进行优化,线程不会去读取内存中数据,而是会从寄存器中读取数据,一旦当singleton值变化时,线程是感知不到的,就会造成内存可见性问题,除此之外,还有另一个问题就是由new SlackerSingleton();引起来的,创建对象大概可以分为三步:①:JVM为对象分配一块内存S,②:在内存S上对对象进行初始化,③:将内存S的地址赋值singleton变量。为了引出bug,我们假设有两个线程t1和t2同时使用getSingleton()方法。

正常情况按照对象创建①②③来执行。

第一步:线程t1直接运行到singleton = new SlackerSingleton();并且一口气执行完了①②③。

第二步:线程t2进行该方法判断singleton就直接返回了。

第三步:线程t2可以正常使用。

出现bug的情况按对象创建的步骤①③②来执行。

第一步:线程t1执行singleton = new SlackerSingleton();执行完①JVM为对象分配一块内存.③将内存的地址赋值给singleton 变量

第二步:线程t2进入该方法进行判断发现singleton不为空,拿到singleton 返回了。此时线程t2拿到的是singleton 的半成品对象,因为该对象没有初始化。

第三步:线程t2拿到该对象去使用就可能会出现异常。

综上所述,还存在两个隐含问题一个是内存可见性问题,另一个是指令重排序问题。解决这个问题只需要将静态变量加上volatile 即可。

volatile 修饰静态变量

public class SlackerSingleton {

    private static volatile SlackerSingleton singleton = null;

    private SlackerSingleton() {
    }

    /**
     * 提供静态方法获取单例
     *
     * @return
     */
    public static SlackerSingleton getSingleton() {
        //需要使用的时候创建对象
        if (singleton == null) {
            synchronized (SlackerSingleton.class) {
                if (singleton == null) {
                    singleton = new SlackerSingleton();
                }
            }
        }
        return singleton;
    }
}

此时多线程长场景下的懒汉模式完成。

枚举实现单例

因为枚举是天然的单例,并且枚举类型无法通过反射来获取封装的私有变量,非常安全。

示例代码

public enum Singleton {
     INSTANCE;
     public void businessMethod() {
          System.out.println("我是一个单例!");
     }
}

简单使用

public class SingletonDemo {
    public static void main(String[] args) {
        Singleton.INSTANCE.businessMethod();
    }
}

运行结果

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023年华数杯数学建模A题思路代码分析 - 隔热材料的结构优化控制研究

# 1 赛题 A 题 隔热材料的结构优化控制研究 新型隔热材料 A 具有优良的隔热特性,在航天、军工、石化、建筑、交通等 高科技领域中有着广泛的应用。 目前,由单根隔热材料 A 纤维编织成的织物,其热导率可以直接测出;但是 单根隔热…

【基于HBase和ElasticSearch构建大数据实时检索项目】

基于HBase和ElasticSearch构建大数据实时检索项目 一、项目说明二、环境搭建三、编写程序四、测试流程 一、项目说明 利用HBase存储海量数据,解决海量数据存储和实时更新查询的问题;利用ElasticSearch作为HBase索引,加快大数据集中实时查询数…

⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态

前置知识:类和对象 参考书籍:《C Primer 第五版》 目录 什么是面向过程?什么是面向对象? 一、封装 1、封装的含义以及如何实现封装 1.1 访问限定符(访问说明符) 1.2 什么是封装? 2、封装的优点…

css word-break

上面的一行还是可以放置很多个字符的,但是就是换行了。 要求填充满整行,超过在换行 加上word-break:break-all;就行

uniapp返回

// 监听返回事件onNavigationBarButtonTap() {uni.showModal({title: 提示,content: 确定要返回吗?,success: (res) > {if (res.confirm) {uni.navigateBack({delta: 2})}}})},

牛客网Verilog刷题——VL46

牛客网Verilog刷题——VL46 题目解析答案 题目 根据题目提供的双口RAM代码和接口描述,实现同步FIFO,要求FIFO位宽和深度参数化可配置。电路的接口如下图所示。   双口RAM端口说明: 同步FIFO端口说明: 双口RAM代码如下&#xff…

【LangChain】向量存储(Vector stores)

LangChain学习文档 【LangChain】向量存储(Vector stores)【LangChain】向量存储之FAISS 概要 存储和搜索非结构化数据的最常见方法之一是嵌入它并存储生成的嵌入向量,然后在查询时嵌入非结构化查询并检索与嵌入查询“最相似”的嵌入向量。向量存储负责存储嵌入数…

【Jmeter】配置不同业务请求比例,应对综合场景压测

目录 前言 Jmeter5.0新特性 核心改进 其他变化 资料获取方法 前言 Jmeter 5.0这次的核心改进是在许多地方改进了对 Rest 的支持,此外还有调试功能、录制功能的增强、报告的改进等。 我也是因为迁移到了Mac,准备在Mac上安装Jmeter的时候发现它已经…

机器学习---概述(一)

文章目录 1.人工智能、机器学习、深度学习2.机器学习的工作流程2.1 获取数据集2.2 数据基本处理2.3 特征工程2.3.1 特征提取2.3.2 特征预处理2.3.3 特征降维 2.4 机器学习2.5 模型评估 3.机器学习的算法分类3.1 监督学习3.1.1 回归问题3.1.2 分类问题 3.2 无监督学习3.3 半监督…

Scikit Learn识别手写数字 -- 机器学习项目基础篇(6)

Scikit learn是机器学习社区中使用最广泛的机器学习库之一,其背后的原因是代码的易用性和机器学习开发人员构建机器学习模型所需的几乎所有功能的可用性。在本文中,我们将学习如何使用sklearn在手写数字数据集上训练MLP模型。 其优势是: 它提…

Springboot 多数据源 dynamic-datasource动态添加移除数据源

0.前言 上一篇文章我们讲了如何通过多数据源组件,在Spring boot Druid 连接池项目中配置多数据源,并且通过DS注解的方式切换数据源,《Spring Boot 配置多数据源【最简单的方式】》。但是在多租户的业务场景中,我们通常需要手动的…

【方法】Excel表格如何拆分数据?

当需要把多个数据逐个填到Excel单元格的时候,我们可以利用Excel的数据拆分功能,可以节省不少时间。 小编以下面的数据为例,看看如何进行数据拆分。 首先,要选择数字所在的单元格,然后依次点击菜单栏中的“数据”>…

FFmpeg解码详细流程

介绍 FFmpeg的 libavcodec 模块完成音视频多媒体的编解码模块。老版本的 FFmpeg 将avcodec_decode_video2()作为视频的解码函数 API,将avcodec_decode_audio4()作为音频的解码函数 API;从 3.4版本开始已经将二者标记为废弃过时 API(attribut…

Dockerfile构建lamp镜像

1、构建目录 [rootdocker ~]# mkdir compose_lamp [rootdocker ~]# cd compose_lamp/ 2、编写Docekerfile [rootdocker compose_lamp]# vim Dockerfile #基础镜像 FROM centos:7#维护该镜像的用户信息 MAINTAINER Crushlinux <crushlinux163.com>#安装httpd RUN yum -…

反转链表的两种方法

反转链表的两种方法 题目介绍 题目链接 206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 效果图如下所示 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]解法一&#xff1a;迭代方法 解题思路&#xf…

Chapter 10: Dictionaries | Python for Everybody 讲义笔记_En

文章目录 Python for Everybody课程简介DictionariesDictionariesDictionary as a set of countersDictionaries and filesLooping and dictionariesAdvanced text parsingDebuggingGlossary Python for Everybody Exploring Data Using Python 3 Dr. Charles R. Severance 课程…

Spring:JDBCTemplate

JDBCTemplate 概述 概述 JDBC&#xff08;Java DataBase Connectivity&#xff0c;Java 数据库连接&#xff09;&#xff0c; 一 种用于执行 SQL 语句的 Java API&#xff08;Application Programming Interface &#xff0c; 应用程序设计接口 &#xff09;&#xff0c;可以为…

计算机网络(2) --- 网络套接字UDP

计算机网络&#xff08;1&#xff09; --- 网络介绍_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131967378?spm1001.2014.3001.5501 目录 1.端口号 2.TCP与UDP协议 1.TCP协议介绍 1.TCP协议 2.UDP协议 3.理解 2.网络字节序 发送逻辑…

小白到运维工程师自学之路 第六十六集 (docker 网络模型)

一、概述 Docker网络模型是指Docker容器在网络中的通信方式和组织结构。Docker容器通过网络连接&#xff0c;使得容器之间可以相互通信&#xff0c;并与主机和外部网络进行交互。 在Docker中&#xff0c;有几种不同的网络模型可供选择&#xff1a; 1、主机模式&#xff08;H…

网络安全(黑客)自学建议一一附学习路线

温馨提示&#xff1a;为了避免误入歧途&#xff0c;自学请优先看《网络安全法》。 下面是一些学习建议&#xff1a; 1、多请教有经验的人 切忌钻牛角尖&#xff0c;特别是刚入门的什么都不了解的情况下&#xff0c;可能你花好几天研究的一个东西&#xff0c;人10分钟就能搞定…