多线程——线程安全

devtools/2025/2/16 7:04:00/

目录

·前言

一、观察线程不安全

二、线程安全概念

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

1.分析示例代码

2.线程随机调度

3.修改共享数据

4.原子性

5.可见性

6.指令重排序

四、解决示例代码的问题

·结尾


·前言

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

一、观察线程不安全

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

java">// 线程不安全演示代码
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 和 t2t1.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 关键字,这里我们先进行运用,后面文章再进行进一步的讲解,修改之后的示例代码及运行结果如下所示:

java">// 修改后,线程安全的代码
public class ThreadDemo13 {public static int count = 0;// 创建锁对象 lockerprivate 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 和 t2t1.start();t2.start();// 等待线程 t1 和 t2 工作完成t1.join();t2.join();// 打印 count 的值System.out.println("count = " + count);}
}

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

·结尾

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


http://www.ppmy.cn/devtools/124665.html

相关文章

高级java每日一道面试题-2024年10月3日-分布式篇-分布式系统中的容错策略都有哪些?

如果有遗漏,评论区告诉我进行补充 面试官: 分布式系统中的容错策略都有哪些&#xff1f; 我回答: 在分布式系统中&#xff0c;容错策略是确保系统可靠性和高可用性的关键。这些策略旨在处理各种类型的故障&#xff0c;包括硬件故障、软件错误、网络问题等。以下是一些常见的…

【AI知识点】多项式时间(Polynomial Time)-算法的时间复杂度

AI知识点总结&#xff1a;【AI知识点】 AI论文精读、项目、思考&#xff1a;【AI修炼之路】 多项式时间&#xff08;Polynomial Time&#xff09; 是计算复杂性理论中的一个概念&#xff0c;指的是算法的运行时间可以用输入规模 n n n 的某个多项式来表达。如果一个算法的运行…

软件体系结构 实验5 (简单工厂模式、工厂模式)实现计算器

实验目的&#xff1a; 1&#xff09;能够表述软件设计的正确性、健壮性、可复用性及可维护性等设计目标2&#xff09;能够选择合适的设计模式设计具有可扩展性及可维护性的程序 实验要求&#xff1a; 调试以下代码&#xff0c;实现程序设计的健壮性、可维护性与可扩展性&…

vmware下ubuntu18.04中使用笔记本的摄像头

步骤1&#xff1a;在windows中检查相机状态 win10系统中&#xff0c;在左下的搜索栏&#xff0c;搜索“相机”&#xff0c;点击进入即可打开相机&#xff0c;并正常显示图像。 注意&#xff1a;如果相机连接到了虚拟机&#xff0c;则不能显示正常。 步骤2&#xff1a;…

transformers和bert实现微博情感分类模型提升

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色…

毕业设计项目 深度学习语义分割实现弹幕防遮(源码分享)

文章目录 0 简介1 课题背景2 技术原理和方法2.1基本原理2.2 技术选型和方法 3 实例分割4 实现效果最后 0 简介 今天学长向大家分享一个毕业设计项目 毕业设计 深度学习语义分割实现弹幕防遮(源码分享) &#x1f9ff; 项目分享:见文末! 1 课题背景 弹幕是显示在视频上的评论…

PostgreSQL:生成-唯一主键id

1. 通过时间戳和随机数拼接生成 select TO_CHAR(NOW(), YYYYMMDDHH24MISS) || LPAD(FLOOR(RANDOM() * 1000000)::TEXT, 6, 0) AS unique_id解析&#xff1a; TO_CHAR(NOW(), ‘YYYYMMDDHH24MISS’)&#xff1a;该部分将当前时间 (NOW()) 格式化为 YYYYMMDDHH24MISS 格式&#…

【作业题】

今日代码 在高速公路上行驶的机动车&#xff0c;当达到或超出本车道限速的10%则处200元罚款&#xff1b;当达到或超出50%时&#xff0c;就要吊销驾驶证。请编写程序根据车速和限速自动判断对该机动车的处理。 【输入形式】输入车速和车道限速 【输出形式】 【样例输入】120 10…