玩转单例模式

news2024/11/16 9:41:46

目录

1. 饿汉式 

2. 懒汉式

3. volatile解决指令重排序

4. 反射破坏单例模式

5. 枚举实现单例模式

6. 枚举实现单例模式的好处

7.尝试反射破坏枚举


所谓单例模式,就是是某个类的实例对象只能被创建一次,单例模式两种实现:饿汉式懒汉式

1. 饿汉式 

所谓饿汉式,顾名思义,很饿,迫不及待,就是在类加载时就已经创建好了对象。优点是没有线程安全问题。缺点是浪费资源空间。不管用不用,对象都会被提前创建出来。

代码演示

// 饿汉模式
class Singleton {
    // 私有构造方法
    private Singleton() {}
    private static final Singleton singleton = new Singleton();
    public static Singleton getInstance() {
        return singleton;
    }

}

2. 懒汉式

所谓懒汉式,就是在当方法调用时才会去创建对象,优点是方法调用时才创建,不浪费空间,缺点是有线程安全问题。

class Singleton {
    private Singleton() {}
    private static Singleton singleton = null;
    public static Singleton getInstance() {
        // 判断对象是否创建
        if(singleton==null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

代码测试(创建10个线程):

public class Demo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> Singleton.getInstance()).start();
        }
    }
}

class Singleton {
    private Singleton() {
        System.out.println(Thread.currentThread().getName()+"创建了对象");
    }
    private static Singleton singleton;
    public static Singleton getInstance() {
        if(singleton==null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

结果:

如上代码,先判断对象是否为空,再创建对象。这个过程在多线程中就会出现多个线程同时判断为空并创建对象的情况。那既然判断是否为空这个过程会有多个线程同时执行,我们可以通过加锁解决。 

class Singleton {
    private Singleton() {}
    private static Singleton singleton;
    public static Singleton getInstance() {
        // 加锁
        synchronized (Singleton.class) {
            if(singleton==null) {
                singleton = new Singleton();
            }    
        }
        return singleton;
    }
}

代码测试(10个线程):

public class Demo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> Singleton.getInstance()).start();
        }
    }
}

class Singleton {
    private Singleton() {
        System.out.println(Thread.currentThread().getName()+"创建了对象");
    }
    private static Singleton singleton;
    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if(singleton==null) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

结果:

通过测试,懒汉模式创建多个对象问题已经解决,但是又引入了一个新问题,那就是性能问题。上面通过加锁确实解决了多线程情况下创建多个实例对象问题。但是这种代码逻辑每个线程都要去获取锁或者没获取到阻塞等待,性能大大降低。不妨在加锁外面再包裹一层判断对象是否为空。这样判断不为空的线程就可以直接返回对象,无需再去获取锁或阻塞等待了。

class Singleton {
    private Singleton() {}
    private static Singleton singleton;
    public static Singleton getInstance() {
        if(singleton==null) {
            synchronized (Singleton.class) {
                if(singleton==null) {
                    singleton = new Singleton(); // 不是一个原子性操作
                }
            }    
        }
        return singleton;
    }
}

除此之外,上面代码还有一个问题,那就是new一个实例对象时并不是一个原子性操作。总共需要三步:

  1. 先分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象指向这个空间

正常创建对象步骤是1->2->3,但有时JVM为了提高性能,会将这三个步骤调整,即指令重排序,这种情况下就会出现1->3->2,如果是是后者,假如有两个线程A,B,若A线程正在创建对象的第二步(按132,把对象指向这个空间,此时对象还没有初始化),此时若线程B正在判断最外层的对象是否为空时就会认为不为空,立即返回该对象。实际上该对象还没有被构造,只是提前分配占用了这块内存空间。

指令重排序是JVM为了提高性能,并且在保证最终结果正确的前提下,对代码执行顺序进行了调整。

3. volatile解决指令重排序

volatile虽然不能解决原子性问题,但是可以禁止指令重排序和内存可见性。

最终的代码如下所示:

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

说好了玩转单例模式,怎么能到这就结束呢。上面的单例模式看似已经优化的很好了,但是由于反射的存在,可以将构造方法从private变成public。这种情况下就直接可以通过构造方法创建,不用去调用静态方法。

4. 反射破坏单例模式

public class Demo {
    public static void main(String[] args) throws Exception {
        // null表示空参构造,获取空参构造器
        Constructor<Singleton> declaredConstructor = 
                Singleton.class.getDeclaredConstructor(null);
        // 设置为true会无视私有构造器
        declaredConstructor.setAccessible(true);
        Singleton singleton = declaredConstructor.newInstance();
        Singleton singleton1 = declaredConstructor.newInstance();
        System.out.println(singleton);
        System.out.println(singleton1);
    }
}

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

可以看出通过反射确实创建出了多个实例对象。 

解决办法也很多,比如在私有构造方法里加锁判断对象是否为空,或者设置一个标志位来判断对象是否为空。但是这两种方法使用反射也还是都可以破坏,比如我可以通过反射每次修改标志位。通过查看反射创建实例对象的源码可以发现,如果使用反射去创建枚举类的实例对象就会报错。这说明是不能通过反射来创建枚举对象的。

5. 枚举实现单例模式

public enum Single {
    INSTANCE;
    public Single getInstance() {
        return INSTANCE;
    }
}

由于使用反射的newInstance()方法创建枚举对象时,这个方法内会判断当前类是否是枚举类,是的话直接就抛异常了。当然可能还有其他原因,正是这样,反射是不能创建枚举对象的。

6. 枚举实现单例模式的好处

1.代码简洁。对比常规的创建单例模式,需要双重校验锁。

2.线程安全。枚举实现的单例天然是线程安全的,因为在底层,其使用static,final修饰,且在初始化过程是线程安全的。

3.反射不能破坏。通常使用反射破坏单例是先通过反射先获取构造器实例,然后设置为允许访问私有构造方法,最后通过newInstance()方法创建枚举对象。但枚举在最后一步是会抛出异常的。因为在newInstance()方法的源码中会检查当前类是否是枚举,是枚举就会抛出异常。

7.尝试反射破坏枚举

如果我们通过反射破坏枚举就一定会抛出newInstance()方法里的异常吗?答案是不一定,我们来尝试使用反射来破坏单例的枚举类,看会发生什么。

如下,通过编译生成的class类以及反编译中均显示有私有无参构造。那么我们从无参构造出发,来通过反射创建枚举对象。代码如下:

@Test
void test5() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
   Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(null);
   declaredConstructor.setAccessible(true);
   Single single = declaredConstructor.newInstance();
   Single single1 = declaredConstructor.newInstance();
   System.out.println(single);
   System.out.println(single1);
}

结果如下:

但是结果并不尽人意,报错意思是没有这个空参构造方法,这就奇了怪了,从上面的class文件以及反编译中都显示有啊,难道显示的骗了我们吗。难道没有这个无参构造吗?

再换一个jad反编译工具。

 原来并不是无参构造,而是这个隐藏其中的有参构造呀。接下来按照这个有参构造来通过反射创建对象。

@Test
void test5() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
     Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(String.class,int.class);
     declaredConstructor.setAccessible(true);
     Single single = declaredConstructor.newInstance();
     Single single1 = declaredConstructor.newInstance();
     System.out.println(single);
     System.out.println(single1);
    }

结果

这个结果跟之前源码中反射创建枚举实例对象的异常一样。这还真说明说明java居然骗了我们哈哈。

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

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

相关文章

unity程序简易框架

1. 框架基本结构 2. 单例模式基类模块 2.1 BaseManager.cs using System.Collections; using System.Collections.Generic; using UnityEngine;public class BaseManager<T> where T:new() {private static T instance;public static T GetInstance(){if (instance == …

直播预约|2024 乐鑫科技全球开发者大会亮点揭秘

时隔一年&#xff0c;2024 乐鑫科技全球开发者大会将在 9 月 3-5 日如约而至。这一年&#xff0c;人工智能给物联网领域带来了哪些变化&#xff1f;乐鑫在 AIoT 领域实现了哪些新突破&#xff1f;ESP-IDF 经过升级后增添了哪些新功能&#xff1f;ESP32-P4、ESP32-C6 迎来了怎样…

C语言指针详解-上

C语言指针详解-上 前言1.指针的基本概念1.1指针是什么1.2指针的声明与初始化1.3取地址符&和解引用符*& 运算符用于**获取变量的地址*** 运算符用于访问指针指向的值 2.指针的类型常见数据类型的指针指针与数组、字符串数组指针结构体指针函数指针二级指针void指针 3.指…

多级评论的实现,评论回复功能(两层型)

前言&#xff1a; 技术栈&#xff1a;springboot mysql mybatis 比着写一遍&#xff0c;你也一定会实现多级评论的功能&#xff0c;简单易上手&#xff01; 1.效果 整体的结构只有两层&#xff0c;但是确实现了评论回复功能 [ { "id": 2, "userId": …

让JMeter测试数据生成更容易

让JMeter测试数据生成更容易 背景&#xff1a; 在软件测试过程中&#xff0c;为了确保系统的稳定性和可靠性&#xff0c;需要对各种场景进行全面的测试。而不同的场景往往需要各种各样的测试数据&#xff0c;这些数据需要具有一定的真实性和多样性&#xff0c;以模拟实际使用…

零基础小白实现C#调用halcon dll的过程,并测试程序证明C#halcon联合开发成功

本篇将介绍零基础小白实现C#调用halcon dll的过程&#xff0c;同时这其中涉及到很多知识&#xff0c;务必将HDevelop和VisualStudio安装成功。下面我将详细阐述C#调用halcon dll的过程&#xff0c;你的点赞和评论是我一直前行的动力。 1. C#调用Halcon DLL的过程 要在C#中调用…

选择TPM管理咨询公司,这几点是关键!

在这个日新月异的商业时代&#xff0c;企业的竞争力不仅体现在产品和服务上&#xff0c;更在于其内部管理的精细化与高效化。TPM作为提升企业综合生产效率的利器&#xff0c;正逐步成为众多企业转型升级的必然选择。然而&#xff0c;如何在众多TPM管理咨询公司中慧眼识珠&#…

嘉盛:股指、国债及商品期货分析

股指展望中证全指在2024年下半年可能迎来约20%的上涨空间。尽管市场估值较低&#xff0c;风险溢价仍维持在高位&#xff0c;这提升了指数的配置性价比。预计无风险利率将继续维持在较低水平&#xff0c;对中证全指估值产生积极影响。盈利方面&#xff0c;预计下半年中证全指EPS…

【广东】邀您共赴纷享销客生态伙伴大会

在数字化浪潮的驱动下&#xff0c;CRM行业正迎来前所未有的创新机遇。作为CRM领域的引领者&#xff0c;纷享销客特别举办“纷享销客2024生态伙伴大会&#xff08;广东站&#xff09;”。我们诚挚邀请各位生态伙伴莅临现场&#xff0c;共同探讨SaaS CRM行业的最新趋势&#xff0…

haproxy负载均衡+mysql读写分离

haproxy负载均衡 [roothaproxy01 ~]# yum -y install ntpdate [roothaproxy01 ~]# yum -y install ntp [roothaproxy01 ~]# systemctl start ntpd 安装haproxy [roothaproxy01 ~]# yum -y install ntpdate 配置文件的地址 [roothaproxy01 ~]# rpm -ql haproxy [roothap…

美摄科技携手蔚来汽车,共创用户出行新体验!

7月27日&#xff0c;蔚来举办了以 AI 为主线的 “NIO IN 2024 蔚来创新科技日”&#xff0c;此次活动展示了蔚来在智能电动汽车领域最新探索与成果&#xff0c;为中国乃至全球的智能汽车行业树立了新的标杆。当晚&#xff0c;央视《主播说联播》以“中国智能车在全球竞争的另一…

MIT6.s081 2021 Lab Multithreading

Uthread: switching between threads 思路 xv6 已经实现了进程的切换机制&#xff0c;本实验要求参考进程的切换&#xff0c;实现一个用户态线程的切换。 要实现线程切换&#xff0c;必然涉及上下文&#xff0c;即寄存器的保存和恢复&#xff0c;那么需要保存哪些寄存器&…

建筑工程项目管理系统-计算机毕设Java|springboot实战项目

&#x1f34a;作者&#xff1a;计算机毕设匠心工作室 &#x1f34a;简介&#xff1a;毕业后就一直专业从事计算机软件程序开发&#xff0c;至今也有8年工作经验。擅长Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等。 擅长&#xff1a;按照需求定制化开发项目…

建筑工地安全检查

在现代化的建筑工地中&#xff0c;安全始终是至关重要的核心问题。随着科技的不断进步&#xff0c;凡尔码建筑工地安全系统应运而生&#xff0c;灵活根据施工现场管理要求搭建建筑工地安全系统各个模块&#xff0c;为施工安全带来了全新的保障。 如何注册建筑工地安全系统后台…

自动打电话软件给企业带来了什么?

使用机器人外呼系统肯定都是想要给自己企业带来好处和解决问题的&#xff0c;想让自己的企业有所改变&#xff0c;有更好的发展&#xff0c;所以才会选择使用机器人外呼系统。而它也确实没让大家失望&#xff0c;使用了机器人外呼系统之后确实有许多企业发生了很大改变和进步&a…

鲁棒性目标检测 TOP2 方案分享

关联比赛: ACM MM2021 安全AI挑战者计划第七期&#xff1a;鲁棒性标识检测 ACM MM2021 鲁棒性目标检测比赛 TOP 2 方案 ​ 赛题背景 在商品知识产权领域&#xff0c;知识产权体现为在线商品的设计和品牌。不幸的是&#xff0c;在每一天&#xff0c;存在着非法商户通过一些…

一文学会本地部署可视化应用JSONCrack并配置公网地址实现远程协作

文章目录 前言1. Docker安装JSONCrack2. 安装Cpolar内网穿透工具3. 配置JSON Crack界面公网地址4. 远程访问 JSONCrack 界面5. 固定 JSONCrack公网地址 前言 本文主要介绍如何在Linux环境使用Docker安装数据可视化工具JSONCrack&#xff0c;并结合cpolar内网穿透工具实现团队在…

[Python学习日记-9] Python中的运算符

简介 计算机可以进行的运算有很多种&#xff0c;但可不只加减乘除这么简单&#xff0c;运算按种类可分为算数运算、比较运算、逻辑运算、赋值运算、成员运算、身份运算、位运算&#xff0c;而本篇我们暂只介绍算数运算、比较运算、逻辑运算、赋值运算 算数运算 一、运算符描述…

猫头虎分享:Python库 Pillow 的简介、安装、用法详解入门教程

猫头虎分享&#xff1a;Python库 Pillow 的简介、安装、用法详解入门教程 &#x1f4da; 大家好&#xff0c;今天猫头虎要和大家分享一款非常实用的 Python 图像处理库——Pillow。 &#x1f4a1; Pillow 是 Python 中非常流行的图像处理库&#xff0c;基于已经停止维护的 PI…

CE修改器步骤9学习教程

一、打开教程&#xff0c;因为我的电脑是64位的&#xff0c;所以打开这个&#xff08;x86_64&#xff09; 二、 跳转到步骤9&#xff0c;并让ce读取其内存 三、使用之前教程学到的知识&#xff0c;找到四个角色的健康值地址&#xff08;找到即可&#xff0c;不必找基址&#xf…