多线程——线程安全

目录

·前言

一、观察线程不安全

二、线程安全概念

三、产生线程安全问题的原因

1.分析示例代码

2.线程随机调度

3.修改共享数据

4.原子性

5.可见性

6.指令重排序

四、解决示例代码的问题

·结尾


·前言

        我们学习多线程编程的目的是为了能够实现“并发编程”,从而来提高我们代码的执行效率,在学习使用多线程时,一定避免不了“线程安全”这样的话题,这可以称的上我们多线程编程中最重要的部分,因为他会关系到我们所写的代码是否能够正确的运行,同时,线程安全也是学习多线程编程中最困难的部分,本篇文章将会对“线程安全”这一话题进行讲解。

一、观察线程不安全

        这里我通过一个代码示例来展现一下线程不安全是什么样的,下面代码示例做的主要工作是使用两个线程,分别对变量 count 进行自增操作,从而快速达到自增 10w 次的效果(一个线程对变量 count 进行 5w 次的自增),代码及运行结果如下:

// 线程不安全演示代码
public class ThreadDemo13 {
    public static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        // 启动线程 t1 和 t2
        t1.start();
        t2.start();
        // 等待线程 t1 和 t2 工作完成
        t1.join();
        t2.join();
        // 打印 count 的值
        System.out.println("count = " + count);
    }
}

        我们代码想让变量 count  自增 10w 次,最终得到的结果应该是 count = 100000,但是由上面的四次执行结果可以看出,这四次的结果都不一样,并且都没有得到正确的结果,所以上面这个循环自增的代码就是存在线程安全问题的代码。

二、线程安全概念

        观察完线程不安全的示例后,我来介绍一下线程安全,我们可以认为,某个代码,无论是在单个线程下执行,还是多个线程下执行,都不会产生 bug ,这个情况就可以成为“线程安全”,但是如果这个代码,在单线程下运行正确,多线程下可能产生 bug ,这个情况就称为“线程不安全”,也就是“存在线程安全”问题。

三、产生线程安全问题的原因

1.分析示例代码

        在介绍产生线程安全问题的原因之前,先解释一下上述示例代码为什么会产生线程安全问题,代码中出现问题的就是 count++ 这一操作,在我们编写代码时,count++ 看起来就只是一句话,但是这个 count++ 操作其实是由三条 CPU 指令构成的:

  1. load:从内存中读取数据到 CPU 的寄存器中;
  2. add :把寄存器中的值进行 + 1操作;
  3. save:把寄存器的值写回到内存中。

        下面我将上面示例代码中两个线程在进行 count++ 操作时可能出现的部分情况画出来,如下图所示:​​​​​​​​​​​​​​         上面我列出来了四种情况,每种情况,都是两个线程在调度中可能产生的执行顺序,但其实,这里的情况可以有无数种,这是由于线程的随机调度所产生的,下面就针对这四种情况来模拟演示一下他们各自在 CPU 上的执行指令的过程,在 CPU 上执行指令需要经过读指令,解析指令,执行指令这几个步骤,下面的图中省略读指令与解析指令的过程,就单看执行指令的过程,这里我们假设线程 t1 与 t2 分别在 CPU 的两个核心上并发执行,如下图所示:

        由上图我们可以观察到,只有情况1与情况2我们得到了正确的两次自增结果,情况3与情况4虽然两个线程都执行了 count++ 的操作,但是由于线程的随机调度,导致他们执行的结果好像只是进行了一次 count++ 操作,这就导致我们上面示例代码运行时没有得到正确的结果。

        经过这几个情况的过程分析,我们可以发现关于这个示例代码中,最关键的问题就在于,我们需要确保第一个线程在执行完 save 指令后,第二个线程再执行 load 指令,这时候第二个线程所加载到的 count 的值才是第一个线程所进行完 count++ 后的结果,否则第二个线程加载到的 count 的值,就是第一个线程执行 count++ 前的结果了,这时候虽然两个线程都执行了 count++ 操作,但其实就只执行了一次。

        这里对示例代码出现线程安全问题的原因做一个总结:

  1. 根本原因:由于操作系统上的线程是“抢占式执行”“随机调度”,导致线程之间的执行顺序产生了诸多变数;
  2. 代码结构:在示例代码中,涉及到多个线程修改同一个变量;
  3. 直接原因:上述多线程执行 count++ 操作不属于“原子性”操作,这就导致在执行 count++ 中,多个 CPU 指令在执行到一半的时候被其他线程调度走,从而给其他线程“可乘之机”。 

2.线程随机调度

        线程的随机调度可以说是产生线程安全问题的“罪魁祸首”,正是因为线程的随机调度,才会给我们在进行多线程编程引入诸多的变数,随机调度使我们的程序在多线程环境下执行顺序存在随机性,我们需要让我们的代码保证在任意执行顺序下都能正常工作才能保证线程安全。

3.修改共享数据

        在我们上面的代码示例中,两个线程都涉及到对同一个变量 count 进行自增操作,这就是修改了共享的数据,这时由于线程的随机调度就可能产生问题。

4.原子性

        一段代码具有原子性,就可以认为在执行这段代码时,要么这段代码都执行完,要么这段代码就都不执行,上述 count++ 操作产生问题就是因为这个操作不具有原子性,所以在线程的随机调度下,产生了问题。

5.可见性

        可见性指,一个线程对共享变量值的修改,能够及时被其他线程看见。

6.指令重排序

        假设目前我们执行的一段代码顺序是这样的:

  1. 去宿舍楼下取外卖;
  2. 回宿舍写作业;
  3. 去宿舍楼下卖水。

        如果上述逻辑是在单线程的情况下,我们的 JVM 会对上述流程进行一个优化,比如按 1->3->2 的顺序执行也是没有问题的,并且可以少下一次楼,这就叫做指令重排序,我们编译器在对于指令重排序的前提是“保持原有的逻辑不发生变化”,这一点在单线程环境下比较容易判断,但是在我们多线程环境下就没那么容易判断了,所以多线程中 JVM 对我们的代码进行指令重排序时就可能出现优化后的逻辑与之前不等价的情况。

四、解决示例代码的问题

        知道了代码中出现的问题,就可以“对症下药”了,根本原因我们无法做出任何改变,因为这是系统内部已经实现的“抢占式执行”“随机调度”,我们干预不了,针对原因2,代码结构,这个有时候可以进行调整,有时候也调整不了,需要看情况,我们这里针对直接原因,count++ 不是原子性来进行入手解决。

        虽然 count++ 看起来生成的三个指令我们无法干预,但其实我们还是有办法的,我们可以通过特殊的手段,把这三个指令打包到一起,成为一个“整体”,这就涉及到“加锁”的操作了,在 Java 中,加锁的方式有好几种,但是最主要使用方式还是用 synchronized 关键字,这里我们先进行运用,后面文章再进行进一步的讲解,修改之后的示例代码及运行结果如下所示:

// 修改后,线程安全的代码
public class ThreadDemo13 {
    public static int count = 0;
    // 创建锁对象 locker
    private static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });
        // 启动线程 t1 和 t2
        t1.start();
        t2.start();
        // 等待线程 t1 和 t2 工作完成
        t1.join();
        t2.join();
        // 打印 count 的值
        System.out.println("count = " + count);
    }
}

        此时,修改之后代码执行的结果就是正确的结果了。

·结尾

        本篇文章到此也就要结束了,文章主要对于线程安全进行了展开介绍,在文章末尾,我们提到解决线程安全问题的一种方式使用 synchronized 关键字,这也是我们学习多线程编程中的一个重点,在下一篇文章里,我会对 synchronized 关键字再进行进一步的讲解,那么关于线程安全这一话题的分享到这里就结束了,我们下一篇文章再见。 

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

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

相关文章

LSTM的变体

一、GRU 1、什么是GRU 门控循环单元&#xff08;GRU&#xff09;是一种循环神经网络&#xff08;RNN&#xff09;的变体&#xff0c;它通过引入门控机制来控制信息的流动&#xff0c;从而有效地解决了传统RNN中的梯度消失问题。GRU由Cho等人在2014年提出&#xff0c;它简化了…

C语言 | Leetcode C语言题解之第466题统计重复个数

题目&#xff1a; 题解&#xff1a; #include <stdlib.h> #include <stdio.h> #include <stdbool.h> #include <string.h> #include <math.h> #include <limits.h>#define MMAX(a, b) ((a) > (b)? (a) : (b)) #define MMIN(a,…

【项目】五子棋对战测试报告

目录 一、项目背景 二、项目功能 三、测试计划 1、功能测试&#xff1a; &#xff08;1&#xff09;测试用例&#xff1a; &#xff08;2&#xff09;实际执行测试的部分操作/截图 2、自动化测试 3、性能测试 一、项目背景 1、五子棋对战游戏 采用了前后端分离的方法来…

GO网络编程(七):海量用户通信系统5:分层架构

P323开始&#xff08;尚硅谷GO教程&#xff09;老韩又改目录结构了&#xff0c;没办法&#xff0c;和之前一样&#xff0c;先说下目录结构&#xff0c;再给代码&#xff0c;部分代码在之前讲过&#xff0c;还有知识的话由于本人近期很忙&#xff0c;所以这些就不多赘述了&#…

web自动化测试基础(从配置环境到自动化实现登录测试用例的执行,vscode如何导入自己的python包)

接下来的一段时间里我会和大家分享自动化测试相关的一些知识希望大家可以多多支持&#xff0c;一起进步。 一、环境的配置 前提安装好了python解释器并配好了环境&#xff0c;并安装好了VScode 下载的浏览器和浏览器驱动需要一样的版本号(只看大版本)。 1、安装浏览器 Chro…

vue-live2d看板娘集成方案设计使用教程

文章目录 前言v1.1.x版本&#xff1a;vue集成看板娘&#xff08;暂不使用&#xff0c;在v1.2.x已替换&#xff09;集成看板娘实现看板娘拖拽效果方案资源备份存储 当前最新调研&#xff1a;2024.10.2开源方案1&#xff1a;OhMyLive2D&#xff08;推荐&#xff09;开源方案2&…

SpringMVC2~~~

目录 数据格式化 基本数据类型可以和字符串自动转换 特殊数据类型和字符串间的转换 验证及国际化 自定义验证错误信息 细节 数据类型转换校验核心类DataBinder 工作机制 取消某个属性的绑定 中文乱码处理 处理json和HttpMessageConverter 处理Json-ResponseBody 处理…

go开发环境设置-安装与交叉编译(二)

1. 引言 Go语言&#xff0c;又称Golang&#xff0c;是Google开发的一门编程语言&#xff0c;以其高效、简洁和并发编程的优势受到广泛欢迎。作为一门静态类型、编译型语言&#xff0c;Go在构建网络服务器、微服务和命令行工具方面表现突出。 在开发过程中&#xff0c;开发者常…

吸毛效果好的宠物空气净化器分享,希喂、霍尼韦尔、米家实测

说起宠物空气净化器&#xff0c;几年前我可能会一脸鄙夷&#xff1a;为啥要花这种智商税冤枉钱&#xff1f; 直到之前养了一只猫&#xff0c;被家中乱飞的浮毛和滂臭的异味搞到头晕&#xff0c;于是作为i一个养宠的家电测评博主&#xff0c;索性对宠物空气净化器这玩意做了超级…

前端继承:原理、实现方式与应用场景

目录 一、定义 二、语法和实现方式 1.原型链继承 2.构造函数继承 3.组合继承 4.ES6类继承 三、使用方式 四、优点 五、缺点 六、适用场景 一、定义 前端继承是指在面向对象编程中&#xff0c;一个对象可以继承另一个对象的属性和方法。在前端领域&#xff0c;通常是指…

OpenCV高级图形用户界面(1)创建滑动条函数createTrackbar()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 创建一个滑动条并将其附加到指定的窗口。 该函数 createTrackbar 创建一个具有指定名称和范围的滑动条&#xff08;滑块或范围控制&#xff09;…

C语言之扫雷小游戏(完整代码版)

说起扫雷游戏&#xff0c;这应该是很多人童年的回忆吧&#xff0c;中小学电脑课最常玩的必有扫雷游戏&#xff0c;那么大家知道它是如何开发出来的吗&#xff0c;扫雷游戏背后的原理是什么呢&#xff1f;今天就让我们一探究竟&#xff01; 扫雷游戏介绍 如下图&#xff0c;简…

使用3080ti配置安装blip2

使用3080ti运行blip2的案例 本机环境&#xff08;大家主要看GPU&#xff0c;ubuntu版本和cuda版本即可&#xff09;&#xff1a;安装流程我最后安装的所有包的信息&#xff08;python 3.9 &#xff09;以供参考&#xff08;environment.yml&#xff09;&#xff1a; 本机环境&a…

【python实操】python小程序之计算对象个数、游戏更新分数

引言 python小程序之计算对象个数、游戏更新分数 文章目录 引言一、计算对象个数1.1 题目1.2 代码1.3 代码解释1.3.1 代码结构1.3.2 模块解释1.3.3 解释输出 二、游戏更新分数2.1 题目2.2 代码2.3 代码解释2.3.1 定义 Game 类2.3.2 创建 Game 实例并调用方法 三、思考3.1 计算对…

安卓13禁止锁屏 关闭锁屏 android13禁止锁屏 关闭锁屏

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.彩蛋1.前言 设置 =》安全 =》屏幕锁定 =》 无。 我们通过修改系统屏幕锁定配置,来达到设置屏幕不锁屏的配置。像网上好多文章都只写了在哪里改,改什么东西,但是实际上并未写明为什么要改那…

RabbitMQ 高级特性——死信队列

文章目录 前言死信队列什么是死信常见面试题死信队列的概念&#xff1a;死信的来源&#xff08;造成死信的原因有哪些&#xff09;死信队列的应用场景 前言 前面我们学习了为消息和队列设置 TTL 过期时间&#xff0c;这样可以保证消息的积压&#xff0c;那么对于这些过期了的消…

数据结构-4.6.KMP算法(旧版下)-朴素模式匹配算法的优化

一.绪论&#xff1a; 当主串字符和模式串字符不匹配时会执行jnext[j]来改变模式串的指针&#xff0c;但主串的指针不变。 二.求模式串的next数组&#xff1a; 1.例一&#xff1a; 如模式串abcabd&#xff0c;当第六个字符d匹配失败时&#xff0c;此时主串中前五个字符abcab都…

连锁店线下线上一体化收银系统源码

近年来线下线上一体化已经成为很多连锁门店追求的方向。其中&#xff0c;线下门店能够赋予品牌发展的价值依然不可小觑。在线下门店中&#xff0c;收银系统可以说是运营管理的关键工具&#xff0c;好的收银系统能够为品牌门店赋能。对于连锁品牌而言&#xff0c;对收银系统的要…

软媒市场新蓝海:软文媒体自助发布与自助发稿的崛起

在信息时代的浪潮中,软媒市场以其独特的魅力和无限的潜力,成为了企业营销的新宠。随着互联网的飞速发展,软文媒体自助发布平台应运而生,为企业提供了更加高效、便捷的营销方式。而自助发稿功能的加入,更是让软媒市场的蓝海变得更加广阔。 软媒市场的独特价值 软媒市场之所以能…

Android Studio Koala中Kotlin引入序列化Parcelable

找了一堆资料没有新构建序列化的方法&#xff0c;踩坑经历如下&#xff1a; 前提是使用Kotlin创建的项目 之前的build.gradle版本写法如下&#xff1a; 但是新版Android Studio Koala使用序列化模式发生了改变&#xff0c;如下&#xff1a; 测试成功如下&#xff1a; 发出来…