JavaEE 初阶篇-深入了解多线程安全问题(指令重排序、解决内存可见性与等待通知机制)

news2024/10/7 2:30:49

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 指令重排序概述

        1.1 指令重排序主要分为两种类型

        1.2 指令重排序所引发的问题

        2.0 内存可见性概述

        2.1 导致内存可见性问题主要涉及两个方面

        2.2 解决内存可见性问题

        2.2.1 使用 volatile 关键字

        2.2.2 使用 synchronized 关键字

        3.0 线程的等待通知机制概述

        3.1 等待 - wait()

        3.2 通知 - notity()

        3.3 通知所有 - notifyAll()


        1.0 指令重排序概述

        指令重排序是指编译器或处理器为了提高性能,在不改变程序执行结果的前提下,可以对指令序列进行重新排序的优化技术。这种优化技术可以使得计算机在执行指令时更高效地利用计算资源,提高程序的执行效率。

        1.1 指令重排序主要分为两种类型

        1)编译器重排序:编译器在生成目标代码时会对源代码中的指令进行优化和重排,以提高程序的执行效率。编译器重排序时在编译阶段完成的,目的是生成更高效率的机器代码。

        2)处理器重排序:处理器在执行指令也可以对指令进行重排序,以最大程度地利用处理器的流水线和多核等特性。目的提高指令的执行效率。

        1.2 指令重排序所引发的问题

        虽然指令重排序可以提高程序的执行效率但是在多线程编程中可能会引发内存可见性问题。由于指令重排序可能导致共享变量的读写顺序与代码中的顺序不一致,当多个线程同时访问共享变量时,可能会出现数据不一致的情况。

        2.0 内存可见性概述

        在多线程编程中,由于线程之间的执行是并发的,每个线程有自己的工作内存,共享变量存储在主内存中,线程在执行过程中会将共享变量从主内存中拷贝到自己的工作内存中进行操作,操作完成后再将结果写回主内存。这里的工作内存指的是:寄存器或者是缓存。

        2.1 导致内存可见性问题主要涉及两个方面

        1)多线程并发操作抢占式执行导致内存可见性:如果一个现车给修改了共享变量的值,但其他线程无法立即看到这个修改之后的共享变量,就会导致数据不一致的情况。

        2)指令重排序导致内存可见性:由于编译器和处理器可以对指令进行重排序优化,可能会导致共享变量的读写顺序与代码中的顺序不一致,从而影响了线程对共享变量的可见性。

代码如下:

public class demo1 {

    public static int count = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            while (count == 0){

            };
            System.out.println("线程 t1 结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输出:");
            count = scanner.nextInt();
        });

        t1.start();
        t2.start();

    }
}

        t1 在启动线程之后,只要 count == 0 这个条件满足时,就会进入循环;t2 启动线程要求输出一个值并且将该值赋值给 count 。

        预想过程:只要输出一个非 0 的值时,那么 count 不为 0 了,t1 线程中的循环就会退出,因此会输出 ”线程 t1 结束“ 这句话。最后程序结束。

运行结果:

        输出 1 之后,按理来说,count 此时应该赋值为 1 了,那么 t1 中的循环应该要结束了并且得输出一段话。但是,看到结果,即使输出了 1 之后,t1 还在循环中。

原因如下:

        由于 t1 循环中的代码块里面是没有任何代码,无需任何操作,在 CPU 中主要执行两条指令:load 将内存中的 count 加载到寄存器中;cmp 将 count 与 0 之间进行比较。

        因为 cpm 执行这条指令直接在寄存器中操作,而 load 需要将内存的数据加载到寄存器中,这个操作的速度就比 cmp 的速度慢很多很多了。所以编译器重排序在生成目标代码时对源代码中的指令进行优化重排,将 count 变量存储到寄存器或者缓存中,目的为了提高执行效率。然而,t2 线程对 count 进行重新赋值后,将重新赋值后的 count 写回到主存中,但是 t1 线程是没有看到重新赋值后的 count 变量。因为对于 t1 线程来说,count 变量已经”固定“在工作内存中,没有重新加载主存中的 count 变量,而是反复读取自己工作内存中的 count == 0 这个变量。

        总而言之,指令重排序导致了内存可见性问题。

        2.2 解决内存可见性问题

        主要有两个方法:使用 volatile 关键字、使用 synchronized 关键字。

        2.2.1 使用 volatile 关键字

        volatile 关键字可以确保被修饰的变量对所有线程可见,禁止指令重排序。

代码如下:

当给 count 加上 volatile 关键时,编译器或者处理器就不会对指令重排序了

import java.util.Scanner;

public class demo1 {

    public static volatile int count = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            while (count == 0){

            };
            System.out.println("线程 t1 结束");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输出:");
            count = scanner.nextInt();
        });

        t1.start();
        t2.start();

    }
}

运行结果:

        当输出 1 回车之后,count 就会重新赋值为 1 。从而 t1 中的循环退出,输出打印之后,整个进程就结束了。

        2.2.2 使用 synchronized 关键字

        可以确保同一时刻只有一个线程可以访问共享变量,同时保证了线程间的数据一致性。

代码如下:

import java.util.Scanner;

public class demo1 {

    public static int count = 0;
    public static void main(String[] args) {
    Object o = new Object();
        Thread t1 = new Thread(()->{
            synchronized (o){
                System.out.println("线程 t1 开始");
                while (count == 0){
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                };

                System.out.println("线程 t1 结束");
            }
        });

        Thread t2 = new Thread(()->{
                System.out.println("输出:");
                Scanner scanner = new Scanner(System.in);
                synchronized (o){
                    count = scanner.nextInt();
                    o.notify();
                }
        });

        t1.start();
        t2.start();

    }
}

运行结果:

        t1 线程在进入循环前会先获取对象 o 的锁,并在循环体中通过 o.wait() 释放锁并等待唤醒。当 t2 线程修改了 count 的值后,会再次获取对象 o 的锁并调用 o.notify() 唤醒 t1 线程,从而解除等待状态,保证了内存可见性和线程间的通信。

        

        3.0 线程的等待通知机制概述

        线程的等待通知机制是多线程编程中常用的一种同步机制,用于实现线程间的协作和通信。

        3.1 等待 - wait()

        线程调用对象的 wait() 方法时,会释放对象的锁并且同时进入等待状态,直到其他线程调用相同对象的 notify() 或者 notifyAll() 方法来唤醒它。在等待的过程中,线程会一直处于阻塞状态。 

        3.2 通知 - notity()

        线程调用对象的 notify() 方法时,会唤醒等待在该对象上的一个线程,若有多个等待唤醒的线程时,具体唤醒的线程是不确定的,使其从等待状态转为就绪状态,被唤醒的线程会尝试重新获取对象的锁,并继续执行。

        3.3 通知所有 - notifyAll()

        线程调用对象的 notifyAll() 方法时,会唤醒所有等待在该对象上的线程,使它们从等待状态转为就绪状态。被唤醒的线程会竞争对象的锁,只有一个线程能够获取锁并继续执行,其他线程会再次进入等待状态。

举个例子:

public class demo2 {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(()->{
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("正在执行 t1 线程");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("正在执行 t2 线程");
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (lock){
                lock.notify();
                lock.notify();
                System.out.println("正在执行 t3 线程");
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t3.start();
    }
}

        t1 ,t2 线程都在阻塞状态,等待 t3 线程通知,但是 t3 线程还没释放锁,所以 t1 ,t2 线程继续阻塞状态。直到 t3 线程释放锁之后,t1,t2 线程就可以竞争获取锁,假设 t1 获取锁之后,执行完代码,释放锁,t1 线程结束。再到 t2 线程获取锁,执行完代码释放锁,t2 线程也结束。因此线程的先后顺序:t3 线程一定是最早结束的,接着到 t1 或者 t2 线程随机其中的一个线程。

运行结果:

补充:

        等待通知机制通常需要搭配 synchronized 关键字来确保线程安全。在Java中, wait()、notiyf() 和 notiyfAll() 方法必须在同步代码块或同步方法中调用,即在获取对象锁的情况下使用,以避免出现并发访问的问题。

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

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

相关文章

C语言----预处理(详解)

好了书接上回。我在讲编译与链接的时候写过宏和条件建议。我说会在下一篇博客中讲解,那么来了。今天我们来详细的讲讲预处理。宏与条件编译也在其中,那么我们现在就来好好会会这个预处理吧。 预定义符号 关于预定义符号,我暂时只知道几个。并…

安装kubesphere的 devops 插件报错

安装kubesphere的 devops 插件报错: ks-minio 报错: TASK [common : Kubesphere | Check minio] *************************************** changed: [localhost]TASK [common : Kubesphere | Deploy minio] ************************************** fa…

期货开户分析的三个基本要素

我常用的交易系统为概率分析系统,具备的三个基本要素与三个基本原则: 1、行情研判规则。这是最基础的,你必须首先用交易系统对行情有一个正确的判断。 2、进出场规则。在进出场规则当中,我个人更看重出场规则。因为实际上&#…

腾讯云容器与Serverless的融合:探索《2023技术实践精选集》中的创新实践

腾讯云容器与Serverless的融合:探索《2023技术实践精选集》中的创新实践 文章目录 腾讯云容器与Serverless的融合:探索《2023技术实践精选集》中的创新实践引言《2023腾讯云容器和函数计算技术实践精选集》整体评价特色亮点分析Serverless与Kubernetes的…

什么是智慧公厕?智慧城市下的智慧公厕有什么功能和特点?

随着科技的不断进步和城市化的加快发展,智慧城市已经成为我们生活中的一部分。而在智慧城市的建设中,智慧公厕作为城市基础设施的重要组成部分发挥着重要的作用。那么什么是智慧公厕?智慧公厕是针对公共厕所的日常使用、运行、管理、运营等过…

数据科学薪酬分析项目

注意:本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 ([www.aideeplearning.cn]) 项目简介 《数据科学薪酬分析》是一个全面的分析项目,旨在探索和解释数据科学领域的薪酬趋势。通过分析607份不同工作年份、经验水平…

跑通飞浆平台的MTMCT 跨镜跟踪示例

想跑通飞浆平台的MTMCT跨镜跟踪示例,真的是难上加难啊! 改了几处代码,可以顺利跑通了,特此记录: 第一处:不要拉主线的代码,改成 !git clone https://gitee.com/paddlepaddle/PaddleDetection…

C++教学——从入门到精通 7.if,else语句

有一个商人在街头衣服,每次卖完都不知道自己是赚了还是亏了,他想请你帮他算一算他一天赚了多少还是亏了多少 首先我们知道商人每天卖5件衣服,利润售价-成本,那么我们该编一个怎么样的程序呢? 我们来学一个新的语句—…

四氟进样器耐腐蚀耐高温可灵活加工PTFE材质含氟塑料注射器

四氟注射器用于抽取或者注入气体或者液体,四氟注射器由前端带有小孔的针筒以及与之匹配的活塞芯杆组成,用来将少量的液体或其注入到其它方法无法接近的区域或者从那些地方抽出,在芯杆拔出的时候液体或者气体从针筒前端小孔吸入,在…

MyBatis 参数重复打印的bug

现象 最近有个需求,需要在mybatis对数据库进行写入操作的时候,根据条件对对象中的某个值进行置空,然后再进行写入,这样数据库中的值就会为空了。 根据网上查看的资料,选择在 StatementHandler 类执行 update 的时候进…

虚拟内存到物理地址的映射,是CPU做的,还是操作系统做的?

虚拟地址到物理地址的转换,是CPU实现得,具体来说,就是CPU的内存管理单元 (Memory Management Unit, MMU)实现的。 为了加速地址翻译的 过程,现代CPU都引入了转址旁路缓存(Translation Loopasid…

HarmonyOS 应用开发之I/O密集型任务开发指导 (TaskPool)

使用异步并发可以解决单次I/O任务阻塞的问题,但是如果遇到I/O密集型任务,同样会阻塞线程中其它任务的执行,这时需要使用多线程并发能力来进行解决。 I/O密集型任务的性能重点通常不在于CPU的处理能力,而在于I/O操作的速度和效率。…

TDK超高压陶瓷电容的国产替代---赫威斯电容HVC Capacitor

螺栓型高压陶瓷电容,英文称为Doorknob Capacitor(美式门拧手式电容)或者High Voltage Screw Terminal Ceramic Capacitor(高压螺栓端子陶瓷电容), 著名的日本TDK公司(东电化电子)称其为“超高压陶瓷电容器”(Ultra High Voltage Ceramic capacitors)。 自2018年秋日本电子业巨头…

分治——归并排序算法

例题一 解法(归并排序): 算法思路: 归并排序的流程充分的体现了「分⽽治之」的思想,⼤体过程分为两步: ◦ 分:将数组⼀分为⼆为两部分,⼀直分解到数组的⻓度为 1 ,使…

STM32——超声测距HC_SR04记录

一、HC_SR04简述 HC-SR04超声波测距模块可提供 2cm-400cm的非接触式距离感测功能,测距精度可达高到 3mm;模块包括超声波发射器、接收器与控制电路。 基本工作原理: (1)采用IO 口TRIG 触发测距,给最少10us 的高电平信呈。 (2)模块…

差点引爆全球的核弹,深度分析XZ-Utils供应链后门投毒事件

处心积虑的投毒者蛰伏三年多,精心选择对象,通过复杂的攻击手法、专业的技战术,一步步支起一张大网,企图掌控全球主流linux发行版,一旦成功他将可以随意侵入全球绝大多数的服务器,这将是足以引爆全球的核弹危…

java高级面试题整理 - 2024

Java 创建对象有几种方式 在Java中,有以下几种常见的方式来创建对象: 使用new关键字:这是最常见的创建对象的方式。通过调用类的构造函数,使用new关键字可以在内存中分配一个新的对象。使用反射:Java的反射机制允许在…

【漏洞复现】通天星CMSV6弱口令漏洞

免责声明:文章来源互联网收集整理,请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关。该…

Anaconda换源和常用命令

设置Anaconda国内镜像加速下载 使用conda install python包非常便捷,但由于官方服务器位于国外,下载速度较慢。为了提升下载速度,国内清华大学提供了Anaconda的仓库镜像。 要将Anaconda设置为使用国内镜像,特别是清华镜像源&…

前后端数据交互

前后端数据交互 网页上所有的数据都是来源于后端,比如淘宝或者京东的秒杀,用户的登陆或者注册,这些都需要借助于后端来存储数据。我们前端需要做的就是把数据发送给后端,后端发送给我们的数据我们要拿到把它显示到页面上&#xff…