【Java 并发编程】单例模式

前言


        单例模式是一种十分常用但却相对而言比较简单的单例模式。虽然它简单但是包含了关于线程安全、内存模型、类加载机制等一些比较核心的知识点。本章会介绍单例模式的设计思想,会去讲解了几种常见的单例实现方式,如饿汉式、懒汉式、双重检锁、静态内部类、枚举等。


前期回顾:【Java 线程通信】模拟ATM取钱(wait 和 notify机制)


目录

前言

单例模式简介

 单例模式设计

饿汉式实现方式

代码分析

代码优劣

代码测试

懒汉式实现方式

代码优化

线程安全

效率低下 

双重检锁

内存可见性

完美代码

代码优劣

静态内部类的实现方式

枚举的实现方式

关于反射破坏

 

单例模式简介


        单例模式,顾名思义就是一个运行时域,一个类只有一个实例对象

        那么为什么需要单例模式呢?单例模式的使用场景是什么?

        像我们之前写的类的实例对象的创建与销毁对资源来说消耗不大,用不用单例模型其实无所谓。但是有些类的消耗比较大,如果频繁的创建与销毁而且这些类的对象完全可以复用的话,这势必会造成不必要的性能浪费

举个栗子~

        我们要写一个访问数据库类,但是创建数据库链接对象是一个十分耗资源的操作,并且数据库链接是完全可以复用的。那么可以把这个类设置为单例的,这样只需要创建一次对象并且重复使用这个对象就好了,而不用每次去访问数据库都要创建链接对象。

 

 单例模式设计


        单例模式有多种写法,比如饿汉式、懒汉式等等。但是不管是哪一种写法其实都要考虑一下三点:

是否线程安全
是否懒加载(也叫延迟加载)
能否反射破坏

 

饿汉式实现方式

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

        因为这种方法在 类加载时 就立即创建了实例,就如饿汉看见吃的就 “迫不及待”的去吃的感觉。这里也是如此,类一加载就迫不及待的创建了对象,所以称之为饿汉。

 

代码分析

(1) 由于单例就是一个类只有一个实例对象的,所以我们并不希望别人能通过 new 直接创建对象,所以我们使用 private 来修饰构造方法

(2) 这个对象由于 static 静态修饰的,所以在类加载的时候就已经创建好了,通过 getInstance 调用只是获取这个对象实例而已,并且这种创建方式是天生线程安全的。

 

代码优劣

优点:

JVM 在加载这个类的时候就会对它进行初始化, 这里包含对静态变量的初始化,天生线程安全
没有加锁,运行效率更高


缺点:

类加载时就初始化,若是重启服务的话,会拖慢运行速度
类加载时就初始化,如果创建了不使用,会导致内存浪费

 

代码测试

class Test{
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

运行结果

true

        所以饿汉的方式创建对象只会创建一个单例,考虑到空间浪费,使用的时候还需权衡优劣。

懒汉式实现方式

以下是只是标准模板(单线程版本),还有很多因素没有考虑 ~

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

        以上代码我们可以发现这个对象并不会随着类加载而创建,而是在第一次访问单例类的实例时才去创建(第一次调用 getInstance 方法),我们将这种延迟创建的行为称之为 “懒汉”。

 

代码优化

线程安全

(1) 首先我们发现以上代码是线程不安全的,在执行以下这条语句时

if (instance == null)

可能会有多个线程已经越过这个语句去创建对象了,所以它是不安全的。

我们需要改进一下:

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

效率低下 

(2) 先给这个方法加上 synchronized ,这样就能保证同一时刻只有一个线程访问这个方法了,但是这样又会引入新的问题:其实我们只想要对像构建的时候同步线程,像以上这种代码是每次在获取对象的时候都要进行同步操作,这样对性能影响是是十分大的。所以这种方法并不推荐。

        通过以上可以知道要想提升效率,直接在对象构建的时候加同步锁就可以了,而使用对象是不需要同步的,那么我们就可以改成这样。如下图:

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
            
        }
        return instance;
    }
}

 

双重检锁

(3) 关于上述代码,getInstance 是不需要参与锁竞争的所有线程都可直接进入,那么现在就开始第二步判断,如果实例对象没有创建,那么所有线程都会去争抢锁,抢到锁的那个线程会开始创建实例对象。实例对象创建了之后,以后所有的 getInstance 操作都是进行到第二步直接跳过,然后返回当前实例对象。这就解决了上述代码的低效问题。

        但是以上代码仍然是有问题的:

 if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
            
        }

        假设 线程A、线程B 同时进入 if 语句,那么线程A拿到锁后创建了一个实例对象后将锁释放了;此时 线程B 拿到锁也可以创建实例对象。此时就可以创建多个实例对象了,所以这也是线程不安全的。有没有一种办法保证线程安全呢?其实我们只要在内部在加上一条 if 判断检查当前对象是否被创建即可。这种方法也被叫做双重检锁

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }

        }
        return instance;
    }
}

        此时 线程B 获得锁以后会进行一个判空,此时 线程A 已经实例过一次了,线程B 自然就不能创建对象了。

 

内存可见性

(4) 关于上述代码虽然看上去已经很完美了,但是还是有一点瑕疵。这里就要谈到 happens-before 内存可见性原则。简单来讲就是我们简单的一条 Java 语句,内部其实是有多种指令运行完成的。

        比如像以下这行代码,由于不是原子操作,虽然只是一条语句,但实际有三个指令在完成操作。

 instance = new Singleton();
(1)为对象分配内存
(2)初始化对象
(3)返回对象指向的内存地址

        以上的一条语句在非并发也就是单线程中是没有问题的,但是在并发执行时,虚拟机为了效率可能会对指令进行重排比如说 线程A 的执行顺序是:1->3->2。那么这个线程是先为对象分配好内存,再返回这个对象指向的内存地址,但是由于这个对象还没来得及初始化。此时如果有一个线程 B 进行 if (instance == null) 判空操作就会返回 false 跳过创建对象这个步骤,直接返回这个未初始化的对象。这也是造就了线程安全问题。那么怎么解决呢?我们只需加上 volatile 修饰即可。

 

完美代码
class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }

        }
        return instance;
    }
}

 

代码优劣

优点

需要这个实例的时候,先判断它是否为空,如果为空,再创建单例对象
用到的时候再去创建,避免了创建了对象不去的用而造成浪费

缺点

由于懒汉模式经过优化过后已经没有什么缺点了,唯一的缺点就是编写略显复杂。

关于其他的创建方式这里简述一下: 

静态内部类的实现方式

        JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性、方法被调用时才会被加载,并初始化其静态属性

class StaticInnerSingleton {

    private StaticInnerSingleton() {}

    public static StaticInnerSingleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private static StaticInnerSingleton instance = new StaticInnerSingleton();
    }
}

        比较推荐这种方式,没有加锁,线程安全。用到时再加载,并发行能高。

枚举的实现方式

        枚举单例是最好的单例,有效防止反射

enum EnumSingleton {
    // 此枚举类的一个实例, 可以直接通过EnumSingleton.INSTANCE来使用
    INSTANCE
}

关于反射破坏

        以上的方式除了枚举,其他都能被放射破坏。但是反射是一种人为操作,只有故意去这样操作才会造成反射破坏。

class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }

        }
        return instance;
    }
}

class Test1{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Singleton s1  = Singleton.getInstance();

        // 使用反射创建Singleton实例
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        // 通过反射获取的实例
        Singleton s2 = declaredConstructor.newInstance();
        System.out.println(s1 == s2);
    }
}

运行结果:

false

 关于如何利用反射破坏单例,请参考以上代码 ~

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

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

相关文章

C++和OpenGL实现3D游戏编程【连载16】——详解三维坐标转二维屏幕坐标(向量和矩阵操作实战)

&#x1f525;C和OpenGL实现3D游戏编程【目录】 1、本节课要实现的内容 在上一课我们了解了着色器&#xff0c;了解了部分核心模式编程内容&#xff0c;从中接触到了线性代数中向量和矩阵相关知识&#xff0c;我们已经能够感受到向量和矩阵在OpenGL编程中的重要性。特别是后期…

Linux——传输层协议

目录 一再谈端口号 1端口号范围划分 2两个问题 3理解进程与端口号的关系 二UDP协议 1格式 2特点 3进一步理解 3.1关于UDP报头 3.2关于报文 4基于UDP的应用层协议 三TCP协议 1格式 2TCP基本通信 2.1关于可靠性 2.2TCP通信模式 3超时重传 4连接管理 4.1建立…

MySQL数据库的高可用

一、MHA工作原理 1、MHA的工作原理 1、MHA利用 select 1 as value 指令判断master服务器的健康性&#xff0c;一旦master宕机&#xff0c;MHA从宕机崩溃idmaster保存二进制日志事件&#xff08;binlog events&#xff09; 2、识别含有最新更新的slave 3、应用差异的中继日志&a…

bcprov-jdk15on-1.52.0.jar has unsigned entries - org/bouncycastle/LICENSE

报错界面如上图 解决办法&#xff1a; 1.修改引用jar包&#xff0c;将build.gradle里面的依赖为 implementation org.bouncycastle:bcprov-jdk15on:1.52 2.到maven上下载最新的bcprov-jdk15on-1.52.0.jar,替换文件夹中原有的jar包

C/C++每日一练:实现一个环形队列

队列&#xff08;queue&#xff09; 队列是一种先进先出&#xff08;FIFO&#xff0c;First In First Out&#xff09; 的数据结构&#xff0c;类似于排队的场景。最先进入队列的元素最先被处理&#xff0c;而后加入的元素则排在队列的末尾。 常见的队列操作&#xff1a; 入队…

第二届中国楚域品牌文化创新发展大会暨楚域尚品发布会在汉圆满落幕

10 月 19 日&#xff0c;“第二届中国楚域品牌文化创新发展大会暨楚域尚品发布会”在武汉市光谷九通海源大酒店隆重举行。本次大会由中国商业文化研究会传承创新工作委员会、楚域品牌文化传承创新工作委员会、华夏品牌文化创新发展大会组委会主办&#xff0c;湖北省企业文化促进…

python爬虫简易入门示例

版本环境 win11python 3.12.4 目标&#xff1a;爬取https://gitee.com/explore的列表内容&#xff0c;并写入txt文本 效果 开始 1.安装依赖 pip install requests beautifulsoup42.编写代码&#xff0c;如下&#xff0c;详见注释 import requests from bs4 import Beauti…

【PFGA】二选一数选器

文章目录 前言一、实验原理二、实验过程三、实验结果参考文献 前言 进行 verilog FPGA 实验 一、实验原理 二、实验过程 三、实验结果 代码 module mux21(input s,input a,input b,output reg y); always(s or a or b) beginif (~s) beginy<a;end else beginy<…

ollama+ollama-webu在windos上部署的教程

ollamaollama-webu在windos上部署的教程 一、需要准备的环境和代码二、开始部署1. 修改系统变量&#xff1a; 常见问题 首先介绍一下ollama&#xff1a; Ollama 是一种为快速大规模语言模型推理所设计的框架和平台。它旨在帮助用户通过高效的方式运行和管理大型语言模型&#x…

使用AITemplate和AMD GPU的高效图像生成:结合Stable Diffusion模型

Efficient image generation with Stable Diffusion models and AITemplate using AMD GPUs 2024年1月24日&#xff0c;作者是[Douglas Jia] Stable Diffusion 已成为图像生成领域的突破性进展&#xff0c;帮助用户将文本描述转化为引人入胜的视觉输出。 Stable Diffusion 的…

SAP_通用模块-MASS批量操作技巧(二)

业务背景&#xff1a; 前两天写了一篇关于MASS批量操作的文档&#xff0c;当时测试批量扩充物料视图的时候失败了&#xff0c;就没记录进去&#xff0c;然后手头上刚好有一个需求&#xff0c;就是物料已经有基本视图等相关信息的情况下&#xff0c;需要扩充相关的物料视图。方法…

光纤光学——弱导光纤与线偏振模

一、基本思想 弱导光纤&#xff1a;n1≈ n2 , k0n1 ≈ k0n2&#xff0c;亦即&#xff1a; k0n1 ≈ k0 n2 ≈ 光线与纤轴的夹角小&#xff1b;芯区对光场的限制较弱&#xff1b; 消逝场在包层中延伸较远。 弱导光纤场的特点&#xff1a; HEι1,m模式与EHι-1,m色散曲线相近…

1024程序员节·城市聚会·西安,它来了

活动名称 CSDN 1024程序员节城市聚会西安 活动主题 智能进化&#xff1a; 开发者在AI时代的工作与生活变革 活动背景 CSDN一年一度的1024程序员节城市聚会&#xff08;西安站&#xff09;是一场专为程序员打造的盛会。这个活动旨在为西安的开发者们提供一个交流技术、分享…

每日OJ题_牛客_数组变换_贪心+位运算_C++_Java

目录 牛客_数组变换_贪心位运算 题目解析 C代码1暴力 C代码2位运算 Java代码位运算 牛客_数组变换_贪心位运算 数组变换__牛客网 (nowcoder.com) 描述&#xff1a; 牛牛有一个数组&#xff0c;里面的数可能不相等&#xff0c;现在他想把数组变为&#xff1a;所有…

MySQL数据库和表的基本操作

目录 一、数据库的基础知识 背景知识 数据库的基本操作 二、数据类型 字符串类型 数值类型 日期类型 三、表的基本操作 创建表 查看表结构 查看所有表 删除表 一、数据库的基础知识 背景知识 MySQL是一个客户端服务器结构的程序 主动发送数据的这一方&#xff0c;…

【Java】集合补充

常见基础集合汇总 数据结构&#xff1a;栈 数据结构分为&#xff1a; &#xff08;1&#xff09;逻辑结构 &#xff1a;--》思想上的结构--》卧室&#xff0c;厨房&#xff0c;卫生间 ---》线性表&#xff08;数组&#xff0c;链表&#xff09;&#xff0c;图&#xff0c;树&…

近期股市热潮,现有架构模块下金融交易系统如何应对“冲击”?优化思路如下

近期股市热情高涨&#xff0c;激增的交易量挑战的不止是券商&#xff0c;还有交易系统的基础架构是否稳固。9月底&#xff0c;股市牛抬头&#xff0c;瞬时的高并发量一把“撞”崩多家券商的交易应用系统&#xff0c;导致交易停滞。 在这场资本盛宴背后&#xff0c;稳定、高效、…

一家异业联盟平台 两年百亿销售额怎么做到的?

近年来&#xff0c;互联网领域涌现了一颗耀眼的新星——“上海我店”&#xff0c;该平台短时间内交易额突破百亿大关&#xff0c;且用户数量在上月实现了近百万的增长。这一迅猛的扩张速度&#xff0c;自然吸引了众多商家的目光。不过&#xff0c;随着其影响力的提升&#xff0…

[自动化测试:Selenium]:环境部署和Webdriver的使用

文章目录 修改安装源打开Python Packages。点击梅花按钮。在弹出的对话框中&#xff0c;填入Name&#xff08;随便填&#xff09;&#xff0c;Repository URL&#xff0c;选择下列的源&#xff0c;一般先选择清华源按OK确认。配置完成 安装seleniumFile→Settings→Project&…

为你的网站增加点灵性:随系统变色

&#x1f33b; 前言 网站切换主题色已经是非常常见的功能了&#xff0c;提供浅色和暗色两种色调可以满足用户的使用习惯&#xff0c;帮助这些用户获得更好的访问体验。但是只能用户手动切换主题。 那如果用户已经将系统切换到了深色模式&#xff0c;当他们打开我们网站的时候…