多线程--深入探究多线程的重点,难点以及常考点线程安全问题

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱
ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客
本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如需转载还请通知˶⍤⃝˶​
个人主页:xiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客

系列专栏:xiaoxie的JAVAEE学习系列专栏——CSDN博客●'ᴗ'σσணღ
我的目标:"团团等我💪( ◡̀_◡́ ҂)" 

( ⸝⸝⸝›ᴥ‹⸝⸝⸝ )欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​+关注(互三必回)!

目录

一.线程安全问题

1.为什么会有线程安全问题

2.一个经典的线程安全的例子

1.Java代码

2.输出结果

 3.说明

4.原因说明

5.画图说明

6.解决方法

二.锁

1.什么是锁

2.如何加锁

1.synchronized关键字

3.为什么锁可以解决线程安全问题

三.内存可见性问题

1.什么是内存可见性

2.举一个例子

 3.造成内存可见性问题主要原因

4.如何解决内存可见性问题

5.具体用法

6.一道关于volatile关键字的面试问题

一.线程安全问题

1.为什么会有线程安全问题

线程安全问题在多线程编程中出现的根本原因是由于并发执行所带来的不确定性以及现代计算机系统在执行多线程任务时的内在机制。以下是线程安全问题产生的主要原因:

  1. 抢占式执行

    • 在多线程环境下,操作系统采用抢占式调度策略,这意味着线程可以在任何时候被停止执行或恢复执行,而不保证按照特定顺序完成。因此,线程间的执行顺序具有不确定性,可能导致数据竞争。
  2. 共享状态

    • 当多个线程访问并修改同一块共享数据时,如果没有适当的同步控制,就可能出现数据不一致或者竞态条件。例如,两个线程同时读取一个变量然后更新它,最终结果可能并不是每个线程单独操作所期望的结果。
  3. 非原子操作

    • 许多操作在硬件层面并不是原子的,即它们可以被中断并在稍后继续执行。如果一个非原子操作在执行过程中被另一个线程打断,可能导致数据损坏。
  4. 内存可见性

    • CPU和编译器为了性能优化,可能缓存数据到本地寄存器或缓存行中,而不是立即写回到主内存。这会导致不同线程看到的数据可能是过期的,即线程间对共享变量的修改彼此不可见。
  5. 指令重排序

    • 编译器或处理器为了优化性能,可能会重新安排指令执行的顺序,只要不影响单线程环境下的程序逻辑。但在多线程环境下,这种重排序可能导致依赖于特定执行顺序的代码出错。
  6. 死锁与资源争抢

    • 当多个线程相互等待对方释放资源时,可能会陷入永久阻塞的状态,即死锁。另外,如果资源分配不当,可能会导致某些线程长期得不到所需的资源而无法执行,形成饥饿现象。

综上所述,线程安全问题主要是由于并发执行中的数据访问冲突、操作的原子性和内存模型的复杂性等因素引起的。而出现这些问题,我们的代码就会有BUG(即不满足我们的业务要求就是BUG)

2.一个经典的线程安全的例子

1.Java代码

public class Demo {public static int count;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.start();t2.start();//保证线程1和线程2执行完毕t1.join();t2.join();System.out.println("count = " + count);}
}

2.输出结果

1.第一次

2.第二次

​ 

 3.说明

我们可以看到上面的代码,就是很简单的,先定义好一个静态的count变量,在线程1和线程2中各循环50000次,每次循环都加加,根据静态变量的特性,我们最后输出的count变量应该为100000,并且每次输出都一样才对,可是最后结果却每次的输出结果都不同,这就代表我们的代码出现了BUG.

4.原因说明

上述代码我们如果是在单线程的环境下,由于不存在并发访问共享资源的情况,当然是没问题的,而多线程,我们都知道,多线程在操作系统中,是抢占式调度,线程间的执行顺序具有不确定性,这便是我们每次执行代码输出结果都不同的主要原因,并且可能导致两个线程在未进行任何同步控制的情况下同时访问和修改 count 变量,进而产生竞态条件使得最终输出的 count 值低于预期的100000.

为了更通俗一点说明,博主通过画图的方式来帮助理解

5.画图说明

操作系统执行一个线程的过程,主要的就是CPU指令,我们就通过这底层的CPU指令来分析多线程问题

按照上图的CPU指令执行顺序,可以分为以下几步

1.t1从内存中的count,读取到寄存器中

 2.t2从内存中的count,读取到寄存器中

 3.t1寄存器中的count进行加加操作

  4.t2寄存器中的count进行加加操作

5.把t1寄存器的值写入到内存中

6.把t2寄存器的值写入到内存中

通过上述过程我们可以发现,正是因为,线程是随机调度的原因导致我们的CPU指令执行的顺序也是随机,从而导致了原本应该加1的操作被重复计数,最终结果小于预期的100000存在线程安全问题.

注意:这只是在循环过程中CPU执行顺序的其中一种.所以我们每次启动代码得到的count值都不相同.

6.解决方法

在上文中我们也提到了为什么会发生线程安全问题

1.线程在操作系统执行时是随机调度的,抢占式执行的

2.多个线程同时访问一个共享数据

3.线程对数据的修改是非原子操作的

4.内存可见性问题

5.指令重排序

我们如何解决线程安全问题呢,当然就是从这些原因入手,首先第一个原因,虽然它是造成线程安全问题的主要原因,但这是操作系统这个层级的问题,我们也是"有心却无力",其次第二个原因是代码问题,我们当然是可以修改代码让多个线程不要同时访问一个共享数据,但大多时候,业务代码都是比较复杂的,你修改了一下代码,属于牵一发而动全身,反而得不偿失.第四,第五的原因,这里还不涉及到(下文在提及),而第三个原因,是解决线程安全问题的最朴实的方法,使用锁来将非原子的操作,封装成一个原子操作即使用锁.

二.锁

1.什么是锁

在多线程环境中,当多个线程同时尝试访问和修改同一份数据时,如果没有妥善的协调机制,将会引发竞态条件(Race Condition)、数据不一致等问题。锁就是用来解决这类问题的一种工具。在最简单的形式下,锁是一种二元状态标志,表示资源是否可用。当一个线程获得了锁,它可以访问受保护的资源;其他尝试获取同一把锁的线程则会被阻塞(挂起),直到该锁被释放为止。这样,锁就确保了在任何给定时间内,只有一个线程能够访问临界区(Critical Section)内的资源。

总的来说:锁主要的方式就是:1.加锁 2.解锁,它的主要特性就是有"互斥性"即,一个线程加锁了之后,直到该锁被持有线程释放(解锁)其他线程不可以尝试加锁了,另一个或者是多个就会,阻塞等待.正是有这个特性,使得锁可以用来解决线程安全问题.

2.如何加锁

在Java中我们主要是使用关键字"synchronized"来进行加锁

有以下几种加锁的例子

1.在非原子操作中加锁(内置锁)

public class Demo {public static int count;public static void main(String[] args) throws InterruptedException {Object locker = new Object();//创建一个锁对象,无论是什么类型都可以为锁对象,//这里的锁对象只是做一个标识的作用Thread t1 = new Thread(()->{synchronized (locker) {for (int i = 0; i < 50000; i++) {count++;}}});Thread t2 = new Thread(()->{synchronized (locker) {for (int i = 0; i < 50000; i++) {count++;}}});t1.start();t2.start();//保证线程1和线程2执行完毕t1.join();t2.join();System.out.println("count = " + count);}
}

注意:这里的Object locker = new Object(); 创建一个锁对象,无论是什么类型都可以为锁对象,
这里的锁对象只是做一个标识的作用.最重要的是多个线程是否对同一个对象加锁这才是最重要的,如果是不同对象,那么锁就没有作用了.

2.使用synchronized修饰的方法

class Count {public static int count;Object locker = new Object();public static synchronized void add(){//使用synochronized修饰的方法count++;}public static int getCount() {return count;}
}
public class Demo10 {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()-> {for (int i = 0; i < 50000; i++) {Count.add();}});Thread t2 = new Thread(()-> {for (int i = 0; i < 50000; i++) {Count.add();}});t1.start();t2.start();//保证线程1和线程2执行完毕t1.join();t2.join();System.out.println("count = " + Count.getCount());}
}

当然还有其他的方法使用synchronized来加锁这里就不过的赘述了

1.synchronized关键字

这里再解释一下synchronized关键字是Java语言中用于处理多线程同步的关键字

1.synchronize在使用于代码块时后面跟的()括号里面的参数为锁对象

2.synchronized(){}进入 { 时就是为()里的锁对象上锁,出入}才代表着解锁

3.为什么锁可以解决线程安全问题

就像上图表示的一样,只有当 t2 进行 lock操作时,锁已经被 t1 占有了,用于锁具有互斥性此时  t2 就只能阻塞等待,直到t1解锁后,t2才能执行 load add save CPU指令操作.这样就相当于count++这个操作是串行化执行的.这里需要注意的是博主说的这里是串行化执行的,仅仅是count++这个操作,两个线程还是并发执行的.

这里只是对锁的初步介绍,后续博主会更新更多关于锁的问题,大家感兴趣可以关注一下.

三.内存可见性问题

1.什么是内存可见性

内存可见性在多线程编程中是一个至关重要的概念,它涉及到当一个线程修改了共享变量的值后,其他线程能否及时看到这个修改后的值。在一个多核或多处理器系统中,每个线程可能有自己的工作内存,而主内存是所有线程共享的。

  • 问题背景: 当线程A在自己的工作内存中修改了共享变量的值,这个更改可能不会立刻同步回主内存,同时线程B也无法感知到线程A所做的更改,除非线程B也有某种机制来刷新或重新获取主内存中该变量的最新值。这就是所谓的内存不可见性问题。

  • 后果: 内存不可见性可能导致程序的行为变得不可预测,特别是在依赖于共享变量状态进行决策的并发代码中。如果不采取措施保证内存可见性,程序可能因为不同线程看到的变量值不同而产生各种错误,例如数据不一致、程序逻辑混乱等。

2.举一个例子

public class Demo11 {public static int count;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (count == 0) {// do nothing}System.out.println("线程t1发现count值已改变,不再为0");});Thread t2 = new Thread(() -> {System.out.println("请输入count的值:");Scanner scan = new Scanner(System.in);count = scan.nextInt(); // 修改count的值scan.nextLine(); // 清除换行符});t1.start();t2.start();// 为了让t1有机会看到t2对count的修改,可以让主线程等待t2结束t2.join();System.out.println("主线程已确认t2线程结束,现在t1应能看到count的新值");}
}

 预期的效果:用户在输入一个非0的整数t1就会退出循环并输出("线程t1发现count值已改变,不再为0") 我们可以看一下结果

我们可以发现t1并没有退出循环,这是为什么呢? 在多线程环境下,当一个线程修改了共享变量的值,其他线程并不一定能立即看到这个修改。这就是内存可见性问题

 3.造成内存可见性问题主要原因

造成内存可见性问题,最主要的原因就是,jvm对操作进行了优化,使得t2修改了count值,但t1,无法察觉到(没有读取到),就造成内存可见性问题这里同样是从CPU指令这一层级进行分析

在由于循环体中是空的所以中主要执行以下两步操作

1.load : 将内存的count读取到CPU寄存器中 

2.cmp : 比较,条件成立就继续顺序执行,条件不成立就跳转到另一个地址中(这里不过多解释)

由于指令的执行其实是很快的,所以,短时间内执行大量的重复的load 和 cmp .

1.由于load要从内存读取数据到寄存器中,这个操作比 cmp 操作要慢很多.

2.并且因为在t2修改前,count值其实是一样的.

3.基于上述原因这个时候 jvm就直接将load这个操作,优化成直接读取之前保存在寄存器中的值(这里只是描述了一下具体的优化是JVM要遵循JMM和编译器的优化规则),使代码的效率提高,这种做法固然是好的,但是在多线程的情况下,你直接读取寄存器的值,就读取不到count被t2修改后的值,导致发生线程安全问题,代码出现了BUG.

4.为什么在循环体里内不做任何事呢,就比如打印一句话,是因为,打印是需要进行I/O操作的,比load还要浪费时间,这个时候 jvm就不一定优化load过程了,虽然还是会产生内存可见性问题,但这是小概率问题了,不易于观察.

4.如何解决内存可见性问题

为了确保内存可见性,Java提供了以下几种机制:

  1. volatile关键字:声明一个变量为volatile可以禁止JVM和CPU对这个变量的读写操作进行重排序,并且要求每次读取该变量都从主内存获取,每次写入都同步到主内存,确保了多线程间的可见性。

  2. synchronized关键字:通过synchronized同步块或方法,不仅保证了在同一时刻只有一个线程访问临界区,而且还隐含地包含了内存可见性,即在同步块或方法结束时,会将修改过的共享变量刷回主内存,同时在进入同步块或方法前会从主内存重新加载变量的值。

  3. final关键字:对于final字段,JMM保证了在构造函数完成后,final字段的值对所有线程都是可见的。

  4. java.util.concurrent.atomic原子类:提供了一系列原子操作,这些操作保证了线程间的原子性和内存可见性。

由于 使用synchronized还会涉及到加锁,以及解锁的时间消耗,这里就不过多的介绍,这里最主要介绍的是volatile关键字.禁止JVM和CPU对这个变量的读写操作进行重排序,并且要求每次读取该变量都从主内存获取,这样就可以避免内存可见性问题了,

5.具体用法

public class Demo {public static volatile int count;//count被volatile修饰public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (count == 0) {// do nothing}System.out.println("线程t1发现count值已改变,不再为0");});Thread t2 = new Thread(() -> {System.out.println("请输入count的值:");Scanner scan = new Scanner(System.in);count = scan.nextInt(); // 修改count的值scan.nextLine(); // 清除换行符});t1.start();t2.start();// 为了让t1有机会看到t2对count的修改,可以让主线程等待t2结束t2.join();System.out.println("主线程已确认t2线程结束,现在t1应能看到count的新值");}
}

结果如下:

这个时候就可以避免内存可见性问题.

6.一道关于volatile关键字的面试问题

问题:volatile的作用,能否保证线程安全问题

volatile关键字在Java中主要作用于变量,其主要目的和作用包括:

  1. 可见性:当一个线程修改了标记为volatile的变量时,其他线程可以立即看到这个变量的最新值,而不是从各自的工作内存(缓存)中读取旧值。这是因为volatile变量的读写操作都会与主内存进行同步,每次读取都会从主内存获取,每次写入都会立即刷新到主内存。

  2. 禁止指令重排序:Java内存模型确保了对volatile变量的操作不会与其他普通变量的读写操作发生重排序,即在多线程环境下,对volatile变量的读写具有一定的顺序约束。

然而,volatile关键字不能完全保证线程安全。它不能防止多个线程同时读写同一变量时产生的竞态条件(race conditions),特别是对于需要多个连续操作组成的原子操作(如递增操作count++),volatile关键字无法保证其原子性。

举例来说,如果你有两个线程同时对一个volatile int count进行递增操作,尽管count的更新对所有线程是可见的,但由于递增操作不是原子的,所以仍然可能发生线程安全问题。

在实际应用中,要实现线程安全,对于需要多个线程读写共享数据的场景,单纯使用volatile往往是不够的,还需要结合synchronizedjava.util.concurrent包中的原子类(如AtomicInteger)或者其他同步机制来确保原子性和线程安全性。

以上就是关于线程安全的初步介绍,感谢你的阅读

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

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

相关文章

Redis高可用主从复制与哨兵模式

前言 在生产环境中&#xff0c;除了采用持久化方式实现 Redis 的高可用性&#xff0c;还可以采用主从复制、哨兵模式和 Cluster 集群的方法确保数据的持久性和可靠性。 目录 一、主从复制 1. 概述 2. 作用 3. 主从复制流程 4. 部署 4.1 安装 redis 4.2 编辑 master 节…

物联网实战--入门篇之(七)嵌入式-MQTT

目录 一、MQTT简介 二、MQTT使用方法 三、MQTT驱动设计 四、代码解析 五、使用过程 六、总结 一、MQTT简介 MQTT因为其轻量、高效和稳定的特点&#xff0c;特别适合作为物联网系统的数据传输协议&#xff0c;已经成为物联网事实上的通信标准了。关于协议的具体内容看看这…

Java实现两数相除

题意 给你两个整数&#xff0c;被除数 dividend 和除数 divisor。将两数相除&#xff0c;要求不使用乘法、除法和取余运算。 整数除法应该向零截断&#xff0c;也就是截去&#xff08;truncate&#xff09;其小数部分。例如&#xff0c;8.345 将被截断为 8 &#xff0c;-2.7335…

leetcode 热题 100(部分)C/C++

leetcode 热题 100 双指针 盛最多水的容器 【mid】【双指针】 思路&#xff1a; 好久没写代码sb了&#xff0c;加上之前写的双指针并不多&#xff0c;以及有点思维定势了。我对双指针比较刻板的印象一直是两层for循环i&#xff0c;j&#xff0c;初始时i,j都位于左界附近&…

Open CASCADE学习|刚体( TopoDS_Shape)按某种轨迹运动,停在指定位置上

今天实现如下功能&#xff1a;刚体做做螺旋运动&#xff0c;轨迹已知&#xff0c;求刚体在每个位置上的所占据的空间&#xff0c;就是把刚体从初始位置变换到该位置。 这里的刚体是一个砂轮截面&#xff0c;螺旋运动轨迹由B样条曲线拟合&#xff0c;通过Frenet标架确定运动轨迹…

iOS使用CoreML运用小型深度神经网络架构对图像进行解析

查找一个图片选择器 我用的是ImagePicker 项目有点老了&#xff0c;需要做一些改造&#xff0c;下面是新的仓库 platform :ios, 16.0use_frameworks!target learnings dosource https://github.com/CocoaPods/Specs.gitpod ImagePicker, :git > https://github.com/KevinS…

Python之Opencv进阶教程(1):图片模糊

1、Opencv提供了多种模糊图片的方法 加载原始未经模糊处理的图片 import cv2 as cvimg cv.imread(../Resources/Photos/girl.jpg) cv.imshow(girl, img)1.1 平均值 关键代码 # Averaging 平均值 average cv.blur(img, (3, 3)) cv.imshow(Average Blur, average)实现效果 1.2…

STM32F407 FSMC并口读取AD7606

先贴一下最终效果图.这个是AD7606并口读取数据一个周期后的数据结果. 原始波形用示波器看是很平滑的. AD7606不知为何就会出现干扰, 我猜测可能是数字信号干扰导致的. 因为干扰的波形很有规律. 这种现象基本上可以排除是程序问题. 应该是干扰或者数字信号干扰,或者是数字和模拟…

基于Spring Boot的餐厅点餐系统

基于Spring Boot的餐厅点餐系统 开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;Maven3.3.9 部分系统展示 管理员登录界面 用户注册登录界面 …

​如何使用ArcGIS Pro进行洪水淹没分析

洪水淹没分析是一种常见的水文地理信息系统应用&#xff0c;用于模拟和预测洪水事件中可能受到淹没影响的地区&#xff0c;这里为大家介绍一下ArcGIS Pro进行洪水淹没分析的方法&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的DEM数据&…

Python学习笔记-Flask接收post请求数据并存储数据库

1.引包 from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy 2.配置连接,替换为自己的MySQL 数据库的实际用户名、密码和数据库名 app Flask(__name__) #创建应用实列 app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://ro…

鸿蒙OS开发实例:【应用事件打点】

简介 传统的日志系统里汇聚了整个设备上所有程序运行的过程流水日志&#xff0c;难以识别其中的关键信息。因此&#xff0c;应用开发者需要一种数据打点机制&#xff0c;用来评估如访问数、日活、用户操作习惯以及影响用户使用的关键因素等关键信息。 HiAppEvent是在系统层面…

适用于 Linux 的 Windows 子系统安装初体验

1、简述 Windows Subsystem for Linux (WSL) 是 Windows 的一项功能&#xff0c;允许您在 Windows 计算机上运行 Linux 环境&#xff0c;而无需单独的虚拟机或双重启动。 WSL 旨在为想要同时使用 Windows 和 Linux 的开发人员提供无缝且高效的体验。 使用 WSL 安装和运行各种 L…

【javaScript】DOM编程入门

一、什么是DOM编程 概念&#xff1a;DOM(Document Object Model)编程就是使用document对象的API完成对网页HTML文档进行动态修改&#xff0c;以实现网页数据和样式动态变化的编程 为什么要由DOM编程来动态修改呢&#xff1f;我们就得先理解网页的运行原理&#xff1a; 如上图&a…

IO流:字节流、字符流、缓冲流、转换流、数据流、序列化流 --Java学习笔记

目录 IO流 IO流的分类 IO流的体系 字节流&#xff1a; 1、Filelnputstream(文件字节输入流) 2、FileOutputStream(文件字节输出流) 字节流非常适合做一切文件的复制操作 复制案例&#xff1a; try-catch-finally 和 try-with-resource 字符流 1、FileReader(文件字符…

ALPHA开发板上的PHY芯片驱动:LAN8720驱动

一. 简介 前面文章了解到&#xff0c;Linux内核是有提供 PHY通用驱动的。 本文来简单了解一下ALPHA开发板上的 PHY网络芯片LAN8720的驱动。是 LAN8720芯片的公司提供的 PHY驱动。 二. ALPHA开发板上的PHY芯片驱动&#xff1a;LAN8720驱动 我 们 来 看 一 下 LAN8720A 的 …

输入url到页面显示过程的优化

浏览器架构 线程&#xff1a;操作系统能够进行运算调度的最小单位。 进程&#xff1a;操作系统最核心的就是进程&#xff0c;他是操作系统进行资源分配和调度的基本单位。 一个进程就是一个程序的运行实例。启动一个程序的时候&#xff0c;操作系统会为该程序创建一块内存&a…

HDLbits 刷题 --Always nolatches

学习: Your circuit has one 16-bit input, and four outputs. Build this circuit that recognizes these four scancodes and asserts the correct output. To avoid creating latches, all outputs must be assigned a value in all possible conditions (See also always…

【HTML】简单制作一个3D动画效果重叠圆环

目录 前言 开始 HTML部分 CSS部分 效果图 总结 前言 无需多言&#xff0c;本文将详细介绍一段代码&#xff0c;具体内容如下&#xff1a; 开始 首先新建文件夹&#xff0c;创建两个文本文档&#xff0c;其中HTML的文件名改为[index.html]&#xff0c;CSS的…

搞学术研究好用免费的学术版ChatGPT网站-学术AI

学术版ChatGPThttps://chat.uaskgpt.com/mobile/?user_sn88&channelcsdn&scenelogin 推荐一个非常适合中国本科硕士博士等学生老师使用的学术版ChatGPT&#xff0c; 对接了超大型学术模型&#xff0c;利用AI技术实现学术润色、中英文翻译&#xff0c;学术纠错&#…