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

🔥博客主页: 【小扳_-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() 方法必须在同步代码块或同步方法中调用,即在获取对象锁的情况下使用,以避免出现并发访问的问题。

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

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

相关文章

【计算机考研】 408难吗?学到什么程度才能考130?

如果你是92科班,我觉得130是有机会的 。如果双非或者跨考,我觉得是很难的。可以关注一下可软和软微的复试通知。可以看到数学130以上的大有人在,408能考到130的寥寥无几。 而且从今年来看,对于基础还行,只用王道的我来…

LeetCode算法——数组/字符串篇

对刷过的算法进行总结,所用解法都是最符合我个人逻辑的,以后再刷的话就看这篇帖子了 # 代码随想录——数组理论基础 首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题 数组是存放在连续内存空间上的相同类型数据的集合 …

蓝桥备赛——贪心

题干 AC Code n, w = map(int, input().split()) # n种类, w核载重 a = [] # [[weight1, value1], [weight2, value2], ...] for _ in range(n):a.append(list(map(int, input().split()))) a.sort(key=lambda x: x[1] / x[0], reverse=True)maxVal = 0for i in a:if i[0…

亮数据Bright Data,引领高效数据采集新体验

随着互联网和大数据的日益普及,我们对于高速、安全和无限畅通的网络体验追求越发迫切,随之而来的网络安全和隐私保护变得越来越重要。IP代理作为一种实用的代理工具,可以高效地帮我们实现网络数据采集,有效解决网络安全问题&#…

大数据量查询语句优化

测试单表模糊查询,符合条件的数量为: -- 查看总共有多少条数据 select count(0) from "REGISTER_HOUSE_INFO" where SEAT_NAME like %1% ;未优化:测试单表模糊查询分页,符合条件的数据为: select * from …

单词精灵,Android 记单词 app 开发

使用 Android Studio 开发了一款 记单词 app —— 《单词精灵》 关键词:单词精灵 A. 项目描述 《单词精灵》是一款专为Android平台设计的单机记单词应用。该应用旨在帮助用户系统、高效地扩展词汇量,提升英语水平。应用内置丰富的词库和记忆方法&#…

C++AVL树拓展之红黑树原理及源码模拟

前言:我们之前已经从零开始掌握AVL树http://t.csdnimg.cn/LaVCChttp://t.csdnimg.cn/LaVCC 现在我们将继续学习红黑树的原理并且实现插入等功能,学习本章的前提要求是掌握排序二叉树和AVL树,本章不再提及一些基础知识,防止本文结…

LeetCode-560. 和为 K 的子数组【数组 哈希表 前缀和】

LeetCode-560. 和为 K 的子数组【数组 哈希表 前缀和】 题目描述:解题思路一:一边算前缀和一边统计。这里用哈希表统计前缀和出现的次数,那么和为k的子数组的个数就是当前前缀和-k的个数,即preSums[presum - k]。画个图表述就是&a…

sparksql执行流程

1. SparkSQL的自动优化 我们前面的文章已经说过spark RDD定义好后,执行经过DAG sechduler划分号内存管道、逻辑任务,然后经由task scheduler来分配到具体worker来管理运行,RDD的运行会完全按照开发者的代码执行 如果开发者水平有限&#xff…

一文了解JAVA的常用API

目录 常用kpimathSystemRuntimeObjectObjectsBigIntegerBigDecima正则表达式包装类 常用kpi 学习目的: 了解类名和类的作用养成查阅api文档的习惯 math 工具类。因为是工具类,因此直接通过类名.方法名(形参)即可直接调用 abs:获取参数绝对…

Spring如何进行事务管理?什么是面向切面编程?

喜欢就点击上方关注我们吧! 本篇将带你快速了解Spring事务管理以及面向切面编程(AOP)相关知识。 一、事务 1、概述 1)事务是一组操作的集合,是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。 2&#xff…

八股 -- C#

面向对象 (三大特性) 三大特性目的是为了提供更好的代码组织、可维护性、扩展性和重用性 C#基础——面向对象 - 知乎 (zhihu.com) 封装 理解: 你不需要了解这个方法里面写了什么代码,你只需要了解这个方法能够给你返回什么数据&…

矩阵乘法优化:GEMM中如何将大矩阵切割成小矩阵

论文自然还是 Anatomy of High-Performance Matrix Multiplication。 如何拆分 一个矩阵乘法有 6 种拆分方式,其中对 row-major 效率最高的是: 第一次拆分 先做第一次拆分,取 A 的 kc 列(PanelA)和 B 的 kc 行&…

基于 7 大城市实景数据,清华大学团队开源 GPD 模型

城市,是人们安居乐业的故土,是政府开展经济建设的基石,承载着细腻的人文情怀与宏伟的国家发展脉络。长期以来,管理者一直在探寻更加高效、科学的城市治理方法,解决不同地区资源供给不平衡、交通拥挤、人口流失等问题。…

Qt项目通过.pri文件将众多文件按功能模块分类显示,开发大型项目必备

Chapter1 Qt项目通过.pri文件将众多文件按功能模块分类显示,开发大型项目必备 Chapter2 在Qt项目中添加pri文件 原文链接:在Qt项目中添加pri文件_qtpri-CSDN博客 前言 一般我们创建Qt项目工程的时候,都是直接把所有的项目,头文…

Chatopera 云服务的智能问答引擎实现原理,如何融合 #聊天机器人 技术 #Chatbot #AI #NLP

观看视频 Bilibili: https://www.bilibili.com/video/BV1pZ421q7EH/YouTube: https://www.youtube.com/watch?vx0d1_0HQa8o 内容大纲 提前在浏览器打开网址: Chatopera 云服务:https://bot.chatopera.comChatopera 入门教程:https://dwz…

微机原理-基于8086电压报警器系统仿真设计

**单片机设计介绍,微机原理-基于8086电压报警器系统仿真设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于8086的电压报警器系统仿真设计概要主要涉及到系统的整体架构设计、硬件组成、软件逻辑设计以及仿真环境…

【智能算法】黄金正弦算法(GSA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2017年,Tanyildizi等人受到正弦函数单位圆内扫描启发,提出了黄金正弦算法(Golden Sine Algorithm, GSA)。 2.算法原理 2.1算法思想 GSA来源于正弦函…

前端学习<二>CSS基础——14-CSS3属性详解:Web字体

前言 开发人员可以为自已的网页指定特殊的字体(将指定字体提前下载到站点中),无需考虑用户电脑上是否安装了此特殊字体。从此,把特殊字体处理成图片的方式便成为了过去。 支持程度比较好,甚至 IE 低版本的浏览器也能…

C语言内存函数(超详解)

乐观学习,乐观生活,才能不断前进啊!!! 我的主页:optimistic_chen 我的专栏:c语言 点击主页:optimistic_chen和专栏:c语言, 创作不易,大佬们点赞鼓…