Java多线程案例之单例模式(饿汉,懒汉)

news2024/9/28 17:26:08

目录

一、饿汉模式

二、懒汉模式

前言:单例模式是校招中最常见的设计模式之一。下面我们来谈谈其中的两个模式:懒汉,饿汉。

何为设计模式?

设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

日常开发中,虽然不能百分之白解决问题,但是大大提高了普通开发者的下限。

单例模式的概念

在应用这个模式时,单例对象的类必须保证只有一个实例存在。

为何有单例模式的出现?

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。

这种方式简化了在复杂环境下的配置管理。

ps:说到这里我们想到Java中JDBC的DataSoucre只需要一个实例,但是如果我们想创建多个实例语法上是没有问题的(虽然会造成代码的冗余)。

因此单例模式本质上就是:借助编程语言自身的语法特性,强制限制某个类,不能创建多个实例。

一、饿汉模式

话不多说,直接看代码(注意看注解):

class Single{
    //用static修饰,这样就可以让instance变为这个类的唯一实例。
    private static Single instance = new Single();

    //因为要在不创建额外实例的情况下调用这个方法,必须将其设置为static方法
    public static Single getSingle() {
        return instance;
    }
    //为了防止Single在类外可以被实例,这边将其的构造方法设计未private。
    private Single() {

    }
}
public class Demo16 {
    public static void main(String[] args) {
        Single instance = Single.getSingle();
    }
}

需要注意的是:饿汉模式是在类加载阶段创建出实例的。(ps:类加载阶段创建实例相对于普通情况早了许多,这也是为什么叫“饿汉”的原因。一个饿了几天的人,对食物的没有抵抗力的,一下子就开始吃了。)

为何饿汉模式不需要考虑线程安全的问题?

首先,我们需要明白线程安全出现的原因是什么;该模式只涉及到单纯的读取数据,并不涉及修改,因此该模式线程安全。

二、懒汉模式

类加载的同时,不再创建实例,而是等第一次使用的时候再创建。(创建的时机更迟,很好的避开了程序刚启动时候资源紧张的情况,提高了效率。)

懒汉模式——单线程版本

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

懒汉模式——多线程版本

在多线程的环境下,如果不使用synchronized关键字进行加锁会引发线程安全问题,原因如下:

 实现代码:

class Singleton{
    private static Singleton instance = null;

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

    }
}

 仔细思考后,我们会发现其实上面多线程版本的懒汉模式是有一定问题的。

虽然通过加锁的方式解决了线程安全问题,但是在这同时又引入了新的问题,因为这里的线程不安全并不是永久性的,什么意思呢?

当我们代码创建了第一个实例之后,那么其实就不需要再执行if语句里面的内容了(条件判断失败),但是我们加锁了之后,就会导致每次执行这个方法,我们都需要进行加锁,这种无脑的加锁方式,会导致程序运行的开销变大(因为加锁可能设计 用户态->内核态 之间的转换,这样的转换成本很高)。

ps:这也是为什么Vector,StringBuffer ,HashTable等不推荐用的原因,因为它们的内部有许多像这样无脑加锁的代码。

解决方案:给外层的嵌套循环加上if。

实现代码:

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

分析:虽然这里使用了两个if语句,但是它们每个的代表含义是不同的,因为有加锁的存在,两个if执行的时间间隔很有可能是特别长的,因为加锁可能会产生锁竞争,竞争就意味着某些线程就会进入阻塞状态,那么什么时候这个处于阻塞状态的线程被唤醒,就无法确认了。

这也就导致了同一个线程中的两个if语句的结果可能是截然不同的,比如 第一个if成立进入,第二个if不成立。

举个例子:

有三个线程同时调用了getInstance这个方法,通过外层的if语句进入,三个线程发生锁竞争,当其中的某个线程拿到锁之后,另外两个线程进入阻塞等待状态,之后这个拿到锁的释放了锁之后(这时已经实例化一个对象了),另外的两个线程再次发生锁竞争,两个中的某个线程拿到锁之后,进行了内层的if条件判断(刚刚这两个线程都已经判断了外层的if条件),发现instance不为空,于是就不再创建实例了。而在这三个线程之后的线程中,如果有人调用getInstance方法,就会在外层的if语句中发现instance不为空,就不再进入内部了。

这样两个if的做法,就很好的提高了效率。

当然,即使修改到这,仍然是不够的,因为这里还存在指令重排序问题。

在多线程的环境下,很可能会出现频繁的判断,这时线程不会读内存中的数据,而是会去读寄存器中的数据,可能instance的值以及发生了改变,但是线程却浑然不知,为了防止这一现象出现,我们使用volatile修饰instance变量,防止这一因为编译器优化而带来的问题。

附上代码:

class Singleton{
    private static volatile Singleton instance = null;

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

    }

}

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

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

相关文章

《Linux性能优化实战》学习笔记 Day04

06 | 锁:如何根据业务场景选择合适的锁? 原文摘抄 当你无法判断锁住的代码会执行多久时,应该首选互斥锁,互斥锁是一种独占锁。 如果你能确定被锁住的代码执行时间很短,就应该用自旋锁取代互斥锁。 对于 99% 的线程…

工信部电子标准院:龙蜥操作系统获评“优秀”

近日,工信部中国电子技术标准化研究院公布第二批通过开源项目成熟度评估的开源项目名单,龙蜥操作系统(Anolis OS)凭借在生态构建、技术创新、应用落地等方面的成熟能力与卓越表现,顺利通过评估并获得优秀贰级&#xff…

简单说说什么是真的懂一道题了

昨天写了Richard的2022年一年级入学小结后,后台有读者问我怎么算真的懂一个知识点了,今天来简单说两句,大家都知道,求123…n的和,高斯的幼年成名作。Richard同学在上中班的时候,我就给他科普过高大神的事迹…

2023牛客寒假算法基础集训营3 -- E-公平守望的灯塔(向量 简单几何)

题目如下: 示例1 输入 1 0 0 1输出 0 0说明 输出1 1也是可以的。 思路 or 题解: 我们可以迅速找到 CCC 有两个位置满足题意,但 CCC 的坐标不一定是整数,我们需要 checkcheckcheck MMM 是 ABABAB 的中点 我们可以求出来 AM→…

「栈和队列」简析

前言 前言:研究一个数据结构的时候,首先讲的是增删改查。 文章目录前言一、简介1. 结构2. 特点3. 存储二、栈1. 类比举例2. 操作3. 实现1)顺序栈(常用)a. 核心b. 要素c. 入栈d. 出栈2)链式栈三、队列1. 类比…

Spring的Aware接口讲解,内含几篇参考文章

Aware接口文章一、使用Aware与不使用Aware的效果二、ApplicationContext实例介绍Aware三、从Aware顶级接口实现自己的XXXAware一、使用Aware与不使用Aware的效果 参考文章:Spring中的aware接口 定义BeanNameAware接口 public interface BeanNameAware extends Aw…

树的应用举例——并查集和树状数组

并查集 并查集是利用森林来描述一些不相交的集合,并支持集合的合并操作和查询操作。 假设有n个元素,分为m个不相交集合,一个集合构成一棵树,同一棵树(集合)中的元素地位相等。例如下图的森林表示一个包含…

【Python】Numpy多项式详解

文章目录多项式简介构造函数与图像运算符重载常用方法多项式简介 Numpy.polynomial中封装了六种多项式类,除了常规的多项式a0a1x⋯anxna_0a_1x\cdotsa_nx^na0​a1​x⋯an​xn之外,还有五种在数学、物理中常用的正交多项式,例如Hermite多项式…

祝贺!2022 Rust中文社区线上Hackathon评审结果出炉!

完全超出预期(的优秀!)!这次黑客松共有9支队伍闯入最后的评审会。他们是:233bithttps://github.com/rustcc/hackathon2022/tree/master/233bitRust 实现的 合成材料计算器EvolutionLabhttps://github.com/rustcc/hacka…

来自AI浩的新年祝福

大家好,感谢大家这一年来的关注和支持,在新春即将到来之际,AI浩祝大家: 祝大家新的一年里生活能够像下面的兔子那样,多彩多姿! 祝愿大家,快乐常伴,笑口常开! 在新的…

chfs安装使用注意事项及如何实现http外网访问

对于分享电脑上的文件,或与手机互传文件,除了 QQ、微信还有很多专业的工具,Cute Http File Server (缩写为 chfs),就是一个免费小巧的 HTTP 文件共享服务器工具,它可以跨平台支持 Windows、Mac 和 Linux,只…

37岁被裁,但毫无遗憾:小镇出身,一穷二白,完整吃过互联网+房地产红利,现在上海有千万房产!...

有人被裁是失业,有人被裁是退休,有时候人类的悲欢真的不相通,来看看这位网友的故事:37岁,年底被裁,但是不留遗憾了。算是完整吃过一波互联网房地产红利,小镇出身,一穷二白&#xff0…

蚂蚁整改,暗藏深意

‍数据智能产业创新服务媒体——聚焦数智 改变商业互联网金融可以提升金融服务的效率和质量,促进传统产业转型升级,是资本竞相追逐的丛林,也是互联网巨头间相互争抢的战略高地。通过上市公司分拆子公司进行上市,可以获得一系列好…

C语言结构体 笔记

C语言提供结构体来管理不同类型的数据组合。C语言中的结构体类似于Java中的类。声明结构体类型定义变量名定义变量名和初始化结构体对齐结构体的大小必须是其最大成员的整数倍!(1)例如,此结构体的大小为16。因为double类型占8个字节&#xff…

聊一聊用户增长

#01 什么是用户增长 用户增长基本上会涉及生意场上的各行各业,你开个店面希望有更多的客户光顾,你做了个APP希望有更多的用户经常使用,你搭建了个电商平台希望有更多的人下单买东西。 用户增长,即以提升用户LTV为目的&#xff08…

MongoDB学习笔记【part1】概念与安装

一、NoSQL简介 NoSQL Not Only SQL,不仅仅是SQL,泛指非关系型数据库。NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。 特点:不遵循SQL标准、不支持ACID(原子、一致…

Linux常用命令——sysctl命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) sysctl 时动态地修改内核的运行参数 补充说明 sysctl命令被用于在内核运行时动态地修改内核的运行参数,可用的内核参数在目录/proc/sys中。它包含一些TCP/ip堆栈和虚拟内存系统的高级选项&#xff…

FANUC机器人INTP-250或251用户坐标系或工具坐标系与示教资料不符报警的处理办法

FANUC机器人INTP-250或251用户坐标系或工具坐标系与示教资料不符报警的处理办法 在机器人的日常使用过程中,有可能会碰到这样的情况: 点位的示教是在工具坐标系1、用户坐标系0下示教的,如下图所示, 但是使用过程中可能被自己或别人不小心修改成了工具坐标系2、用户坐标系…

HTML学习02

表格标签的学习 表格 table 行 tr列 td表头列 thtable中有如下属性 border:表格边框的粗细width:表格的宽度cellspacing:单元格间距cellpadding:单元格填充. tr中有一个属性: align -> center , left , right …

数和森林(快来瞧)

森林的定义 森林是由多颗互不相交的树所构成的树的集合,即森林包含多棵树,每一棵树都有自己的根结点。一棵树也可以看成森林。 树的表示及基本操作 1.树(一般树)的表示方法 1.1树的双亲表示法 树的双亲表示法是将树的各个节点…