Java单例模式演示与理解

news2024/11/20 0:32:08

目录

    • 单例模式
    • 1、饿汉式
    • 2、懒汉式
    • 3、DSL懒汉式(双重锁懒汉模式)
    • 静态内部类懒汉式
    • 单例模式的如何破坏
    • 4、使用枚举类

单例模式

为什么使用单例模式? 单例模式确保一个类在内存中只会有同一个实例对象存在。不管声明获取实例对象多少次都是内存中同一块地址。
不管哪一种单例模式都是要将自己的构造器进行私有化,防止外面通过构造器创建出多个不同的实例化对象。对象是在对应类的内部通过调用自身的构造方法创建的,然后以static方法的方式向外提供。

1、饿汉式

在类启动时就会将创建实例对象放在内存中。

饿汉式的思路就是1.将构造器私有化 2.声明一个私有的静态的类型的变量并在类的内部通过构造函数为其赋值 3. 通过静态方法将其暴露出去
在这里插入图片描述
在使用时,只需要的调用该类向外提供的静态方法即可
在这里插入图片描述

但这种饿汉式的单例模式有一个非常大的缺点就是,如果一个类中方法变量十分的多,且在不能确定是否需要该实例的情况下,直接将对象加载到内存会十分耗费内存。
在这里插入图片描述

2、懒汉式

只有用到时此对象时,才会创建类实例对象将其放入内存中,这样就可以防止将不必要的对象加载到内存,节省资源。相较于饿汉式是一种以时间换空间的方式。

懒汉式思路:1. 将构造器私有化 2. 声明一个私有的静态的类型的变量,注意没有赋值 3. 在向外提供实例对象时,进行判断是否已经创建对象,如果没有再为其创建对象。统一向外暴露

懒汉式相较于饿汉式是在需要此对象时(调用此类向外提供暴露对象的方法)才会创建对象,再创建对象时还会判断是否已经存在此类对象,如果存在则继续使用,不存在作为new一个新的对象,确保内存中只有一个此类的对象。

在这里插入图片描述
但这种懒汉式也存在一个问题,就是线程不安全。当在多线程环境下使用获取此对象时,由于判断与操作不是一体可能导致线程安全问题

在这里插入图片描述

线程安全问题产生原因:只有在第一次获取且多线程时才有可能出现线程安全问题
在这里插入图片描述
这样就不会产生线程安全性问题:
在这里插入图片描述

具体的解决方式,要使用DSL懒汉式或静态内部类懒汉式解决

3、DSL懒汉式(双重锁懒汉模式)

DSL:Double Check Lock
思考:如果直接在获取对象的静态方法上加上synchronized会有什么影响?使得获取对象的操作变为了同步操作。无论是第一次创建对象还是已有对象的返回都是同步操作。在上面已知只有在第一次获取对象时在多线程的环境下才有可能发生线程安全问题。全部锁上,一律变成单线程操作会影响性能。

在这里插入图片描述
思考:为什么在变量User前使用volatile?
volatitle的三大特点:

  • 可见性
  • 不保证原子性
  • 禁止指令重排

在User变量前加上了Volatile关键字,这里就用到了它的禁止指令重排的特性。那么什么是指令重排呢?要知道,虽然在我们自己的代码中只有一行代码user =new User(),但在JVM内部他可能分为三步执行:1. 在堆内存开辟空间 2. 在开辟的空间保存对象信息 3. 由栈内对象变量指向堆内存的空间 。但是,JVM为了效率存在乱序执行的可能,原本是1->2->3,乱序后就可能发生 1->3->2,虽然最后的结果是一样,但放在代码中,当有多个线程去执行它的时候,就会有小概率发生异常
在这里插入图片描述
而在User前使用Volatile关键字修饰,就可以防止user变量赋值指令发生重排,能够按照1、2、3的顺序执行不乱序。确保返回的对象变量在堆空间已经完成赋值

思考:为什么在类中锁住的是user.class? synchronized 锁类与锁对象的区别?

  • synchronized加在非静态方法锁住的是对象,加载静态方法锁住的是整个class类。例如:我在一个F非静态方法前上synchronized关键字,那么它的作用范围只是对于同一个对象来说的。同一个对象,即使在多个线程中调用此方法,在同一时刻内只能有一个线程进入了此方法。但如果我创建了两个对象A,B,分别在三个线程中(线程T1,T2使用对象A,线程T3使用对象B)调用F这一相同方法,那么会发生什么呢?
    同一时刻,在F方法中,线程(T1或T2)可能与T3同时调用这一个方法,而同一个时刻,线程T1,T2在只能有一个线程在执行F方法。
    这就是对象锁,同一个时刻,在多线程中同一个对象只能有一个线程执行此方法

  • synchronized 加在静态方法锁住的类,这个类创建的所有对象在多线程中只能有一个对象在执行此方法
    还是上面的例子,如果方法使用的是synchronized锁住类,那么同一个时刻,T1,T2,T3三个线程中只能有一个线程的一个对象在执行此方法

以下是针对synchronized的六种情况测试: (这里使用sleep是为了增大出现线程安全问题概率,并无实际意义。否则业务简单执行过快,根本没有给其他插入机会 )

  • 测试同一个对象 不加任何锁,在高并发环境下测试线程安全问题:
public class Application {
    public static void main(String[] args) throws InterruptedException {
        TestSync testSync = new TestSync();
        new Thread(testSync::start,"A").start();
        new Thread(testSync::start,"B").start();
        new Thread(testSync::start,"C").start();
    }
}
class TestSync{
    void start(){
        System.out.println(Thread.currentThread().getName() + "begin");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName() + "end");
    }
}

结果:
在这里插入图片描述
统一时刻,三个线程操作同一个对象进入方法


  • 测试不同个对象 不加任何锁,在高并发环境下测试线程安全问题:
    在这里插入图片描述
public class Application {
    public static void main(String[] args) throws InterruptedException {
        TestSync testSync1 = new TestSync();
        TestSync testSync2 = new TestSync();
        TestSync testSync3 = new TestSync();
        new Thread(testSync1::start,"A").start();
        new Thread(testSync2::start,"B").start();
        new Thread(testSync3::start,"C").start();

    }
}
class TestSync{
    void start(){
        System.out.println(Thread.currentThread().getName() + "begin");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName() + "end");

    }
}
  • 测试同个对象 加对象锁,在高并发环境下测试线程安全问题:
public class Application {
    public static void main(String[] args) throws InterruptedException {
        TestSync testSync = new TestSync();
        new Thread(testSync::start,"A").start();
        new Thread(testSync::start,"B").start();
        new Thread(testSync::start,"C").start();
    }
}
class TestSync{
   synchronized void start(){
        System.out.println(Thread.currentThread().getName() + "begin");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName() + "end");
    }
}

思考:结果是什么?
因为锁住的是对象,而多个线程操作的是同一个对象,也就是说,只能同时有一个线程执行此方法
在这里插入图片描述

  • 测试不同个对象 加对象锁,在高并发环境下测试线程安全问题:
public class Application {
    public static void main(String[] args) throws InterruptedException {
        TestSync testSync1 = new TestSync();
        TestSync testSync2 = new TestSync();
        TestSync testSync3 = new TestSync();
        new Thread(testSync1::start,"A").start();
        new Thread(testSync2::start,"B").start();
        new Thread(testSync3::start,"C").start();
    }
}
class TestSync{
  synchronized void start(){
        System.out.println(Thread.currentThread().getName() + "begin");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName() + "end");
    }
}

思考:结果是什么?
因为加的是对象锁,但每个线程对象都不一样,所以多个线程都可以进入方法,对象锁无效
在这里插入图片描述

  • 测试同个对象 加类锁,在高并发环境下测试线程安全问题:
public class Application {
    public static void main(String[] args) throws InterruptedException {
        TestSync testSync = new TestSync();
        new Thread(testSync::start,"A").start();
        new Thread(testSync::start,"B").start();
        new Thread(testSync::start,"C").start();

    }
}
class TestSync{
   void start(){
 	 synchronized (TestSync.class){ // 在一个普通方法使用代码块的方式锁住一个类
        System.out.println(Thread.currentThread().getName() + "begin");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName() + "end");
		}
    }
}

思考:结果是什么?
锁住的是各个对象,同一个时刻只能有一个对象的线程去访问此方法,所以结果和对象锁相同
在这里插入图片描述

  • 测试不同个对象 加类锁,在高并发环境下测试线程安全问题:
public class Application {
    public static void main(String[] args) throws InterruptedException {
        TestSync testSync1 = new TestSync();
        TestSync testSync2 = new TestSync();
        TestSync testSync3 = new TestSync();
        new Thread(testSync1::start,"A").start();
        new Thread(testSync2::start,"B").start();
        new Thread(testSync3::start,"C").start();

    }
}
class TestSync{
void start(){
 	 synchronized (TestSync.class){ // 在一个普通方法使用代码块的方式锁住一个类
        System.out.println(Thread.currentThread().getName() + "begin");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {

        }
        System.out.println(Thread.currentThread().getName() + "end");
		}
    }
}

思考:结果是什么?
即使是多个对象,但同一个时刻只能有一个对象的线程去访问此方法
在这里插入图片描述

静态内部类懒汉式

思路:1. 将构造私有化 2. 创建一个静态内部类 3. 在此内部类的内声明外部类对象类型的变量作为内部类属性,并对外部类对象变量使用外部类的私有构造为其赋值 4. 在外部类创建公有且静态方法,在方法中向外提供静态内部类声明的外部类对象
在这里插入图片描述
注意:静态内部类懒汉式也是线程安全的

思考:为什么静态内部类的方式单例模式是懒汉式?
因为程序启动时,JVM会将外部类加载到内存中进行初始化,而内部类不会加载,也就不会初始化。只有在使用时,才会将类进行初始化,为其静态变量进行赋值。也就是说,只有调用getInstance()这个外部类方法时才会加载到内存, 这就是为什么是懒汉式。

思考:为什么静态内部类的方式是线程安全的呢?
静态内部类的静态属性是JVM在加载此类时进行赋值,而JVM在初始化此类时会为其上锁,使得在多线程环境下,内部类静态变量赋值时依然是线程安全的。

思考:静态内部类的方式与DSL的方式有什么区别?或者说静态内部类构造单例模式有什么缺点?
静态内部类最大的缺点就是:由于采用静态变量赋值,无法从外部传递参数
例:

public class Application {
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println(User.getInstance(1).getAge());
        },"A").start();
        new Thread(()->{
            System.out.println(User.getInstance(2).getAge());
        },"B").start();
        new Thread(()->{
            System.out.println(User.getInstance(3).getAge());
        },"C").start();
    }
}
class User{
    private static User user;
    private int age;
    private User(){

    }
    public int getAge(){
        return this.age;
    }
    private User(int age){
        this.age=age;
    }
    public static User getInstance(int age){
        if(user==null){
            synchronized (User.class){
                if(user==null){
                    System.out.println(Thread.currentThread().getName()+"先进来");
                    user=new User(age);
                }
            }
        }

        return user;
    }
}

单例模式的如何破坏

因为单例模式的核心是构造器的私有化,如果通过反射的方式将类的构造器设置为public,这样不就可以通过构造器实例化多个不同的实例对象了吗?


阶段1:
思路:通过类反射获取构造参数,并将构造参数设置为可见(因为是私有),然后通过反射获取的构造再去创建创建新对象。这样每次获取对象都是通过构造函数获取的,对象实例化有多个。

    System.out.println(User.getInstance());// 使用单例模式的构建对象
        Constructor<User> constructor = User.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        System.out.println(constructor.newInstance()); // 使用反射获取的对象构建对象
        System.out.println(constructor.newInstance()); // 使用反射获取的对象构建对象

结果
在这里插入图片描述

防止这样破解怎么办?采用阶段2的方式


阶段2:
思路:在构造时,判断此类的对象是否存在。
在这里插入图片描述
但这种只适用于在使用反射获取对象前已经使用单例获取了对象,如果全部使用反射构造获取,类中的构造方法将无法获取不使用单例模式创建的对象,使得这种方式无效。
阶段3:
思路:无论是单列模式还是通过反射创建对象都离不开构造方法,要实现只有一个实例,只需要确保构造方法只能被调用一次即可。可以设置一个标志确保构造只会调用一次。
但这种还存在一个问题,就是通过反射去修改这个标志。如果flag不是为boolean类型,那通过返射知道要修改为什么吗?如果设置为一个密码,只有符合这个密码,才可以调用构造方式实例化一个对象,调用一次立即修改为其他值,这样就只有通过反射将这个flag修改为和密码一样才可以再实例化一个对象。
如何无法通过反射破坏单实例模式呢?可以通过枚举来构造单例模式。

4、使用枚举类

public class Application {

    public static void main(String[] args) throws Exception {
        System.out.println(Singleton.INSTANCE.getInstance());
        System.out.println(Singleton.INSTANCE.getInstance());
    }
}

enum Singleton {

    INSTANCE;
    private User instance;

    Singleton() {
        instance = new User();
    }

    private class User{

    }

    public User getInstance() {
        return instance;
    }
}

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

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

相关文章

2023年网络安全八大预测!

随着创新技术的不断兴起&#xff0c;以及网络犯罪的日益专业化&#xff0c;网络安全攻击风险仍在持续增长。可以预见&#xff0c;2023年的网络安全形势依然严峻&#xff0c;需要国家不断完善网络安全政策和法规&#xff0c;网络安全企业积极创新网络安全防护技术。瑞数信息作为…

Allegro如何导出和导入器件模型Signal_Model操作指导

Allegro如何导出和导入器件模型Signal_Model操作指导 在用Allegro做PCB设计的时候,通常需要给器件加上Signal_Model,在做等长的时候用到的非常频繁。 Allegro除了可以给器件添加模型,还支持从一块加好模型的BRD导入到另外一块BRD中, 如下图,需要把R7002的Signal_Model导入…

剪报浏览器:可以自己设计网页的浏览器

总的功能就是一句话“不同网站的精华内容裁剪下来&#xff0c;合并到一处浏览”把自己关注的网页版块从不同网站上裁剪下来放在一个页面里&#xff0c;一次刷新就可以看到不同网站的最新内容&#xff0c;而不用逐个打开网站去看&#xff0c;提高了上网的效率。关键特征汇聚浏览…

排序算法(带动图)

0、算法概述0.1 算法分类十种常见排序算法可以分为两大类&#xff1a;比较类排序&#xff1a;通过比较来决定元素间的相对次序&#xff0c;由于其时间复杂度不能突破O(nlogn)&#xff0c;因此也称为非线性时间比较类排序。非比较类排序&#xff1a;不通过比较来决定元素间的相对…

【数据结构初阶】第七篇——二叉树的顺序结构及实现(堆的向下,向上调整算法)

二叉树的顺序结构 堆的概念及结构 堆的向下调整算法 堆的向上调整算法 堆的实现 初始化堆 销毁堆 打印堆 堆的插入 堆的删除 获取堆顶的数据 获取堆的数据个数 堆的判空 建堆的时间复杂度 二叉树的顺序结构 普通二叉树是不适合用数组来存储的,因为可能会导致大量…

为nginx配置好看的错误提示页面

前言 nginx默认错误页面确实有些丑哈&#xff0c;leeader让我换一个样式 &#xff0c;我就来喽&#xff01; 为nginx配置好看的错误提示页面前言1 找异常页原始页2 win上替换3 再linux服务器上替换4 不生效解决办法样式显示不正确6 错误页源码1 找异常页 原始页 nginx默认错误…

2个月快速通过PMP证书的经验

01 PMP证书是什么&#xff1f; 指的是项目管理专业人士资格认证。它是由美国项目管理协会&#xff08;Project Management Institute(简称PMI)&#xff09;发起的&#xff0c;严格评估项目管理人员知识技能是否具有高品质的资格认证考试。其目的是为了给项目管理人员提供统一的…

初学者的Metasploit教程 - 从基础到高级

Metasploit是使用最广泛的渗透测试工具之一&#xff0c;是一个非常强大的多合一工具&#xff0c;用于执行渗透测试的不同步骤。 文章目录前言安装Metasploit在 Linux 上安装 Metasploit了解 Metasploit 的版本并更新渗透测试的基础知识1. 信息收集/侦察2. 漏洞分析3.渗透4. 渗透…

OSCP_VULHUB_Hack the Kioptrix Level-1.2

文章目录前言渗透方法论&#xff08;方法一&#xff09;渗透方法论&#xff08;方法二&#xff09;第一种sqlmap扫描&提取数据库和用户凭证ssh登录使用 SUID 位和 SUDO 二进制文件利用目标第二种方法searchsploit LotusCMS前言 Kioptrix 的 CTF 挑战&#xff1a;Level1.2 …

Linux搭建Hyperledger Fabric区块链框架 - Hyperledger Fabric 概念

企业选型的区块链底层技术 Hyperledger Fabric 概念 2015年&#xff0c;Linux基金会启动了Hyperledger项目&#xff0c;目标是发展跨行业的区块链技术。 Hyperledger Fabric是Hyperledger中的一个区块链项目&#xff0c;包含一个账本&#xff0c;使用智能合约并且是一个通过所…

上海亚商投顾:三大指数均涨约1% 两市近4300股飘红

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。市场情绪三大指数早盘冲高回落&#xff0c;午后又震荡走强&#xff0c;深成指、创业板指均涨超1.2%。人工智能概念掀涨停潮&a…

Pytorch实战笔记(3)——BERT实现情感分析

本文展示的是使用 Pytorch 构建一个 BERT 来实现情感分析。本文的架构是第一章详细介绍 BERT&#xff0c;其中包括 Self-attention&#xff0c;Transformer 的 Encoder&#xff0c;BERT 的输入与输出&#xff0c;以及 BERT 的预训练和微调方式&#xff1b;第二章是核心代码部分…

机器视觉_HALCON_HDevelop用户指南_4.HDevelop开发程序

文章目录四、HDevelop编程4.1. 新建一个新程序4.2. 输入一个算子4.3. 指定参数4.4. 获取帮助4.5. 添加其他程序4.6. 理解图像显示4.7. 检查变量4.8. 利用灰度直方图改进阈值4.9. 编辑代码行4.10. 重新执行程序4.11. 保存程序4.12. 选择特征区域4.13. 打开图形窗口4.14. 循环遍历…

Swig工具在win10上使用

SWIG 是一种软件开发工具&#xff0c;它将 C 和 C 编写的程序与各种高级编程语言连接起来。这里我们用它来将 C/C 转换成 Java。 一、Swig安装 1、下载 官网&#xff1a;SWIG官网下载 源码链接 GitHub&#xff1a;https://github.com/swig/swig.git 这两个地址可能会出现无…

STM32单片机智能蓝牙APP加油站火灾预警安防防控报警监控系统MQ2DHT11

实践制作DIY- GC0122-智能蓝牙APP加油站火灾预警 一、功能说明&#xff1a; 基于STM32单片机设计-智能蓝牙APP加油站火灾预警 功能介绍&#xff1a; 基于STM32F103C系列最小系统&#xff0c;MQ-2烟雾传感器&#xff0c;火焰传感器&#xff08;不能直视阳光会受到阳光干扰&…

Cesium 渐变长方体实现-Shader

position获取: 1.1 在cesium中,可通过vec4 p = czm_computePosition();获取 模型坐标中相对于眼睛的位置矩阵 1.2 vec4 eyePosition = czm_modelViewRelativeToEye * p; // position in eye coordinates 获取eyePosition 1.3 v_positionEC = czm_inverseModelView * eyePo…

Python流程控制详解

和其它编程语言一样&#xff0c;Python流程控制可分为 3 大结构&#xff1a;顺序结构、选择&#xff08;分支&#xff09;结构和循环结构。 Python对缩进的要求&#xff08;重点&#xff09; Python 是一门非常独特的编程语言&#xff0c;它通过缩进来识别代码块&#xff0c;…

ConditionalOnBean详解及ConditionalOn××总结

ConditionalOnBean详解 为什么学习ConditionalOnBean 在学习 Springboot 自动装配的时候遇到 Bean 装配和 Bean 配置需要条件判断的场景时&#xff0c;查阅了相关内容了解到 Conditional 和 ConditionalOnBean 注解&#xff0c;深入学习之后受益匪浅。 ConditionalOnBean测试…

后量子 KEM 方案:Newhope

参考文献&#xff1a; Lyubashevsky V, Peikert C, Regev O. On ideal lattices and learning with errors over rings[J]. Journal of the ACM (JACM), 2013, 60(6): 1-35.Lyubashevsky V, Peikert C, Regev O. A toolkit for ring-LWE cryptography[C]//Advances in Cryptol…

Linux常见指令大全(一)

&#x1f339;作者:云小逸 &#x1f4dd;个人主页:云小逸的主页 &#x1f4dd;Github:云小逸的Github &#x1f91f;motto:要敢于一个人默默的面对自己&#xff0c;强大自己才是核心。不要等到什么都没有了&#xff0c;才下定决心去做。种一颗树&#xff0c;最好的时间是十年前…