Java多线程-----线程安全问题(详解)

目录

🍇一.线程安全问题的引入:

🍒二.线程安全问题产生的原因:

🍌三.如何解决线程安全问题:

🎉1.synchronized关键字:

🦉sychronized关键字的特性:

✨2.volatile关键字:


🍇一.线程安全问题的引入:

首先我们来看下面一段代码,我们通过两个线程同时操作一个静态成员变量count,使其一共累加10w次,看看结果:

public class Main {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.start();t2.start();//让主线程等待t1,t2线程结束,统计此时count的累加结果t1.join();t2.join();System.out.println(count);}
}
第一次执行结果:
59355
第二次执行结果:
54362
第三次执行结果:
53976

这是我们发现,三次累加count的结果都不一样,很明显,这里出现了bug!

因为多个线程并发执行,引起的bug,这样的bug称为“线程安全问题”或者叫做“线程不安全”

🍒二.线程安全问题产生的原因:

那么这个问题是怎样产生的呢?这里,我们引出线程安全问题产生的原因:

  • 线程在操作系统中是随机调度,抢占式执行的【根本原因】

程序中的多个线程是并发执行的,某个线程若想被执行必须要得到CPU的使用权,Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称为线程的调度。

两种线程调度的模式:

①.分时调度模式让所有的线程轮流获得CPU的使用权,并且平均分配给每个线程占用CPU的时间段

②.抢占式调度模式让就绪队列中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,在随机选择其他线程使其占用CPU。

  • 多个线程,由于是并发执行的,此时如果同时修改一个共享数据的代码块或则变量,会导致线程安全问题

  • 修改操作,不是“原子”的

这里的原子性与MySQL事务中的原子性是一个意思,对于一组操作,这组操作是不可分割的最小单元,程序运行时要么同时成功,要么同时失败,不存在程序运行一般就结束的情况,这个操作要求一气呵成。而在CPU的视角,一条指令就是CPU上不可分割的最小单元,CPU在进行调度切换线程的时候,势必会确保执行完一条完整的指令,这个过程包含取指令,解析指令和执行指令。而上述的count++这个操作,就不是原子的,其在CPU看来有三个指令:

①.把内存中的数据,读取到CPU寄存器中 (load操作)

②.把CPU寄存器里的值+1 (add操作)

③.把寄存器里的值,写回到内存中 (save操作)

此时我们在对上述操作进行分析t1,t2两个线程:

  • 内存可见性问题:

可见性定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile关键字来保证可见性,当一个变量被volatile修饰后,表示着本地内存无效,当一个线程修改共享变量后他会立即被更新到主存中,其他线程读取共享变量的时候,会直接从主存中读取,从而实现了可见性.

  • 指令重排序:

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为如下三种:

1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。


JVM翻译字节码指令,CPU执行机器码指令,都可能发生重排序来优化执行效率

比如有这样三步操作:(1) 去前台取U盘 (2) 去教室写作业 (3) 去前台取快递 JVM会对指令优化,也就是重排序,新的顺序为(1)(3)(2),这样来提高效率

导致线程安全问题的小结:

  1. 线程是抢占式的执行,线程间的调度充满了随机性
  2. 多个线程对同一个变量进行修改操作
  3. 对变量的操作不是原子性的
  4. 内存可见性导致的线程安全
  5. 指令重排序也会影响线程安全

🍌三.如何解决线程安全问题:

🎉1.synchronized关键字:

解决方案:将操作共享数据的代码块锁起来

①.修饰代码块:

synchronized(锁对象)
{//操作共享数据的代码};

特点1:锁默认打开,有一个线程进去了,锁自动关闭

特点2:里面的代码全部执行完毕,线程出来,锁自动打开

特点3:锁对象,一定是唯一的

②.修饰普通方法:

public synchronized void doSomething(){//操作共享数据的代码
}

其等同于:

public void doSomething(){//this->当前对象的引用synchronized(this){//操作共享数据的代码块}
}

③.修饰静态方法:

public static synchronized void doSomething(){//操作共享数据的代码块
}

其相当于

public static void doSomething(){//锁对象是当前类的字节码文件对象synchronized(A.class){//操作共享数据的代码块}
}

这里我们利用synchronized关键字,解决上述线程安全问题:通过两个线程同时操作一个静态成员变量count,使其一共累加10w次

public class Main {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0;i < 50000;i++){//利用锁将操作共享数据的代码块锁起来,每次只允许一个线程进入进行操作//从而解决了线程安全问题synchronized (Main.class){count++;}}});Thread t2 = new Thread(()->{for(int i = 0;i < 50000;i++){//利用锁将操作共享数据的代码块锁起来,每次只允许一个线程进入进行操作synchronized(Main.class){count++;}}});t1.start();t2.start();//让主线程等待t1,t2线程结束,统计此时count的累加结果t1.join();t2.join();System.out.println(count);}
}

运行结果(此时我们无论运行多少次,count的计算结果都是10w):

sychronized关键字的作用:

①.sychronized是基于对象头加锁的,特别注意:不是对代码加锁,所说的加锁操作就是给这个对象的对象头里设置了一个标志位,一个对象在同一时间只能有一个线程获取到该对象的锁

②.sychronized保证了原子性,可见性,有序性(这里的有序不是指指令重排序,而是具有相同锁的代码块按照获取锁的顺序执行)

🦉sychronized关键字的特性:

1).互斥:

synchronized 会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏ 到同⼀个对象synchronized就会阻塞等待.

• 进⼊synchronized修饰的代码块,相当于加锁

• 退出synchronized修饰的代码块,相当于解锁

加锁的大致过程:

2) 可重⼊:

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题。

⼀个线程没有释放锁,然后⼜尝试再次加锁,按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待.直到第⼀次的锁被释放,才能获取到第二个锁.但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想⼲了,也就⽆法进行解锁操作.这时候就会死锁.

Java 中的synchronized是可重⼊锁,因此没有上⾯的问题

public class Main2 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0;i < 50000;i++){//利用锁将操作共享数据的代码块锁起来,每次只允许一个线程进入进行操作//从而解决了线程安全问题synchronized (Main2.class){synchronized (Main2.class){count++;}}}});Thread t2 = new Thread(()->{for(int i = 0;i < 50000;i++){//利用锁将操作共享数据的代码块锁起来,每次只允许一个线程进入进行操作synchronized (Main2.class){synchronized (Main2.class){count++;}}}});t1.start();t2.start();//让主线程等待t1,t2线程结束,统计此时count的累加结果t1.join();t2.join();System.out.println(count);}
}

运行结果:

在可重⼊锁的内部,包含了"线程持有者"和"计数器"两个信息.

• 如果某个线程加锁的时候,发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器⾃增.

• 解锁的时候计数器递减为0的时候,才真正释放锁.(才能被别的线程获取到)

✨2.volatile关键字:

volatile关键字的作用主要有如下两个:(volatile是用来修饰变量的,它的作用是保证可见性,有序性)

  1. 保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

  2. 保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。

注意:volatile 不能保证原子性

public class Main {public boolean flag = true;//改变flag的值public void changeFlag(){this.flag = false;}public static void main(String[] args) {Main test = new Main();Thread t1 = new Thread(()->{while(test.flag){}System.out.println("线程一结束~~~");});Thread t2 = new Thread(()->{try {Thread.sleep(1000);test.changeFlag();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程二结束");});t1.start();t2.start();}
}

运行结果:

当我们给变量flag加上volatile关键字:

 public volatile boolean flag = true;

运行结果(从打印结果不难看出,线程一读取到了flag修改后的值,线程一顺利结束):

说到可见性,我们需要先了解一下Java内存模型,Java内存模型如下所示:

线程之间的共享变量存储在主内存中(Main Memory)中,每个线程都一个都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

所以当一个线程把主内存中的共享变量读取到自己的本地内存中,然后做了更新。在还没有把共享变量刷新的主内存的时候,另外一个线程是看不到的。 引入volatile关键字,保证了内存的可见性。

由于volatile不能保证原子性,对于count++这类非原子指令的操作来说,其并不能保证线程安全:

public class Main {public 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,t2线程结束,统计此时count的累加结果t1.join();t2.join();System.out.println(count);}
}

运行结果:

//第一次运行结果:
77463
//第二次运行结果:
76841
//第三次运行结果:79114

Volatile和Synchronized的比较:

①.volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以volatile性能更好

②.volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块

③.volatile对任意单个变量的读/写具有原子性,但是类似于count++这种复合操作不具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性

④.多线程访问volatile不会发生阻塞,而synchronized会发生阻塞

⑤.volatile使变量在多线程之间的可见,synchronized保证多线程之间共享资源访问的同步性

参考资料:

Volatile关键字的作用-CSDN博客

线程安全问题(面试常考)-CSDN博客

结语: 写博客不仅仅是为了分享学习经历,同时这也有利于我巩固知识点,总结该知识点,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进。同时也希望读者们不吝啬你们的点赞+收藏+关注,你们的鼓励是我创作的最大动力!

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

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

相关文章

橙芯创想:香橙派AIPRO解锁升腾LLM与Stable Diffusion的创意密码

文章目录 引言 一. 香橙派AI PRO配置以及展示优秀的扩展能力实物展示 二、Ascend-LLM模型部署开机xshell连接香橙派实战运行部署运行结果分析开发版表现 三、Stable Diffusion文生图性能表现 四、体验总结性能噪音便捷性 引言 在科技的浪潮中&#xff0c;一场融合智慧与创意的盛…

SpringBoot+Vue的图书销售网站(前后端分离)

技术栈 Java SpringBoot Maven MySQL mybatis Vue Shiro Element-UI 角色对应功能 网站用户 管理员 项目功能截图

C#中的线性表

什么是线性表 线性表是最简单、最基本、最常用的数据结构。线性表是线性结构的抽象(Abstract),线性结构的特点是结构中的数据元素之间存在一对一的线性关系。这种一对一的关系指的是数据元素之间的位置关系,即:(1)除第一个位置的数据元素外,其它数据元素位置的前面都只有一个数…

Kotlin协程最佳实践

使用合适的作用域&#xff1a; 避免使用GlobalScope&#xff0c;因为它的生命周期是整个应用程序&#xff0c;可能会导致内存泄漏。相反&#xff0c;使用与组件生命周期绑定的CoroutineScope&#xff0c;例如在Android中使用lifecycleScope。 管理协程的生命周期&#xff1a; …

代码随想录训练营【贪心算法篇】

贪心 注&#xff1a;本文代码来自于代码随想录 贪心算法一般分为如下四步&#xff1a; 将问题分解为若干个子问题找出适合的贪心策略求解每一个子问题的最优解将局部最优解堆叠成全局最优解 这个四步其实过于理论化了&#xff0c;我们平时在做贪心类的题目 很难去按照这四步…

深潜数据海洋:Java文件读写全面解析与实战指南

在软件开发的世界里&#xff0c;文件如同沉默的守护者&#xff0c;承载着程序与现实世界的交流。Java语言&#xff0c;以其强大的文件处理能力&#xff0c;为我们提供了丰富的工具箱&#xff0c;让数据的读写变得既优雅又高效。本文将带你从零开始&#xff0c;逐步深入Java文件…

掌握Python中的文件序列化:Json和Pickle模块解析

Python 文件操作与管理&#xff1a;Open函数、Json与Pickle、Os模块 在Python中&#xff0c;文件是一个重要的数据处理对象。无论是读取数据、保存数据还是进行数据处理&#xff0c;文件操作都是Python编程中不可或缺的一部分。本文将详细介绍Python中文件操作的几种常用方法&…

ImportError: cannot import name ‘packaging‘ from ‘pkg_resources‘

降低setuptools版本 pip install setuptools69.5.1https://github.com/aws-neuron/aws-neuron-sdk/issues/893

阿尔泰科技利用485模块搭建自动灌溉系统实现远程控制

自动灌溉系统又叫土壤墒情监控系统&#xff0c;土壤墒情监控系统主要实现固定站无人值守情况下的土壤墒情数据的自动采集和无线传输&#xff0c;数据在监控中心自动接收入库&#xff1b;可以实现24小时连续在线监控并将监控数据通过有线、无线等传输方式实时传输到监控中心生成…

Express+mysql单表分页条件查询

声明&#xff08;自己还没测试过&#xff0c;只提供大概逻辑&#xff0c;什么多表连接查询可以在原基础上添加&#xff09; class /*** param connection Express的mysql数据库链接对象* current 当前页* pageSize 一页显示行数* where [{key:id,operator:,value15}], key查询…

open3d:ransac分割多个平面(源码)

1、背景介绍 随机采样一致性算法(RANSAC Random Sample Consensus)是一种迭代的参数估计算法,主要用于从包含大量噪声数据的样本中估计模型参数。其核心思想是通过随机采样和模型验证来找到数据中最符合模型假设的点。因此,只要事先给定要提取的参数模型,即可从点云中分割…

[rustlings]13_error_handling

errors6 这一个就是在Err(E)中加了点手脚,就是Err(E)中E的类型也是一个Err类型. 这里是创建了一个新的Err类型,Err类型中有两种不同的枚举值.对于不同的枚举值代表两种不同的错误. // Using catch-all error types like Box<dyn Error> isnt recommended for // library…

【HarmonyOS】HarmonyOS NEXT学习日记:四、布局与容器组件

【HarmonyOS】HarmonyOS NEXT学习日记&#xff1a;四、布局与容器组件 学习了基础组件之后&#xff0c;想要利用基础组件组装成一个页面&#xff0c;自然就要开始学习布局相关的知识。我理解的ArkUI的布局分为两个部分 一、组件自身的通用属性&#xff0c;诸如weight、height、…

Linux 下的项目开发:从入门到精通

在 Linux 系统上开发项目是一种常见且高效的实践。Linux 提供了强大的工具和环境&#xff0c;使得开发过程更加流畅。本文将带你了解如何在 Linux 下进行项目开发&#xff0c;从环境搭建到代码管理&#xff0c;再到最终的部署。 一、环境搭建 1.1 安装 Linux 发行版 首先&am…

加密软件有什么用?五款电脑文件加密软件推荐

加密软件对于个人和企业来说至关重要&#xff0c;尤其是在2024年这样一个高度数字化的时代&#xff0c;数据安全变得尤为重要。 数据保护&#xff1a;加密软件可以保护敏感信息不被未经授权的人访问。这包括个人数据、财务记录、健康信息、企业机密等。 防泄漏&#xff1a;防…

HarmonyOS工程目录结构

应用级配置文件app.json5 应用唯一标识、版本号、应用图标、应用名称等信息 模块级配置文件module.json5 oh-package.json5 三方库的管理 其他配置 用于编译构建&#xff0c;包括构建配置文件、编译构建任务脚本、混淆规则文件、依赖的共享包信息等。 build-profile.json…

用Wireshark观察IPsec协议的通信过程

目录 一、配置本地安全策略 二、启动Wireshark&#xff0c;设置过滤器&#xff0c;开始捕获 1. 主模式 2. Quick mode 三、心得体会 1. 碰到的问题和解决办法 2. 心得 一、配置本地安全策略 配置好IPsec如下&#xff1a; 由于在windows server2008安装wireshark失败&…

常见的排序算法,复杂度

稳定 / 非稳定排序&#xff1a;两个相等的数 排序前后 相对位置不变。插入排序&#xff08;希尔排序&#xff09;&#xff1a; 每一趟将一个待排序记录&#xff0c;按其关键字的大小插入到已排好序的一组记录的适当位置上&#xff0c;直到所有待排序记录全部插入为止。稳定&…

Android IjkPlayer内核编译记(一)so库编译使用

转载请注明出处&#xff1a;https://blog.csdn.net/kong_gu_you_lan/article/details/140528831 本文出自 容华谢后的博客 0.写在前面 最近在搞RTMP协议直播拉流的功能&#xff0c;使用了B站开源的IjkPlayer作为播放器内核&#xff0c;在网络不好的情况下延迟会比较高&#xf…

网络安全防御【防火墙双机热备带宽管理综合实验】

目录 一、实验拓扑图 二、实验要求 三、实验思路&#xff1a; 四、实验步骤&#xff1a; 1、FW3的网络相关配置&#xff1a; 2、FW1的新增配置&#xff1a; 3、交换机LSW6&#xff08;总公司&#xff09;的新增配置&#xff1a; 4、双机热备技术配置&#xff08;双机热…