JavaEE-多线程编程单例模式

news2024/9/22 11:34:37

一、等待通知

系统内部,线程之间是抢占式执行的,随即调度,程序可以通过手动干预的方式,能够让线程一定程度的按咱们想要的顺序执行,无法主动让某个线程被调度,但可以主动让某个线程等待。等待通知可以安排线程之间的执行顺序。

举个栗子:当t1线程要在队列获取元素,由于此时队列是空的无法进行工作,它只能频繁的进行获取释放锁的操作,导致其他线程不能得到cpu分配资源,线程中调度是无序的,这种情况很可能出现,称为——线程饿死(不会像死锁那样卡死,但是可能会卡一下,影响程序效率)

等待通知机制可以解决上述问题条件判断是否能执行当前逻辑,不能就主动wait阻塞等待,把执行的机会让给别的线程,避免该线程进行一些无意义的重试,等时机成熟时(其他线程通知-notify),阻塞被唤醒。代码实现

public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                System.out.println("t1等待前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1等待后");
        });

当我们执行这样的逻辑时,线程就会在执行完第一句输出语句后通过wait阻塞等待,注意因为wait操作被执行时是先解锁然后阻塞等待,解锁的前提是有锁,所以需要在操作前先加锁。此时可以通过jconsole来查看线程状态:

可以看出此时是WAITING状态。再写另一个线程来唤醒它:

Thread t2 = new Thread(()->{
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker){
               System.out.println("t2唤醒前");
               locker.notify();
               System.out.println("t2唤醒后");
           }
        });

此时工作台可以从输出顺序看到执行过程

(1)t1线程获取锁 (2)t1线程阻塞等待且解锁 (3)t2线程获取锁 (4)t2线程唤醒t1线程,执行完逻辑后释放锁 (5)t1重新获取锁,从上次被阻塞的地方继续执行。t1的状态变化是:WAITING->RUNNABLE->BLOCKED 处于blocked状态是因为唤醒后需要等t1先释放锁。

注意:notify一次只能唤醒一个线程,而且是随机的。不过notify也有可以唤醒所有线程的方法:

locker.notifyAll();

wait也有一个带参数的版本,无参数版本采用的是死等战术,等不到唤醒程序就一直等,带参数版本和join差不多,过了参数时间就不会再阻塞状态。

二、单例模式

单例模式是一种经典的设计模式,相比其他的设计模式算是比较简单的设计模式,也是面试中常考的设计模式。

单例模式->单个实例,整个进程中有且只有一个对象,这样的对象就成为单例(instance),那么如何保证进程中只有一个实例呢?

需要让编译器帮我们进行检查,通过编码上的技巧,使编译器自动发现我们是否创建了多个实例,并尝试创建多个实例时,直接编译报错。

单例模式有很多种写法,本篇文章主要介绍两种:饿汉模式&懒汉模式

1、饿汉模式

先看这样的一串代码:

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

static成员初始化时机是在类加载的时候,可以简单理解为JVM一启动就立即加载,成员也就立即创建了。static修饰的类属性是类对象的,每个类的类对象在JVM中只有一个,里面的静态成员只有一个,初始化也只执行一次,当后续需要这个类的实例时可以通过方法来获取已经创建好的实例,而不是再创建新的,这个方法为:

 public static Singleton getInstance() {
        return instance;
    }

那么如果其他线程想通过此类创建新的对象该怎么办呢?

当类之外的代码想尝试创建新的对象时一定会调用构造方法,所以将构造方法的权限设置为private时就会无法调用,编译报错。如下:

private Singleton(){
     /  
    }

当类一加载静态成员就被创建了,就像饿的人看见吃的会想赶紧吃的感觉一样,所以这种模式可以被称为“饿汉模式”。

2、懒汉模式

在计算机中,懒往往是一个褒义词,代表着高效率。相对于饿汉模式一加载类就创建对象,懒汉则是当第一次需要使用对象才会去创建,就把创建实例的代价省下来了,按照这个思路来创建类:

class SingletonLazy{
    public static SingleLazy instance = null;

    public static SingleLazy getInstance() {
        if(instance == null){
            instance = new SingleLazy();
        }
        return instance;
    }
}

先将静态成员的引用指向空,当需要创建实例时判断当前引用是否为空,为空时再创建新的,不为空就直接返回实例。懒的本质就是偷懒,能少做就少做,懒->缓。

如果代码中存在多个单例类,都使用懒汉模式的话这些实例会在程序启动时扎堆的创建,可能把程序启动时间拖慢,如果使用饿汉模式的话,调用时机是分散的,化整为0,让用户感受不到卡顿

多线程模式下分析懒汉模式与饿汉模式

思考当多个线程同时getInstance时这两种模式是否会引起线程不安全问题?

饿汉模式安全,但懒汉模式是不安全的。

饿汉模式安全的原因:创建实例的时机是java进程启动时,比主线程还早创建,因此在其他线程调用getInstance时实例肯定已经创建好了,每个线程只做了一件事,就是读取上述静态变量的值,多个线程读取一个变量,安全。

而懒汉模式与其不同,懒汉模式的关键操作代码是这些

第一行是:“读”,查看一下实例引用的地址的是否为空,而第二行是赋值,也就是修改操作,上述操作在多线程环境下容易出现问题,比如会产生下面这种执行顺序

假定最初instance引用为空,t1判断引用为空,t2判断引用为空,t1创建实例对象,由于t2已经判定完是否为空,所以也会创建实例对象。

上述代码t2创建的引用会覆盖掉t1的引用的地址,进一步t1的instance没有指向了就会被GC回收掉

解决办法可以通过加锁的方式来保证懒汉模式下getInstance是安全的,当t1线程进入判定语句时t2需阻塞等待,t1创建完实例释放锁后t2才能获取锁,开始判定操作,此时的instance就已经指向了地址不为空了。初步优化后的代码:

class SingletonLazy{
    public static SingletonLazy instance = null;

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

但是懒汉模式只有在初次调用getInstance时会涉及到线程安全问题,一旦实例创建好了后面再调用都是只读操作,不涉及线程安全问题,而后续调用明明没有线程安全问题还要加锁,增加了没必要的开销

解决办法在加锁前再判断一次当前调用是否为第一次调用,如果是第一次调用再去获取锁,判定条件还是看instance是否为空即可。

别忘了上篇文章提到的volatile,二次优化后的代码:

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

通过双重if避免了重复创建对象。

下篇文章更新多线程编程经典案例二——阻塞队列

感谢观看

道阻且长,行则将至

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

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

相关文章

嵌入式人工智能(45-基于树莓派4B的扩展板-舵机驱动板PCA9685)

1、简介 智能小车、机械臂、摄像头云台会有多个舵机,而微控制器芯片的PWM输出引脚不够的情况下,就可以用PCA9685(16路舵机)来解决这一问题。 PCA9685是一款I2C总线控制的16通道LED控制器,专为红/绿/蓝/琥珀&#xff…

Spring Boot - 在Spring Boot中实现灵活的API版本控制(下)_ 封装场景启动器Starter

文章目录 Pre设计思路ApiVersion 功能特性使用示例配置示例 ProjectStarter Code自定义注解 ApiVersion配置属性类用于管理API版本自动配置基于Spring MVC的API版本控制实现WebMvcRegistrations接口,用于自定义WebMvc的注册逻辑扩展RequestMappingHandlerMapping的类…

医院预约挂号小程序的设计

管理员账户功能包括:系统首页,个人中心,用户管理,医生管理,科室分类管理,医生信息管理,预约挂号管理,系统管理 微信端账号功能包括:系统首页,医生信息&#…

Python酷库之旅-第三方库Pandas(074)

目录 一、用法精讲 301、pandas.Series.dt.components属性 301-1、语法 301-2、参数 301-3、功能 301-4、返回值 301-5、说明 301-6、用法 301-6-1、数据准备 301-6-2、代码示例 301-6-3、结果输出 302、pandas.Series.dt.to_pytimedelta方法 302-1、语法 302-2、…

17.1 分布式限流组件Sentinel

17.1 分布式限流组件Sentinel 1. Sentinel介绍1.1 Sentinel 介绍1.2 Sentinel 功能和设计理念流量控制2. Sentinel安装控制台2.1 概述2.2 启动控制台*****************************************************************************1. Sentinel介绍 github 官方中文文档 1.…

Rest风格快速开发

Rest风格开发简介 简单点来说,Rest风格的开发就是让别人不知道你在做什么,以deleteUserById和selectUserById为例: 普通开发:路径 /users/deleteById?Id666 /users/selectById?Id666 别人很容易知道你这是在干什么 Rest风…

半导体行业人士宋仕强谈生产力

近日,半导体行业人士,金航标电子和萨科微创始人宋仕强强调了技术进步与管理创新在提升生产效率中的作用。深圳作为中国效率驱动发展模式的典范,其核心竞争力在于高效利用资源。从早期的快速城市建设到现今华强北电子市场的繁荣,深…

批量ncm转mp3

软件上线一段时间后发现大家用ncm转MP3功能比较多,并且很多用户都是同时转换好几个音乐,为了方便大家使用这里就给大家提供了一个批量ncm转MP3的功能,下面简单介绍一下如何使用 打开智游剪辑(zyjj.cc),搜索…

Mouser中元件特性对比功能

搜索所需的元件,并点击比对 在比对界面里搜索所需比对的另外元器件,并比对3.得到的结果

深入探索 Wireshark——网络封包分析的利器

一、引言 在当今数字化的时代,网络通信变得日益复杂和关键。无论是企业的网络运维,还是网络安全研究,都需要深入了解网络中传输的数据。Wireshark 作为一款强大的网络封包分析工具,成为了网络工程师、安全研究人员和技术爱好者不…

linux 查看端口占用并处理

lsof 命令 lsof -i:端口注意pid netstat 命令 netstat -tnpla | grep 端口注意pid 查看详情 ps -ef | grep 3766607删除 kill -9 PIDkill -9 3766607

OpenCV图像滤波(7)cv::getDerivKernels() 函数的使用

操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 函数返回用于计算空间图像导数的滤波系数。 该函数计算并返回用于空间图像导数的滤波系数。当 ksizeFILTER_SCHARR 时,生成 Scharr 3…

【Python机器学习】树回归——复杂数据的局部性建模

线性回归包含一些强大的方法,但这些方法创建的模型需要拟合所有的样本(局部加权线性回归除外),当数据拥有众多特征并且特征之间关系十分复杂时,构建全局模型的想法就显得很困难,也略显笨拙。而且&#xff0…

MySql 5.7.1 分区的实践

在性能优化中,Mysql 进行分区,能有效提高查询效率,因此开始百度了起来。但是结果总不是那么一番风顺的。如今使用 uuid 作为主键的情况已是主流,因此在给表增加分区时,出现了如下错误: 错误: A …

FishSpeech 实测,免费语音克隆神器,5分钟部署实战,让川普给你来段中文绕口令?

拍短视频,开始的时候是真人语音,之后是电脑配音,今年年初剪映上线了克隆语音,很多人都用起来了。 想要克隆别人的语音怎么办? 之前需要用 GPT-SoVITS 训练声音模型,操作复杂,对电脑配置要求较…

【算法设计】深入理解波兰表达式与逆波兰表达式

文章目录 介绍1. 波兰表达式(Prefix Notation)2. 逆波兰表达式(Postfix Notation)3. 比较与优劣4. 简单示例5. 实例演示6. 应用场景和案例7. 中缀表达式转后缀表达式8. 结论 更多相关内容可查看 应用场景:Excel自定义公…

Mozilla Firefox侧边栏和垂直标签在131 Nightly版本中开始试用

垂直选项卡和全新的侧边栏体验现已在Mozilla Firefox Nightly 131 中提供。这一更新备受社区期待和要求,我们期待看到它如何提高您的浏览效率和工作效率。如果您想体验一下这项正在进行中的工作,请这样操作: 更新到最新的Nightly版 转到设置…

LeetCode刷题笔记第49题:字母异位词分组

LeetCode刷题笔记第49题:字母异位词分组 题目: 想法: 遍历列表中的所有字符串,将字符串中字符进行排序并作为字典的键,并将原字符串做为值保存,最终输出字典的值就是最终的答案。 class Solution:def gr…

MySQL 日志表改造为分区表

文章目录 前言1. 分区表改造方法2. 操作步骤2.1 调整主键2.2 无锁变更2.3 回滚策略 3. 分区表维护3.1 创建分区3.2 删除分区3.3 分区表查询 后记 前言 业务有一张日志表,只需要保留 3 个月的数据,仅 3 月的数据就占用 80G 的存储空间,如果不…

二维数组指针,指针数组,指针函数

指针 操作 二维字符型数组 1、 首先理解二维数组指针 int a[3][4]; 第一步,确定基类型:上面的数组从本质上讲,是一维数组的数组,写成int[4] a[3]可以更好的理解,a[3]是一个一维数组,其数组…