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

˃͈꒵˂͈꒱ 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.rhkb.cn/news/296772.html

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

相关文章

C语言交换二进制位的奇数偶数位

基本思路 我们要先把想要交换的数的二进制位给写出来假如交换13的二进制位&#xff0c;13的二进制位是 0000 0000 0000 0000 0000 0000 0000 1101然后写出偶数位的二进制数&#xff08;偶数位是1的&#xff09; 1010 1010 1010 1010 1010 1010 1010 1010然后写出奇数位的二进…

2012年认证杯SPSSPRO杯数学建模C题(第一阶段)碎片化趋势下的奥运会商业模式全过程文档及程序

2012年认证杯SPSSPRO杯数学建模 C题 碎片化趋势下的奥运会商业模式 原题再现&#xff1a; 从 1984 年的美国洛杉矶奥运会开始&#xff0c;奥运会就不在成为一个“非卖品”&#xff0c;它在向观众诠释更高更快更强的体育精神的同时&#xff0c;也在攫取着巨大的商业价值&#…

Spring Boot Mockito (三)

Spring Boot Mockito (三) 这篇文章主要是讲解Spring boot 与 Mockito 集成测试。 前期项目配置及依赖可以查看 Spring Boot Mockito (二) - DataJpaTest Spring Boot Mockito (一) - WebMvcTest Tag("Integration") SpringBootTest // TestMethodOrder(MethodOr…

go 指针和内存分配

定义 了解指针之前&#xff0c;先讲一下什么是变量。 每当我们编写任何程序时&#xff0c;我们都需要在内存中存储一些数据/信息。数据存储在特定地址的存储器中。内存地址看起来像0xAFFFF&#xff08;这是内存地址的十六进制表示&#xff09;。 现在&#xff0c;要访问数据…

程序员们应注意的行业特有的法律问题

大家好&#xff0c;我是不会魔法的兔子&#xff0c;是一枚执业律师&#xff0c;持续分享技术类行业项目风险及预防的问题。 一直以来兔子都在以大家做项目时候会遇到的风险问题做分享&#xff0c;最近有个念头一直挥之不去&#xff0c;就是要不要给我们广大的程序员们也分享一…

【接口】HTTP(1)|请求|响应

1、概念 Hyper Text Transfer Protocol&#xff08;超文本传输协议&#xff09;用于从万维网&#xff08;就是www&#xff09;服务器传输超文本到本地浏览器的传送协议。 HTTP协议是基于TCP的应用层协议&#xff0c;它不关心数据传输的细节&#xff0c;主要是用来规定客户端和…

【C++练级之路】【Lv.18】哈希表(哈希映射,光速查找的魔法)

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《算法神殿》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、哈希1.1 哈希概念1.2 哈希函数1.3 哈希冲突 二、闭散列2.1 数据类型2.2 成员变量2.3 默认成员函数2.…

【yy讲解PostCSS是如何安装和使用】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

深度学习:神经网络模型的剪枝和压缩简述

深度学习的神经网路的剪枝和压缩&#xff0c;大致的简述&#xff0c; 主要采用&#xff1a; network slimming&#xff0c;瘦身网络... 深度学习网络&#xff0c;压缩的主要方式&#xff1a; 1.剪枝&#xff0c;nerwork pruing&#xff0c; 2.稀疏表示&#xff0c;sparse rep…

KV260 BOOT.BIN更新 ubuntu22.04 netplan修改IP

KV260 2022.2设置 BOOT.BIN升级 KV260开发板需要先更新BOOT.BIN到2022.2版本&#xff0c;命令如下&#xff1a; sudo xmutil bootfw_update -i “BOOT-k26-starter-kit-202305_2022.2.bin” 注意BOOT.BIN应包含全目录。下面是更新到2022.1 FW的示例&#xff0c;非更新到2022.…

八数码问题——A*算法的应用(A-Star)

文章目录 1 问题描述2 启发式搜索3 A*算法3.1 参考网址3.2 是什么3.3 为什么A*算法适用于八数码问题3.4 A* 算法的基本框架 4 A* 算法如何解决八数码问题4.1 八数码状态的存储4.2 启发式函数4.3 构造目标状态元素位置的字典4.4 在二维列表中查找目标元素4.5 A* 算法主体4.6 路径…

Git 术语及中英文对照

完毕&#xff01;&#xff01;感谢您的收看 ----------★★历史博文集合★★---------- 我的零基础Python教程&#xff0c;Python入门篇 进阶篇 视频教程 Py安装py项目 Python模块 Python爬虫 Json Xpath 正则表达式 Selenium Etree CssGui程序开发 Tkinter Pyqt5 列表元组字…

Ubuntu22.04安装Anaconda

一、下载安装包 下载地址&#xff1a;https://www.anaconda.com/download#Downloads 参考&#xff1a;Ubuntu下安装Anaconda的步骤&#xff08;带图&#xff09; - 知乎 下载Linux 64-Bit (x86) installer 二、安装 在当前路径下&#xff0c;执行命令&#xff1a; bash Ana…

了解以太坊虚拟机(EVM)

了解以太坊虚拟机&#xff08;EVM&#xff09; 以太坊虚拟机&#xff08;Ethereum Virtual Machine&#xff0c;简称EVM&#xff09;是以太坊网络的核心组件之一&#xff0c;它承担着智能合约执行的重要任务 特点 智能合约执行环境&#xff1a;EVM提供了一个安全的环境&#xf…

vxe-table表格组件给row-style和cell-style等修改样式无效的问题,例如:background-color

因情况而异吧&#xff0c;我是因为使用了jsx jsx的语法规则之一&#xff1a;内联样式&#xff0c;要用 style{{key:value}}的形式去写。有的需要以小驼峰式写,例如&#xff1a;font-size需要写成 fontSize background-color就是backgroundColor

【智能排班系统】快速消费线程池

文章目录 线程池介绍线程池核心参数核心线程数&#xff08;Core Pool Size&#xff09;最大线程数&#xff08;Maximum Pool Size&#xff09;队列&#xff08;Queue&#xff09;线程空闲超时时间&#xff08;KeepAliveTime&#xff09;拒绝策略&#xff08;RejectedExecutionH…

Raven:一款功能强大的CICD安全分析工具

关于Raven Raven是一款功能强大的CI/CD安全分析工具&#xff0c;该工具旨在帮助广大研究人员对GitHub Actions CI工作流执行大规模安全扫描&#xff0c;并将发现的数据解析并存储到Neo4j数据库中。 Raven&#xff0c;全称为Risk Analysis and Vulnerability Enumeration for C…

jQuery(一)

文章目录 1. 基本介绍2.原理示意图3.快速入门1.下载jQuery2.创建文件夹&#xff0c;放入jQuery3.引入jQuery4.代码实例 4.jQuery对象与DOM对象转换1.基本介绍2.dom对象转换JQuery对象3.JQuery对象转换dom对象4.jQuery对象获取数据获取value使用val&#xff08;&#xff09;获取…

HCIA-RS基础-VLAN路由

目录 VLAN 路由1. 什么是 VLAN 路由2. VLAN 路由的原理及配置3. VLAN 的缺点和 VLAN Trunking4. 单臂路由配置 总结 VLAN 路由 1. 什么是 VLAN 路由 VLAN 路由是指在虚拟局域网&#xff08;VLAN&#xff09;之间进行路由转发的过程。传统的 VLAN 配置只能在同一个 VLAN 内进行…

【LeetCode热题100】51. N 皇后(回溯)

一.题目要求 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回所有不同的 n 皇后问题 的解决方…