玩转单例模式

目录

1. 饿汉式 

2. 懒汉式

3. volatile解决指令重排序

4. 反射破坏单例模式

5. 枚举实现单例模式

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

7. 尝试反射破坏枚举

8. CAS实现单例模式


所谓单例模式,就是是某个类的实例对象只能被创建一次,单例模式有多种实现方式,如饿汉懒汉内部类枚举CAS等。接下来我们一一分析。

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()方法创建枚举对象时,这个方法内会判断当前类是否是枚举类,是的话直接就抛异常了,并且反编译之后可以看出,实例对象属性是用final static修饰的。正是这样,反射是不能创建枚举对象的。

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居然骗了我们哈哈。

8. CAS实现单例模式

在之前的懒汉式代码中,说到底还是由于在多线程下不能安全的更新变量。我们的解决办法是加锁。使其在修改变量操作时,变成串行执行。但是另一方面也降低了CPU效率,影响了性能。那是否能不用加锁,也能实现线程安全的单例模式?当然是有的,除了枚举,我们还可以通过CAS来实现,在Java中使用CAS就需要用到JUC包下的一个类 AtomicReference 。该类提供了一个方法compareAndSet(expectedValue, updateValue),这个方法就是CAS的实现,该操作也是原子的。通过比较内存值与expectedValue期望值是否相等,若相等,就更新为updateValue,不相等则不做操作。以下是代码实现:

public class Singleton {

    // 无参构造私有化
    private Singleton () {
    }

    // 使用AtomicReference来包装我们的单例对象
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    public static Singleton getInstance() {
       // 循环检查并更新实例
        while (true) {
            Singleton singleton = INSTANCE.get();
            if (singleton != null) {
                return singleton;
            }
            // 尝试创建一个实例,但注意,实例虽然创建了,但是不一定进入到AtomicReference中
            singleton = new Singleton();
            // 使用CAS操作来更新实例
            // 注意,就算上一步会发生多个线程都创建了对象的情况,但是走到下面代码块,
            // 最终只有一个线程的创建的实例被成功设置到AtomicReference中
            if (INSTANCE.compareAndSet(null,singleton)) {
                return singleton;
            }
        }

    }

}

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

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

相关文章

【安全工具推荐-Search_Viewer资产测绘】

目录 一、工具介绍 二、工具配置 三、传送门 一、工具介绍 Search_Viewer&#xff0c;集Fofa、Hunter鹰图、Shodan、360 quake、Zoomeye 钟馗之眼、censys 为一体的空间测绘gui图形界面化工具&#xff0c;支持一键采集爬取和导出fofa、shodan等数据&#xff0c;方便快捷查看…

批发供应系统:提升效率与竞争力的关键

在当今复杂多变的商业环境中&#xff0c;批发供应系统作为连接生产商、分销商与零售商的重要纽带&#xff0c;其效率与智能化水平直接决定了供应链的运作效率与市场竞争力。随着信息技术的飞速发展&#xff0c;尤其是大数据、云计算、人工智能&#xff08;AI&#xff09;及物联…

基于HarmonyOS的宠物收养系统的设计与实现(一)

基于HarmonyOS的宠物收养系统的设计与实现&#xff08;一&#xff09; 本系统是简易的宠物收养系统&#xff0c;为了更加熟练地掌握HarmonyOS相关技术的使用。 项目创建 创建一个空项目取名为PetApp 首页实现&#xff08;组件导航使用&#xff09; 官方文档&#xff1a;组…

Qt系列之数据库(三)补充篇

一、数据库删除操作&#xff1a; 基本语法 DELETE FROM table_name WHERE [condition]; DELETE FROM ---- 关键字 table_name ---- 表名 WHERE ---- 条件的关键字 [condition] --- 条件表达式在这里插入代码片具体使用&#xff1a; QString sqlDelete QString("DELETE…

落地 DevOps,探索高效研发运营一体化解决方案

前言与概述 伴随着企业业务的快速发展&#xff0c;为了支撑业务发展&#xff0c;提高 IT 对业务的支撑能力建设。在研发工程协同方面&#xff0c;希望加强代码管理&#xff0c;实现持续构建、自动化测试、自动化部署、自动化运维&#xff0c;同时加强产品的安全和质量管理&…

ggplot阶截断坐标轴-gggap

目录 gggap包安装 功能查询 简单版使用代码 复杂版使用代码 gggap包安装 CRAN: Package gggap (-project.org) 手动下载安装 功能查询 > ?gggap > ?gggapDefine Segments in y-Axis for ggplot2 Description Easy-to-define segments in y-axis for ggplot2. …

React+Vis.js(05):节点的点击事件

文章目录 需求实现思路抽屉实现完整代码需求 双击节点,弹出右侧的“抽屉”,显示节点的详细信息 实现思路 vis.network提供了一个doubleClick事件,代码如下: network.on(doubleClick, function (properties) {// console.log(nodes);let id = properties

【数据结构】PTA 带头结点的链式表操作集 C语言

本题要求实现带头结点的链式表操作集。 函数接口定义&#xff1a; List MakeEmpty(); Position Find( List L, ElementType X ); bool Insert( List L, ElementType X, Position P ); bool Delete( List L, Position P ); 其中List结构定义如下&#xff1a; typedef struc…

STM32第十二节(中级篇):串口通信(第一节)——功能框图讲解

前言 我们在51单片机中就已经学习过了串口通信的相关知识点&#xff0c;那么我们现在在32单片机上进一步学习通信的原理。我们主要讲解串口功能框图以及串口初始化结构体以及固件库讲解。 STM32第十二节&#xff08;中级篇&#xff09;&#xff1a;串口通信&#xff08;第一节…

漏洞扫描的重要性,如何做好漏洞扫描服务

随着互联网技术的飞速发展&#xff0c;网络安全问题已成为不容忽视的重大挑战。其中&#xff0c;系统漏洞威胁作为最常见且严重的安全危险之一&#xff0c;对组织和个人的信息资产构成了巨大威胁。下面我们就来了解下漏洞扫描的好处、漏洞扫描的操作方法以及如何做好网络安全。…

使用 onBeforeRouteUpdate 组合式函数提升应用的用户体验

title: 使用 onBeforeRouteUpdate 组合式函数提升应用的用户体验 date: 2024/8/15 updated: 2024/8/15 author: cmdragon excerpt: 摘要&#xff1a;本文介绍如何在Nuxt 3开发中使用onBeforeRouteUpdate组合式函数来提升应用用户体验。通过在组件中注册路由更新守卫&#xf…

个人理解—MKCONFIG的常用配置参数与链接脚本

前面的文章说到&#xff0c;编写Makefile文件的常用语句以及相应的语法&#xff0c;但也提到了MKCONFIG去控制Makefile文件的变量实现条件编译&#xff0c;在MKCONFIG过程中&#xff0c;常用的变量配置有例如架构配置、交叉编译工具链配置等&#xff0c;这些选项要么你去通过改…

界面控件DevExpress .NET MAUI v24.1 - 发布TreeView等新组件

DevExpress拥有.NET开发需要的所有平台控件&#xff0c;包含600多个UI控件、报表平台、DevExpress Dashboard eXpressApp 框架、适用于 Visual Studio的CodeRush等一系列辅助工具。屡获大奖的软件开发平台DevExpress 今年第一个重要版本v23.1正式发布&#xff0c;该版本拥有众多…

10 个 C# 关键字和功能

在 Stack Overflow 调查中&#xff0c;C# 语言是排名第 5 位的编程语言。它广泛用于创建各种应用程序&#xff0c;范围从桌面到移动设备再到云原生。由于有如此多的语言关键字和功能&#xff0c;对于开发人员来说&#xff0c;要跟上新功能发布的最新信息将是一项艰巨的任务。本…

基于ssm+vue+uniapp的二手物品交易平台小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…

数据结构--图(Graph)

定义 图&#xff08;Graph&#xff09;是由顶点的有穷非空集合和顶点之间边的集合组成的一种非线性表结构&#xff0c;通常表示为&#xff1a;G(V,E)&#xff0c;其中&#xff0c;G表示一个图&#xff0c;V是图G中顶点的集合&#xff0c;E是图G中边的集合。 顶点&#xff08;…

大模型之战-操作数据表-coze

工作流直接操作数据库啦【何时可以直接访问自己的数据库呢】 1&#xff0c;第一步创建一个bot智能体 1.1&#xff0c;bot中创建数据库表&#xff1a; 1.2&#xff0c;智能体可以通过对话&#xff0c;操作表&#xff1b;【增加&#xff0c;筛选查询等】 1.2.1&#xff0c;增加…

C++ 设计模式——模板方法模式

模板方法模式 模板方法模式逐步重构并引入模板方法模式初始实现提取共性并引入模板方法模式实现具体类 完整代码示例模板方法模式的 UML 图UML 图详细介绍 模板方法模式适用于以下场景 模板方法模式 模板方法模式是一种行为设计模式&#xff0c;它定义了一个算法的骨架&#x…

Python(PyTorch)硅光电倍增管和量化感知训练亚光子算法验证

&#x1f3af;要点 &#x1f3af;亚光子光神经网络矩阵计算 | &#x1f3af;光学扇入计算向量点积 | &#x1f3af;表征测量确定不同光子数量下计算准确度 | &#x1f3af;训练全连接多层感知器基准测试光神经网络算法数字识别 | &#x1f3af;物理验证光学设备设置 | &#x…

美股收涨,半导体板块领涨;苹果iPhone出货预测上调

市场概况 在昨夜的交易中&#xff0c;美股三大股指全线收涨。道琼斯工业平均指数上涨1.39%&#xff0c;纳斯达克综合指数上涨2.34%&#xff0c;标准普尔500指数上涨1.61%。值得注意的是&#xff0c;英伟达股票涨幅近4%&#xff0c;推动了科技股的整体表现。美国十年期国债收益…