JavaEE-多线程初阶3

news2024/11/25 22:33:50

✏️作者:银河罐头
📋系列专栏:JavaEE

🌲“种一棵树最好的时间是十年前,其次是现在”

目录

  • volatile关键字
  • wait 和 notify
  • 多线程案例
    • 单例模式
      • 饿汉模式
      • 懒汉模式

volatile关键字

volatile : 易变的,易失的

volatile和内存可见性是密切相关的。

class MyCounter{
    public int flag = 0;
}
public class ThreadDemo{
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();
        Thread t1 = new Thread(()->{
            while(myCounter.flag == 0){
                //
            }
            System.out.println("t1 循环结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            myCounter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

image-20221213135416359

image-20221213135654353

这就和预期不符。

这个情况就是“内存可见性问题”,这也是一个线程不安全问题。

while(myCounter.flag == 0){
     //
}
//这里使用汇编来理解,大概就是2步操作,
//1. load , 把内存中 flag 的值,读取到寄存器里
//2.  cmp , 把寄存器的值和0进行比较,根据比较结果,决定下一步往哪个方向执行

这个循环执行速度极快,一秒钟执行百万次以上。

CPU针对寄存器的操作要比内存操作快很多,快3-4个数量级。计算机对于内存的操作,比硬盘快3-4个数量级。

循环执行这么多次,在t2真正修改之前, load得到的结果都是一样的,load和cmp相比速度慢很多。

由于load速度比cmp慢太多+反复load得到的结果是一样的。JVM就不再真正的重复load了,判定好像没人改flag值,干脆就只读取一次(编译器优化的一种方式)

内存可见性:一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改。此时读到的值不一定是修改之后的值,这个读线程没有感知到变量的变化。(归根结底是编译器/JVM在多线程环境下优化时产生误判了)

此时就需要人为手动干预,可以给flag这个变量加上volatile关键字,意思是告诉编译器,这个变量是“易变的”,一定要每次都重新读取这个变量的内存内容,不确定什么时候就变了,不能再进行激进的优化。

volatile public int flag = 0;

image-20221213145004722

volatile只能修饰变量。

volatile不能修饰方法里的变量(局部变量),局部变量只能在当前线程里面用,不能多线程之间同时读取/修改,天然的就规避了线程安全问题。

方法内部的变量在“栈”这样的空间上。每个线程都有自己的栈空间。即使是同一个方法,在多个线程中被调用,这里的局部变量也会处在不同的栈空间中,本质上是不同变量。

栈就是记录了方法之间的调用关系。

上述说的内存可见性 编译器优化问题,也不是始终会出现的(编译器可能会误判,也不是100%误判)

class MyCounter{
    public int flag = 0;
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();
        Thread t1 = new Thread(()->{
            while(myCounter.flag == 0){
                //休眠
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 循环结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            myCounter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}
//去掉volatile,在循环体加休眠控制循环速度
//输出结果:
请输入一个整数:
1
t1 循环结束

编译器的优化,往往是无法预期的,所以稳妥点加上volatile

内存可见性问题,其他的一些资料,谈到 JMM,Java Memory Model , Java内存模型。

从 JMM的角度重新表述内存可见性的问题:

Java程序里,主内存,每个线程还有自己的工作内存( t1 的 t2 的工作内存不是一个东西)

t1 线程进行读取的时候,只是读取了工作内存的值

t2 线程进行修改的时候,先修改的工作内存的值,然后再把工作内存的内容同步到主内存中。

但是由于编译器优化,t1没有重新的从主内存同步数据到工作内存,读到的结果就是修改之前的结果。

主内存:main memory , 主存,也就是内存

工作内存: work memory => 工作存储区 (并非所说的内存,而是CPU上存储数据的单元,寄存器)

为啥Java这里不直接叫CPU寄存器,而搞了个“工作内存”这样的说法呢?

这里的工作内存,不一定只是CPU的寄存器,还可能包括CPU的缓存cache

CPU读取寄存器,速度比读取内存快多了,因此就会在CPU内部引入缓存 cache

寄存器存储空间小,读写速度快,贵;

中间搞了个 cache ,存储空间居中,读写速度居中,成本居中;

内存存储空间大,读写速度慢,便宜(相对于寄存器来说)

当CPU要读取一个内存数据时,可能是直接读内存,也可能是读cache,还可能是读寄存器。

引入cache之后,硬件结构就更复杂了。

工作内存(工作存储区):CPU寄存器+CPU的cache

一方面是为了表述简单,一方面也是为了避免涉及到硬件的细节和差异。Java里就使用工作内存这个词给他涵盖了

有的CPU可能没有cache,有的有;

有的CPU可能有1个cache,还可能多个;

现代CPU普遍是3级cache,L1,L2,L3…

volatile不保证原子性,原子性是靠synchronized来保证的。synchronized和volatile都能保证线程安全。

wait 和 notify

线程最大的问题就是抢占式执行,随即调度。

有一些办法可以控制线程之间的执行顺序,虽然线程在内核里的调度是随机的,但是可以通过一些api让线程主动阻塞,主动放弃CPU(为别的线程让路)

比如,t1 t2 两个线程,希望t1先干活,等把活干完了再让 t2 来干。就可以让 t2 先 wait(阻塞,主动放弃 CPU),等 t1 干的差不多了, 再notify通知t2,把t2唤醒,让 t2接着干。

那么上述场景,使用 join或sleep,行不行呢?

使用join必须要让t1彻底执行完,t2才能接着执行。如果希望t1执行50%再让t2接着执行,那么join做不到。

使用sleep,无法精确控制好休眠时间。

使用wait和notify可以更好的解决上述问题。

可以认为 wait和notify比join功能更强,涵盖了join的用途,但是wait和notify使用起来要比join麻烦

wait, notify, notifyAll 都是 Object 类的方法。所有类都默认继承Object类。所以所有类都有这3个方法。

wait进行阻塞。某个线程调用wait方法,就会进入阻塞(无论是通过哪个对象wait),此时就处在WAITING

InterruptedException
//这个异常,很多带有阻塞功能的方法都带
//这些方法都是可以被 interrupt 方法通过这个异常给唤醒的
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        object.wait();
        //wait 不带任何参数,就是一个死等,一直等待,等到有其他线程唤醒它
    }
}

image-20221213192818796

锁的状态无非就是被加锁和被解锁

wait操作:

1.先释放锁

2.进行阻塞等待

3.收到通知之后,重新尝试获取锁,并且在获取锁之后继续向下执行

对于上述代码而言,这还没加锁怎么释放锁?!

所以,wait需要搭配synchronized使用

有关wait操作这里,举个形象的例子。

有一天张三去银行ATM机取钱,发现ATM机没钱了,只能等运钞机运钱过来。ATM也有个锁,只能等进去的人解锁,下一个人才能进去用。张三后面还排着很多人也要用ATM机。此时张三只能解锁(先释放锁)出来等(进行阻塞等待)运钞机。运钞机来了,操作之后ATM能取钱了(这里张三相当于收到通知了),然后张三再次进去ATM机取钱(重新尝试获取锁)。

image-20221213193951499

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("t1: wait 之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1: wait 之后");
        });
        Thread t2 = new Thread(()->{
            System.out.println("t2: notify 之前");

            synchronized (object) {
                //notify要先获取到锁才能进行通知
                object.notify();
            }
            System.out.println("t2: notify 之后");
        });
        t1.start();
        Thread.sleep(500);
        t2.start();
    }
}

image-20221213210242332

image-20221213210253438

wait无参数,死等

wait带参数,指定了等待的最大时间

wait带参数版本和sleep有点类似,

wait是使用notify唤醒,sleep是用interrupt唤醒。

notify唤醒wait不会出现异常,而interrup唤醒sleep是出异常了

如果当前有多个线程在等待object对象,此时有一个线程object.notify(),此时是随机唤醒一个等待的线程

notifyAll和notify非常相似,多个线程wait的时候,notify随机唤醒一个,notifyAll唤醒所有线程,这些线程再一起竞争锁。

public class ThreadDemo {
    //有 3 个线程,分别只打印 A , B , C ,控制 3 个线程固定按照 ABC 的顺序来打印
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (locker1){
                locker1.notify();
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1){
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B");
            synchronized (locker2){
                locker2.notify();
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (locker2){
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("C");
        });

        t2.start();
        t3.start();
        Thread.sleep(100);
        t1.start();
        //保证t2和t3的wait都执行完了,再执行t1的notify
    }
}

多线程案例

单例模式

单例模式是设计模式的一种。

单例模式,单个实例(对象)。

在有些场景中,有的特定的类,只能创建出一个实例,不应该创建多个实例。使用了单例模式后,此时想创建多个实例都难。

单例模式,就是针对上述的需求场景进行了个更强制的保证,通过巧用Java的现有语法,达成了某个类只能被创建出一个实例这样的效果(如果不小心创建出多个实例就会编译报错)

JDBC,DataSource 这样的类,其实就非常适合于用单例模式

在 Java 里实现单例模式的方式有很多种。

下面介绍最常见的 2 种

1)饿汉模式

2)懒汉模式

饿汉模式

// 饿汉模式 的 单例模式 实现
//此处保证 Singleton 这个类只能创建出一个实例
class Singleton{
    //在此处,先把这个实例给创建出来了
    private static Singleton instance = new Singleton();
    //如果需要使用这个唯一实例,只能通过 Singleton.getInstance()方式进行获取
    public static Singleton getInstance(){
        return instance;
    }
    private Singleton(){}
    //为了避免 Singleton 这个类不小心被复制出多个来
    //把构造方法设置为 private , 这样在类外就无法通过 new 的方式创建出 Singleton 的实例
}
public class ThreadDemo{
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //Singleton s3 = new Singleton();
        System.out.println(s1 == s2);
    }
}

image-20221214162827496

Java代码里的每个类,都会在编译完成后得到.class文件

JVM运行时就会加载这个.class文件读取其中的二进制指令,并且在内存中构造出对应的类对象(形如Singleton.class)

由于类对象在一个Java 进程里,只是有唯一一份的,因此类对象内部的类属性也是唯一一份了

饿汉模式:一个饿了很久的人看到吃的就会很急切

image-20221214163656896

image-20221214191914207

private static Singleton instance = new Singleton();
//static 保证这个实例是唯一的,
//static 保证这个实例是在一定时机被创建出来
//(static属于这个实现方式中的灵魂角色)
//static 这个操作是让当前 instance 属性是类属性了,类属性是长在类对象上的,类对象又是唯一实例的(只是在类加载阶段被创建出一个实例)

运行一个 Java 程序,就需要让 Java 进程能够找到并读取对应的 .class 文件,就会读取文件内容并解析,构造成类对象…这一系列的过程操作,称为类加载。

要执行 Java 程序前提是要把类加载起来才行。

懒汉模式

class SingletonLazy{
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
            //这个实例并非是类加载的时候创建了,而是真正第一次使用的时候去创建(如果不用就不创建)
        }
        return instance;
    }
    private SingletonLazy(){}
}
public class ThreadDemo{
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

上述写的饿汉模式和懒汉模式,如果在多线程环境调用getInstance,是否是线程安全的?

image-20221214192151432image-20221214192855955

如何能让上述懒汉模式成为线程安全?

加锁

刚才线程不安全的原因是读,比较和写这 3 个操作不是原子的,这就导致 t2 读到的数据是 t1 还没来得及修改的(脏读)
image-20221214193245140
这样写就会出现新的问题,每次getInstance都需要加锁,加锁操作是有开销的

仔细分析,其实只用在new对象之前加锁就可以,new完对象之后调用getInstance就只有比较和返回这 2 个读操作了。
image-20221214193846255
此时还存在一个问题:内存可见性

如果很多线程都去getInstance , 这个时候是否有会被编译器优化的风险?(只有第一次读是读了内存,后续都是读寄存器/cache)

另外还有指令重排序的问题

 instance = new SingletonLazy();
 //这里拆分成 3 步
 //1.申请内存空间
 //2.调用构造方法,把这个内存空间初始化成一个合理的对象
 //3.把内存空间的地址赋值给 instance 引用

正常情况下是按照1,2,3的顺序执行的。

但是编译器可能会进行指令重排序(为了提高程序效率,调整代码执行顺序)

1,2,3这个顺序可能会变化1 ,3,2 (如果是单线程,这里调整顺序没有本质区别)

但在多线程环境下会出问题

假设 t1 是按照 1,3,2 的顺序执行的,当 t1 执行到 1,3 之后执行 t2 之前被切出 CPU,t2来执行。当 t1 执行完 3 之后,instance 就非空了, t2 就直接返回了 instance 引用,并且还可能会尝试使用引用中的属性。但是 t1 的 2 还没完成,t2 拿到的是一个非法的对象。

这种情况轮到 volatile 上场了

volatile 有 2 个功能:解决内存可见性 + 禁止指令重排序。

最终调整之后的代码如下:

image-20221214195455180

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

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

相关文章

关于实体类中VO、DTO、Entity的区别

关于实体类中VO、DTO、Entity的区别 1、entity 里的每一个字段,与数据库相对应, 2、vo 里的每一个字段,是和你前台 html 页面相对应, 3、dto 这是用来转换从 entity 到 vo,或者从 vo 到 entity 的中间的东西 。(DTO中拥…

leetcode: 1091. 二进制矩阵中的最短路径

1091. 二进制矩阵中的最短路径 来源:力扣(LeetCode) 链接: https://leetcode.cn/problems/shortest-path-in-binary-matrix/description/ 给你一个 n x n 的二进制矩阵 grid 中,返回矩阵中最短 畅通路径 的长度。如果不存在这样的路径,返回 -1 。 二进制矩阵中的 畅通…

刚刚接触编程,纠结应该先学Python还是Java?看完心里就有数了。

准备学习编程前,你一定会打开电脑搜索一下“先学Python还是先学Java?”扑面而来的是海量回答。 如果你问一名Java程序员,肯定会建议你先学Java,因为他们认为Python像个乳臭未干的黄毛小子。但如果你问Python程序员,可…

MacOS Ventura 13.1 (22C65) 正式版带 OC 0.8.7 and winPE 双分区原版黑苹果镜像

12 月 14 日,苹果向 Mac 电脑用户推送了 macOS 13.1 更新(内部版本号:22C65),本次更新距离上次发布隔了 34 天。本次更新引入了无边记应用,旨在让用户在空白的白板式画布上进行素描、绘画和写作&#xff0c…

[附源码]Nodejs计算机毕业设计基于Web的在线音乐网站Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置: Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术: Express框架 Node.js Vue 等等组成,B/S模式 Vscode管理前后端分…

能力评估网站

开发工具(eclipse/idea/vscode等): 数据库(sqlite/mysql/sqlserver等): 功能模块(请用文字描述,至少200字): (1)高校学生评估系统用户页面设计 前台页面主要是为用户设计,经过对用户和评估系统分析,目前主要…

足球视频位置与平面坐标的转换

依赖: C# OpenCVSharp WPF Numpy 目的:解决足球场上,人物投影到二维平面的位置坐标 图A / B/ C 一、基础概念 1.1标准球场的定义: 参考:https://zh.m.wikipedia.org/zh/%E8%B6%B3%E7%90%83%E5%A0%B4图 D 1.2 配准…

m软件接收机中同步技术以及LMS-RLS自适应均衡技术的matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 自适应均衡属于自适应信号处理的应用范畴,各种各样的自适应均衡算法如迫零(ZF)算法、最小均方(LMS)算法、递归最小二乘(RLS)算法、变换域均衡算法、Bussgang算法、高阶或循环统计量算…

使用CreateJS实现一个【拯救球员】小游戏,快@你的小伙伴一起来玩儿吧

拯救球员游戏需求👇核心玩法👇👇界面原型👇👇成品演示👇游戏开发1.游戏素材准备2.代码实现1.创建index.html页面2.首页转场动画实现3.添加分数倒计时4.卡片排序展示5.游戏结束世界杯开赛前夕,球…

拷贝构造,赋值运算符重载(六千字长文详解!)

c之类和对象详解 拷贝构造,赋值运算符重载 文章目录c之类和对象详解 拷贝构造,赋值运算符重载拷贝构造拷贝构造特征拷贝构造的注意赋值运算符重载运算符重载赋值重载赋值运算符的写法注意赋值重载的默认性赋值重载和拷贝赋值的区别在哪里?拷贝…

Ubuntu问题汇总

1.sudo ifconfig 找不到命令 ubuntu找不到ifconfig_猿 白的博客-CSDN博客_ubuntu ifconfig命令找不到 没有找到ifconfig的命令,需要进行安装,按照提示中的命令安装相关的工具包 sudo apt install net-tools 2.ip查找 3.重启服务器后,nvid…

记一次浏览器预览通过nginx且有权限控制的静态文件

我的需求是,后台生成了合同文件,用户需要进行预览,如果采用流的实现方式的话,会涉及到输入流、输出流,性能开销较大,所以采用的是直接访问文件,这里就涉及到一个问题,就是 需要设置…

Mobtech秒验SDK——一站式解决用户登录场景

据悉,北京中文万维科技有限公司旗下多款APP,和MobTech开发的秒验SDK达成合作,为其提供用户一键登录解决方案。 北京中文万维科技有限公司是一家立志以移动互联网阅读为发展起点的阅读互动娱乐高新技术企业,旗下拥有多款阅读类APP。…

互动教学场景下的视频直播线上研讨会应用(组图)

阿酷TONY / 原创 / 2022-12-14 / 长沙 / 互动教学/互动培训类场景特点: 1. 直播过程中,学员不仅是观看讲师的授课内容,还要与讲师直接进行音视频会话;当然一些非培训教学场景也常用到,比如线上学术类研讨会等等。 2…

【Pycharm教程】 详解 PyCharm Macros宏

宏提供了一种方便的方法来自动化您在编写代码时经常执行的重复过程。您可以录制、编辑和播放宏,为它们分配快捷方式并共享它们。 宏可用于在文件中组合一系列与编辑器相关的操作。 您无法记录按钮单击、导航到弹出窗口以及访问工具窗口、菜单和对话框。 可以使用没…

从3s到40ms,看看人家的性能优化技巧,确实优雅

什么是高性能系统 先理解一下什么是高性能设计,官方定义: 高可用(High Availability,HA)核心目标是保障业务的连续性,从用户视角来看,业务永远是正常稳定的对外提供服务,业界一般用几个 9 来衡量系统的可用性。通常采…

代码随想录算法训练营第五十天|123.买卖股票的最佳时机III、 188.买卖股票的最佳时机IV

123.买卖股票的最佳时机III 此题限定了买卖的次数,所以应该用几个状态来记录所对应得利润 至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。 dp数组及下标含义 一天一共就有五个状态, 0 没有操作 1 第…

java+mysql 基于ssm的驾校预约管理系统

随着现代驾校预约管理的快速发展,可以说驾校预约管理已经逐渐成为现代驾校预约管理过程中最为重要的部分之一。但是一直以来我国传统的驾校预约管理并没有建立一套完善的行之有效的驾校预约管理系统,传统的驾校预约管理已经无法适应高速发展,无论是从效率还是从效果来看都远远的…

代码随想录Day50|123.买卖股票的最佳时机III、188.买卖股票的最佳时机IV

文章目录123.买卖股票的最佳时机III188.买卖股票的最佳时机IV123.买卖股票的最佳时机III 文章讲解:代码随想录 (programmercarl.com) 题目链接:123. 买卖股票的最佳时机 III - 力扣(LeetCode) 题目: 给定一个数组&…

[附源码]Python计算机毕业设计感动校园人物投稿网站Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等…