【多线程】线程安全

目录

 一、初识线程安全

什么是线程安全问题

理解线程不安全的原因

原因总结

二、解决线程不安全

加锁🔐

锁对象

synchronized几种使用方式

死锁🔏

死锁的三个场景

(1)一个线程针对一把锁连续加锁两次

(2)两个线程两把锁

(3)N个线程M个锁

如何解决死锁问题

三、内存可见性问题

什么是内存可见性问题

volatile 关键字


多线程章节中,最重要的话题就是线程安全。因为多个线程同时执行某个代码的时候,可能会引起一些奇怪的bug,理解了线程安全,才能避免/解决上述的bug。

 一、初识线程安全

什么是线程安全问题

public class Demo18 {private 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.start();t2.start();t1.join();t2.join();System.out.println("count=" + count);}
}

预期是10w结果和实际不一样,而且每次运行的结果都不一样

理解线程不安全的原因

上面代码中的cout++操作,其实在CPU视角来看,是3个指令
1)把内存中的数据,读取到CPU寄存器里     load
2)把CPU寄存器里的数据+1     add
3)把寄存器的值,写回内存     save

CPU在调度执行线程的时候,说不上啥时候,就会把线程给切换走(抢占式执行,随机调度)。指令是CPU执行的最基本单位,要调度,至少把当前执行完,不会执行一半调度走。但是由于cout++是三个指令,可能会出现CPU执行了其中的1个指令或者2个指令或者3个指令调度走的情况,这是都有可能无法预测的。

基于上面的情况,两个线程同时对count进行++就容易出现bug。

上述的执行顺序,只是一种可能的调度顺序,由于调度过程是"随机"的,因此就会产生很多其他的执行顺序。上述过程中,明明是++了两次但是最终结果,还是1,因为这两次加的过程中,结果出现了"覆盖"

由于循环5w次过程中,也不知道有多少次的执行顺序,是这种正确情况,有多少次是其他的出错情况,因此最终的结果,是不确定的值,而且这个值,一定小于10W。

对于多线程代码来说,最大的困难,就在于"随机调度,抢占式执行",是多线程编码的"罪魁祸首,万恶之源”。

面试的时候,被问到,线程不安全的原因,你也可以尝试给面试官画图。

其他所有原因总结

1️⃣线程在操作系统中,随机调度,抢占式执行 [根本原因]
2️⃣多线程,同时修改同一个变量(如果是多个线程读取变量或只有一个线程或修改不同的变量都不会)
3️⃣修改操作,不是"原子"的(count++ 背后是三个指令,这个操作不是原子的)

4️⃣内存可见性问题

5️⃣指令重排序

二、解决线程不安全

第一个原因,无法干预,操作系统内核,负责的工作,咱们作为应用层的程序员,无法干预。第二个原因,可以让线程修改不同的变量,可能可行,取决于实际的需求,有的场景能这么改,有的场景不能这么改,取决于实际的需求,在Java中这个方案不算很普适的方案,但是有的语言,更青睐这个方案,erlang这个语言,就是采取这个方案,解决并发编程中的"线程安全"问题的,它没有变量,所有的"变量"都是"常量",不能修改,自然也就不必担心上述的线程安全问题了。

加锁🔐

解决线程安全问题,最主要的办法,就是把"非原子"的修改,变成"原子"--“加锁”。

此处的加锁,并不是真的让count++变成原子的,也没有干预到线程的调度,只不过是通过这种加锁的方式,使一个线程在执行count++的过程中,其他的线程的count++不能插队进来。
下面结合代码来看:Java中提供了synchronized关键字,来完成加锁操作

public class Demo18 {private static int count = 0;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.start();t2.start();t1.join();t2.join();System.out.println("count=" + count);}
}

进入代码块就会进行加锁,出了代码块就会进行解锁

本质上是把随机的并发执行过程,强制变成了串行,从而解决了刚才的线程安全问题

锁对象

上述代码有效的前提是,两个线程,都加锁了,而且是针对同一个对象加锁

 

锁对象作用,就是用来区分,多个线程,是否是针对"同一个对象"加锁",是针对同一个对象加锁,此时就会出现"阻塞"(锁竞争/锁冲突)。不是针对同一个对象加锁,此时不会出现"阻塞",两个线程仍然是随机调度的并发执行。锁对象,填哪个对象,不重要,重要的是,多个线程是否是同一个锁对象。 

锁对象,肯定得是个对象,不能拿int,double这种内置类型,来写到()里,但是其他的类型,只要是Object(或者是子类)都是可以的,例如字符串就可以。

或者

⚠️注意:咱们此处的加锁后的代码本质上比join的串行执行,效率还是要高的。加锁,就是变成"串行执行",那么是否就没必要使用多线程了?当然不是的,加锁,只是把线程中的一小部分逻辑,变成"串行执行",剩下的其他部分,仍然是可以并发执行的。

如果是3个线程针对同一个对象加锁,也是类似的情况。其中某个线程先加上锁,另外两个线程阻塞等待(哪个线程拿到锁,这个过程不可预期的)。拿到锁的线程释放了锁之后,剩下两个线程谁先拿到锁呢?也是顺序不确定的。123,比如最开始1拿到锁,2、3阻塞等待1释放锁之后,2和3谁先拿到锁?不一定,也是随机的,即使2先加锁,3后加锁,也不一定谁先拿到。此处synchronized是JVM提供的功能,synchronized底层实现就是JVM中,通过C++代码来实现的,进一步的,也是依靠操作系统提供的API实现的加锁,操作系统的API则是来自于CPU上支持的特殊的指令来实现的。

synchronized几种使用方式

synchronized还可以修饰一个方法

class Counter {public int count = 0;public void add() {count++;}
}public class Demo19 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (counter){counter.add();}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (counter){counter.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println("counter=" + counter.count);}
}

或者

class Counter {public int count = 0;synchronized public void add() {count++;}
}public class Demo19 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("counter=" + counter.count);}
}

1)synchronized(){}
圆括号指定锁对象
2)synchronized修饰一个普通的方法
相当于针对this加锁
3)synchronized修饰一个静态的方法
相当于针对对应的类对象加锁

锁是解决线程安全问题典型的做法,关于锁内部的原理和特性,Java其他的锁的实现,后面慢慢展开。

死锁🔏

死锁的三个场景

(1)一个线程针对一把锁连续加锁两次
class Counter {public int count = 0;public void add() {synchronized (this) {count++;}}
}public class Demo19 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (counter) {counter.add();}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (counter) {counter.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println("counter=" + counter.count);}
}

1)里面的synchronized要想拿到锁,就需要外面的synchronized释放锁
2)外面的synchronized要释放锁,就需要执行到}
3)要想执行到}就需要执行完这里的add
4)但是add正阻塞着

上面一顿分析猛如虎,结果一运行,结果出来了,没有死锁呀,这里没有死锁,是Java的synchronized做了特殊处理。同样的代码,换成C++/Python就会死锁,Java为了减少程序员写出死锁的概率,引入了特殊机制,解决上述的死锁问题,"可重入锁"。加锁的时候,是需要判定,当前这个锁,是否是被占用的状态,可重入锁,就是在锁中,额外记录一下,当前是哪个线程,对这个锁加锁了。对于可重入锁来说,发现加锁的线程就是当前锁的持有线程,并不会真正进行任何加锁操作,也不会进行任何的"阻塞操作"而是直接放行,往下执行代码。

可重入锁引入之后,为了避免,出现上述一个线程连续加锁两次就死锁的情况,synchronized就是可重入锁,可重入锁内部记录了当前是哪个线程持有的锁,后续加锁的时候都会进行判定,还会通过一个引用计数维护当前已经加锁几次了,并且描述出何时真正释放锁。

(2)两个线程两把锁

public class Demo20 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("t1 加锁 locker1 完成");//这里的 sleep 是为了确保,t1 和 t2 都先分别拿到 locker1 和 locker2 然后再分别拿对方的锁//如果没有 sleep 执行顺序就不可控,可能出现某个线程一口气拿到两把锁,另一个线程还没执行呢,无法构造出死锁try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("t1 加锁 locker2 完成");}}});Thread t2 = new Thread(() -> {synchronized (locker2) {System.out.println("t2 加锁 locker2 完成");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker1) {System.out.println("t2 加锁 locker1 完成");}}});t1.start();t2.start();}
}

借助第三方工具也可以看到两线程都是BLOCKED的状态

(3)N个线程M个锁

死锁经典模型:哲学家就餐问题

如何解决死锁问题

死锁产生的四个必要条件:
·互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
·不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
·请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
·循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
其中最容易破坏的就是"循环等待"

最常用的一种死锁阻止技术就是锁排序,假设有N个线程尝试获取M把锁,就可以针对M把锁进行编号(1,2,3......M)N个线程尝试获取锁的时候,都按照固定的按编号由小到大顺序来获取锁。这样就可以避免环路等待。

每个滑稽加锁的时候一定是先拿起编号小的筷子,后拿起编号大的筷子。同一时刻,所有线程拿起第一根筷子。

public class Demo20 {private static Object locker1 = new Object();private static Object locker2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("t1 加锁 locker1 完成");//这里的 sleep 是为了确保,t1 和 t2 都先分别拿到 locker1 和 locker2 然后再分别拿对方的锁//如果没有 sleep 执行顺序就不可控,可能出现某个线程一口气拿到两把锁,另一个线程还没执行呢,无法构造出死锁try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("t1 加锁 locker2 完成");}}});Thread t2 = new Thread(() -> {synchronized (locker1) {System.out.println("t2 加锁 locker1 完成");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker2) {System.out.println("t2 加锁 locker2 完成");}}});t1.start();t2.start();}
}

三、内存可见性问题

什么是内存可见性问题

如果一个线程修改,另一个线程读取,这样的代码是否会有线程安全呢?

import java.util.Scanner;public class Demo21 {private static int n = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (n == 0) {// 啥都不写}System.out.println("t1 线程结束循环");});Thread t2 = new Thread(() -> {Scanner sc = new Scanner(System.in);System.out.println("请输入一个整数:");n = sc.nextInt();});t1.start();t2.start();}
}

上述问题的原因就是内存可见性问题

内存可见性问题,本质上,是编译器/JVM对代码进行优化的时候优化出bug,如果代码是单线程的,编译器/JVM,代码优化一般都是非常准确的,优化之后,不会影响到逻辑。但是代码如果是多线程的,编译器/JVM的代码优化,就可能出现误判(编译器/JVM的bug),导致不该优化的地方,也给优化了,于是就造成了内存可见性问题了。编译器为啥要做上述的代码优化?为啥不老老实实的按照程序员写的代码,一板一眼执行,主要是因为,有的程序员,写出来的代码,太低效了,为了能够降低程序员的门槛,即使你代码写的一般,最终执行速度也不会落下风。因此主流编译器,都会引入优化机制(优化手段是多种多样的),优化编译器自动调整你的代码,保持原有逻辑不变的前提下,提高代码的执行效率,代码优化的效果是非常明显的。

解决方案一:

此处即使sleep时间非常短,但是刚才的内存可见性问题就消失了,t2的修改就能被t1感知到。说明加入sleep之后,刚才谈到的针对读取n内存数据的优化操作,不再进行了。和读内存相比,sleep开销是更大的,远远超过了读取内存就算把读取内存操作优化掉,也没有意义,杯水车薪。

volatile 关键字

如果代码中,循环里没有sleep,又希望代码能够没有bug的正确运行呢?volatile关键字修饰一个变量,提示编译器说,这个变量是"易变"的。编译器进行上述优化的前提是编译器认为,针对这个变量的频繁读取,结果都是固定的,此时,编译器就会禁止上述的优化,确保每次循环都是从内存中重新读取数据。

import java.util.Scanner;public class Demo21 {private static volatile int n = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (n == 0) {// 啥都不写}System.out.println("t1 线程结束循环");});Thread t2 = new Thread(() -> {Scanner sc = new Scanner(System.in);System.out.println("请输入一个整数:");n = sc.nextInt();});t1.start();t2.start();}
}

编译器的开发者,知道这个场景中,可能出现误判,于是就把权限交给了程序员,让程序员能够部分的干预到优化的进行。让程序员显式的提醒编译器,这里别优化。引入volatile的时候,编译器生成这个代码的时候,就会给这个变量的读取操作,附近生成一些特殊的指令,称为"内存屏障",后续JVM执行到这些特殊指令,就知道了,不能进行上述优化了。

⚠️注意:volatile只是解决内存可见性问题,不能解决原子性问题。如果两个线程针对同一个变量进行修改(count++),volatile无能为力:

public class Demo22 {private static volatile 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.start();t2.start();t1.join();t2.join();System.out.println("count =" + count);}
}

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

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

相关文章

传统文旅+AI构建数字文旅新生态

传统文旅AI构建数字文旅新生态 前言&#xff1a; 当前许多旅游景区在旅游管理和旅游基础设施配套上都下足了功夫&#xff0c;在一定程度上也给旅客和消费者带来了舒适的体验感。但是针对于我们游客而言&#xff0c;似乎只有欣赏沿途风景、了解景区历史文化、拍照打卡和品尝特色…

VSCode - VSCode 切换自动换行

VSCode 自动换行 1、基本介绍 在 VSCode 中&#xff0c;启用自动换行可以让长行代码自动折行显示&#xff0c;避免水平滚动条频繁使用&#xff0c;提升代码阅读体验 如果禁用自动换行&#xff0c;长行代码就需要手动结合水平滚动条来阅读 2、演示 启用自动换行 禁用自动换…

解锁音频新境界:LALAL.AI 与 Audo Studio 深度解析

在音频处理的世界里&#xff0c;噪音常常是困扰我们的一大难题。无论是专业的音频工作者&#xff0c;还是普通的音频爱好者&#xff0c;都渴望拥有一款强大的工具来解决这个问题。今天&#xff0c;就为大家介绍两款来自 AI 工具导航&#xff08;AIDH.NET&#xff09;的 AI 语音…

线上死锁问题排查和处理

Java 死锁排查 通过 jps jstack 来定位和排查 如果线程长时间处于阻塞&#xff0c;就需要考虑是否是死锁了。 模拟死锁 public class DeadlockDemo {private static final Object lock1 new Object();private static final Object lock2 new Object();public static vo…

5 分钟用满血 DeepSeek R1 搭建个人 AI 知识库(含本地部署)

最近很多朋友都在问&#xff1a;怎么本地部署 DeepSeek 搭建个人知识库。 老实说&#xff0c;如果你不是为了研究技术&#xff0c;或者确实需要保护涉密数据&#xff0c;我真不建议去折腾本地部署。 为什么呢&#xff1f; 目前 Ollama 从 1.5B 到 70B 都只是把 R1 的推理能力…

MT7628基于原厂的SDK包, 修改ra1网卡的MAC方法。

1、在/etc/config/wireless文件添加多个WIFI网卡的方法。 2、修改WIFI驱动&#xff0c;在src/embedded/ap/ap.c文件里面&#xff0c;从系统文件信息来修改ra1网卡的MAC内容&#xff0c;添加红色部分源代码。 RTMP_IO_WRITE32(pAd, RMAC_RMACDR, Value); if (idx > 0) …

brew Nushell mac升级版本

运行命令&#xff1a; brew upgrade nushell 国内更新比较慢建议架个梯子。 如果没有更新则先更新一下brew brew update 更新后看下版本是否死最新的了

windows怎样查看系统信息(处理器等)

首先打开命令行工具 win R 输入 cmd&#xff0c; 输入 msinfo32 &#xff0c;然后回车 这个页面就可以看到 电脑的锐龙版就是 AMD 芯片 酷睿版就是 intel 芯片

mysql之Innodb数据页

Innodb数据页结构 InnoDB数据页结构一、数据页基础概念二、数据页核心结构1. 头部控制区2. 数据存储区3. 尾部与目录区 三、关键机制详解1. 记录链表与删除优化2. 页目录与二分查找3. 空间复用与碎片管理4. 数据页的合并与分裂 四、应用与性能影响1. 索引效率2. 插入优化3. 事务…

1200沿指令和取反指令的应用。

以下是关于西门子S7-1200 PLC中沿指令&#xff08;边沿检测指令&#xff09;和取反指令的详细解析及应用示例&#xff0c;结合其工作原理、编程方法和典型场景&#xff1a; 一、沿指令&#xff08;边沿检测指令&#xff09; 1. 功能说明 沿指令用于检测信号状态的变化&#x…

three.js之特殊材质效果

*案例42 创建一个透明的立方体 <template><div ref"container" className"container"></div> </template><script setup> import * as THREE from three; import WebGL from three/examples/jsm/capabilities/WebGL.js // 引…

三格电子上新了——PLC 数据采集网关

型号&#xff1a;SG-PLC-Private 第一章 产品概述 PLC 转 Modbus 网关型号 SG-PLC-Private &#xff08; PLC 私有协议网关&#xff09;&#xff0c;是三格电子推出的工业 级网关&#xff08;以下简称网关&#xff09;&#xff0c;主要用于 在不需要对 PLC 编程的情况…

算法日记25:01背包(DFS->记忆化搜索->倒叙DP->顺序DP->空间优化)

对于01背包这类DP入门的问题&#xff0c;新手应该是去了解如何一步步得出所谓的状态转移方程&#xff0c;而不是直接去看答案所给予的方程过程应该为&#xff1a;DFS->记忆化搜索->倒序递推->循序递推->二维->一维 一、DFS暴力搜索 O ( 2 n ) O(2^n) O(2n) 1…

Spring AutoWired与Resource区别?

大家好&#xff0c;我是锋哥。今天分享关于【Spring AutoWired与Resource区别?】面试题。希望对大家有帮助&#xff1b; Spring AutoWired与Resource区别? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 Spring 中&#xff0c;Autowired 和 Resource 都是用于…

【知识】深度学习中,应该先zero_grad还是先backward?

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 抛出问题 各大GPT的回答 ChatGPT-4o ChatGPT-o3-mini-high Kimi-长思考 Deepseek-R1 Grok3 Pytorch官方教程中 抛出问题 以下哪种方式是…

Python----数据结构(哈希表:哈希表组成,哈希冲突)

一、哈希表 哈希表(Hash table)是一种常用、重要、高效的数据结构。 哈希表通过哈希函数,可以快速地将键(Key)映射到值(Value)。从而允许在近常数时间内对键关联的值进行插入、删除和查找操作。 哈希表的主要思想是通过哈希函数将键转换为索引&#xff0c;将索引映射到数组中…

使用excel中的VBA合并多个excel文件

需求是这样的&#xff1a; 在Windows下&#xff0c;用excel文件让多个小组填写了统计信息&#xff0c;现在我需要把收集的多个文件汇总到一个文件中&#xff0c;前三行为标题可以忽略&#xff0c;第四行为收集信息的列名&#xff0c;处理每一行数据的时候&#xff0c;发现某一行…

功能全面的手机壁纸应用,种类齐全、众多高清壁纸

软件介绍 应用亮点&#xff1a;今天给大家分享一款超神奇的手机应用 —— 奇幻壁纸。它作为手机动态壁纸软件&#xff0c;功能超全面&#xff0c;操作还便捷&#xff0c;极具创意&#xff0c;能瞬间将你的手机屏幕变成奇幻世界&#xff0c;带来全新视觉感受。 使用便捷性&…

docker安装kafka,并通过springboot快速集成kafka

目录 一、docker安装和配置Kafka 1.拉取 Zookeeper 的 Docker 镜像 2.运行 Zookeeper 容器 3.拉取 Kafka 的 Docker 镜像 4.运行 Kafka 容器 5.下载 Kafdrop 6.运行 Kafdrop 7.如果docker pull wurstmeister/zookeeper或docker pull wurstmeister/kafka下载很慢&#x…

前端导出word文件,并包含导出Echarts图表等

基础导出模板 const html <html><head><style>body {font-family: Times New Roman;}h1 {text-align: center;}table {border-collapse: collapse;width: 100%;color: #1118FF;font-weight: 600;}th,td {border: 1px solid black;padding: 8px;text-align: …