多线程——线程安全

news2024/11/23 13:10:11

目录

·前言

一、观察线程不安全

二、线程安全概念

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

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 关键字再进行进一步的讲解,那么关于线程安全这一话题的分享到这里就结束了,我们下一篇文章再见。 

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

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

相关文章

WAFER连接器在现代电子领域的多样化应用

WAFER连接器是一种广泛应用于现代电子设备中的连接组件&#xff0c;其设计和功能使其在多种应用场景中表现出色。作为一种高效、可靠的连接解决方案&#xff0c;WAFER连接器凭借其小巧、精密的设计赢得了工程师和设计师的青睐。这篇文章将探讨WAFER连接器在不同行业和应用领域中…

力扣第1题:两数之和(图解版)

Golang版本 func twoSum(nums []int, target int) []int {m : make(map[int]int)for i : range nums {if _, ok : m[target - nums[i]]; ok {return []int{i, m[target - nums[i]]}} m[nums[i]] i}return nil }

pip install ERROR: Could not install packages due to an OSError

问题解决 pip install xxx报错&#xff1a; WARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNone)) ERROR: Could not install packages due to an OSError 使用 pip install xxx --user 安装

游离的 HEAD 如何解决

简介 问题描述&#xff1a;使用 IDEA 在提交代码时&#xff0c;禁止提交 如何解决&#xff1a;迁出分支再提交&#xff0c;最后合并到 main 或 master 上 如何解决

面向抽象和面向接口的区别

‌1.概念 01、抽象类 在 Java 中&#xff0c;通过关键字 abstract 定义的类叫做抽象类。Java 是一门面向对象的语言&#xff0c;因此所有的对象都是通过类来描述的&#xff1b;但反过来&#xff0c;并不是所有的类都是用来描述对象的&#xff0c;抽象类就是其中的一种。 以下示…

接口测试常用工具及测试方法

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 接口一般来说有两种&#xff0c;一种是程序内部的接口&#xff0c;一种是系统对外的接口。 系统对外的接口&#xff1a;比如你要从别的网站或服务器上获取资源或信…

vue基础语法的用法(API组合式风格)

一、项目文件结构 .vscode我们在那个编辑器中编辑时就会有对应的这个文件夹&#xff0c;不是固定的 进行编写代码前先把资源自带的页面删除&#xff0c;以防误导&#xff0c;可以像我一样的删除内容 vue文件结构 二、你好 vue el插值 script代码 v-text插值 script代码 三、…

A2P云短信应用场景

中国联通国际公司产品之 A2P 云短信 在当今这个全球化的商业环境中&#xff0c;企业要想在激烈的市场竞争中脱颖而出&#xff0c;不仅需要提供优质的产品和服务&#xff0c;还需要建立起与客户之间的紧密沟通桥梁。中国联通国际公司凭借其强大的国际通信能力和丰富的行业经验&…

麒麟信安参编《信息技术应用创新 移动智能终端操作系统测试规范》

9月20日&#xff0c;广州信创协会团体标准编制培训会暨参编证书颁发仪式在北京举行。会上颁发了T/GZXC 003-2024《信息技术应用创新 移动智能终端操作系统测试规范》团体标准参编证书。麒麟信安作为重点参编单位之一&#xff0c;凭借在移动智能终端操作系统测试领域的丰富实践经…

Python微震波频散相速分析

&#x1f3af;要点 在二维均匀介质均匀源中合成互相关函数以便构建波层析成像。闭环系统中微积分计算情景&#xff1a;完美弹性体震波、随机外力对模式的能量分配。开环系统中微积分计算情景&#xff1a;无数震源激发波方程、闭合曲线上的随机源、不相关平面波事件。整理地震波…

鸿蒙NEXT开发-面试题库(最新)

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…

LUCEDA IPKISS Tutorial 77:在版图一定范围内填充dummy

案例分享&#xff1a;在给定的Shape内填充dummy 所有代码如下&#xff1a; from si_fab import all as pdk from ipkiss3 import all as i3 from shapely.geometry import Polygon, MultiPolygon import numpy as np import matplotlib.pyplot as pltclass CellFilledWithCon…

【AI换脸】Rope一键整合包,实现视频多人实时换脸

随着人工智能技术的发展&#xff0c;人们越来越注重人机交互的趣味性和实用性。AI换脸技术正是在这种背景下兴起的一种创新应用。Rope换脸工具以其易用性和卓越的效果&#xff0c;成为了众多用户和专业人士青睐的对象。 Rope是什么&#xff1f; Rope是一款开源的deepfake软件&…

Redis 的安装与部署(图文)

前言 Redis 暂不支持Windows 系统&#xff0c;官网上只能下载Linux 环境的安装包。但是启用WSL2 就可以在Windows 上运行Linux 二进制文件。[要使此方法工作&#xff0c;需要运行Windows 10 2004版及更高版本或Windows 11]。本文在CentOS Linux 系统上安装最新版Redis&#xf…

健身房预约小程序开发,高效管理健身场馆!

随着社会生活的提高&#xff0c;健身成为了人们在日常生活中的必要选择&#xff0c;而健身房也随之成为了大众经常光顾的地方。健身房预约管理系统是一个便捷预约健身的平台&#xff0c;可以让大众灵活预约&#xff0c;提高健身的便捷性和服务体验。同时&#xff0c;健身场馆也…

JAVA 并发八股

线程与进程区别 进程是正在运行的程序的实例&#xff0c;进程中包含了线程&#xff0c;每个线程执行不同的任务 不同进程使用不同的内存空间&#xff0c;在当前进程下所有线程可以共享内存空间 线程更轻量&#xff0c;线程上下文切换成本一般上要比进程上下文切换低&#xff0…

vueJS中wowjs、animate、swiper的使用

原文关注公众号 本文演示利用swiper纵向全屏滚动 npm 安装 wow.js&#xff0c;安装 wow.js后animate.css会自动安装&#xff1b; npm install wowjs --save-dev npm 安装 animate.css animate.css文档&#xff1a;http://5kzx.cn/doc.html npm install animate.css --save …

Python和MATLAB及C++和Fortran胶体粒子数学材料学显微镜学微观流变学及光学计算

&#x1f3af;要点 二维成像拥挤胶体粒子检测算法粒子的局部结构和动力学分析椭圆粒子成链动态过程定量分析算法小颗粒的光散射和吸收活跃物质模拟群体行为提取粒子轨迹粘弹性&#xff0c;计算剪切模量计算悬浮液球形粒子多体流体动力学概率规划全息图跟踪和表征粒子位置、大小…

创建docker虚拟镜像,创建启动服务脚本

进入系统命令服务目录 编辑服务 [Unit] DescriptionDocker Application Container Engine Documentationhttps://docs.docker.com Afternetwork-online.target firewalld.service Wantsnetwork-online.target [Service] Typenotify ExecStart/usr/bin/dockerd ExecReload/bin/…