如何解决线程安全问题(synchronized、原子性、产生线程不安全的原因,锁的特性,加锁的方式等等干货)

文章目录

  • 💐线程不安全的示例
  • 💐锁的特性
  • 💐产生线程不安全的原因:
  • 💐加锁的三种方式

💐线程不安全的示例

对于线程安全问题,这里用一个例子进行讲解👇:

我现在定义一个变量count, 使用两个线程对这个count进行自增,每一个线程中,count都自增10000, 那么,两个线程执行结束之后,最后的count应该是20000,下面验证一下:

public class Demo1 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for(int i = 0; i < 10000; i++) {//count自增10000次count++;}});Thread thread2 = new Thread(() -> {for(int i = 0; i < 10000; i++){//count自增10000次count++;}});thread1.start();thread2.start();//让主线程阻塞等待thread1.join();thread2.join();//两个线程执行完毕后,打印最后结果System.out.println("count:"+count);}
}

第一次打印:

在这里插入图片描述

第二次打印:

在这里插入图片描述

第三次打印:

在这里插入图片描述

通过以上代码的得到,最后的结果与我们预期的结果完全不一样,而且,每次运行都会产生不一样的结果;以上代码如果放在一个线程中去运行,那是没有任何问题的,放在多线程中就出现了问题,以上情况就是一个非常典型的线程安全问题;

那么我们修改一下代码:

将thread1的join放在thread1的start后面,最后运行的结果就是对的!!!

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个 “同时执行” 是很关键的,既然同时执行时会产生线程不安全问题,那么有没有办法解决这样的问题呢?答案是:有的

产生问题的愿意:

首先,count++ 这个操作,站在CPU的角度上是由三步操作完成的:

  1. load操作,把数据从内存中拿出来放到寄存器中

  2. add操作,把count的值进行+1

  3. save操作,把寄存器中的值放到内存中

以两个线程为例,因为线程是随机调度执行的,所以这三步操作就有以下的操作顺序:

在这里插入图片描述

上述,第一和第二种情况产生的结果就是正确的,下面通过画图演示一下以上这三步在CPU核心上的操作步骤,观察以下count是如何完成自增操作的;

假设,thread1 和 thread2这两个线程是在两个CPU核心上执行的,当然,在一个CPU核心上也是没有问题的,因为会有对应的上下文进行记录,为了好理解,下面我们假设在两个CPU核心上:

在这里插入图片描述

交叉执行的情况就会产生bug👇

在这里插入图片描述

由于在这20000次中,我们不知道有多少次是按照正确的方式自增的,又有多少次按照错误的方式自增的,所以最后就会产生一个小于20000的随机值;对于上述情况,就可以使用 **“加锁操作“**👇

💐锁的特性

在Java中,对线程加锁主要就是使用 synchronized

synchronized在使用时,需要修饰一个代码块,例如以下:

在这里插入图片描述

对上述自增例子的代码进行加锁:

public class Demo1 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object obj = new Object();//创建一个对象作为锁对象Thread thread1 = new Thread(() -> {synchronized (obj) {//对线程进行加锁for(int i = 0; i < 10000; i++) {count++;}}});Thread thread2 = new Thread(() -> {synchronized (obj) {for(int i = 0; i < 10000; i++){//count自增10000次count++;}}});//此时两个线程同时执行,就会同时获取锁对象,就会产生竞争thread1.start();thread2.start();//两个线程执行完毕后,打印最后结果System.out.println("count:"+count);}
}

在这里插入图片描述

锁的特性主要分为两点:

(1)互斥特性

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

一个线程在加锁情况下,另一个线程也想要进行加锁,这样就会产生“锁冲突/锁竞争”,获得锁的线程就会继续执行,没有获得锁的线程就会阻塞等待,直到得到锁的线程释放锁为止;

如何理解阻塞等待?

例如这样一个场景,我们在排队上厕所时,当张三进入厕所后,就会将们锁住,这时候,李四和王五就只能在原地等待,等到张三出来以后,李四和王五才能去上厕所;

注意:

1.张三出厕所以后,李四和王五并不会按照顺序上厕所,而是会进行一个抢厕所,这就相当于,虽然现在有多个线程,但是在获取锁时,并不会按照先来后到的顺序获取,而是会进行争抢

2.上一个线程释放锁之后,下一个线程并不是直接就可以获取锁,而是需要操作系统进行一个“唤醒”的过程,这个过程是操作系统线程调度的工作部分

(2)刷新内存

synchronized的工作过程:

1.尝试获得锁

2.从主内存拷贝变量最新内容到工作内存

3.执行代码

4.将更改后的变量刷新到主内存中

5.释放锁

所以,synchronized也可以保证内存可见性

💐产生线程不安全的原因:

1.线程的调度顺序是随机的,是抢占式执行的

2.两个线程针同一个变量进行修改

1.两个线程针对两个不同的变量进行修改

2.一个线程针对一个变量进行修改

3.两个线程针对同一个线程进行读取

以上改正后的三点就不会发生不安全问题;

3.修改操作不是原子的

什么是原子性:就像最开始讲的 load,add,save 指令,它们是三个指令,需要一个一个执行,所以它们不是原子的,如果它们三个指令发生的动作能够一个指令就完成,或者说在它们三个指令执行的过程中,不会被其他的指令进行穿插,这就是原子的。

4.内存可见性问题

5.指令重排序问题

那么,如何将不安全改成安全的呢?

首先,第一条原因是没办法修改的,因为线程的抢占式执行是由操作系统决定的,我们是无法修改的,第二条原因的话,针对有些代码,可能可以经过代码的调整来避免,但是,有些代码可能无法调整,就比如我们写的这个,我们的目的就是让count在两个线程中进行自增,所以第二条原因需要针对业务的情况,第三条原因就可以进行修改,将不是原子的操作改为原子的,通过加锁就可以解决上述非原子性问题,内存可见性和指令重排序问题也可以通过 Volaite 来解决。

💐加锁的三种方式

1.synchronozed修饰代码块👇

  Object obj = new Object();Thread thread1 = new Thread(() -> {synchronized (obj) {//对代码进行加锁for(int i = 0; i < 10000; i++) {count++;}}});

**2.修饰实例方法👇:**哪个对象调用increase,哪个就是 “锁对象”

class Counter{public static int count;//谁调用这个方法,谁就是锁对象//写法一:synchronized public void increase() {count++;}//写法二:public void increse() {synchronized(this) {count++;}}
}
public class Demo1 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(() -> {for(int i = 0; i < 10000; i++) {counter.increase();}});thread1.start();System.out.println(Counter.count);}
}

在这里插入图片描述

3.修饰静态方法👇

如果是修饰静态方法,相当于是对“类对象”进行了加锁;

class Counter{public static int count;//写法一:synchronized public static void increase1() {count++;}//写法二public static void increase2() {//对类对象加锁synchronized (Counter.class) {count++;}}
}

在这里插入图片描述

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

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

相关文章

CTFHUB 命令执行

命令执行 原理&#xff1a; 在编写程序的时候&#xff0c;当碰到要执行系统命令来获取一些信息时&#xff0c;就要调用外部命令的函数&#xff0c;比如php中的exec()、system()等&#xff0c;如果这些函数的参数是由用户所提供的&#xff0c;那么恶意用户就可能通过构造命令拼…

MogaNet实战:使用MogaNet实现图像分类任务(一)

文章目录 摘要安装包安装timm 数据增强Cutout和MixupEMA项目结构计算mean和std生成数据集 摘要 论文&#xff1a;https://arxiv.org/pdf/2211.03295.pdf 作者多阶博弈论交互这一全新视角探索了现代卷积神经网络的表示能力。这种交互反映了不同尺度上下文中变量间的相互作用效…

微信小程序云开发教程——墨刀原型工具入门(编辑页面)

引言 作为一个小白&#xff0c;小北要怎么在短时间内快速学会微信小程序原型设计&#xff1f; “时间紧&#xff0c;任务重”&#xff0c;这意味着学习时必须把握微信小程序原型设计中的重点、难点&#xff0c;而非面面俱到。 要在短时间内理解、掌握一个工具的使用&#xf…

Django 管网项目 三

Django 官网文档 ​​Writing your first Django app, part 2 | Django documentation | Django 本文内容涉及创建视图 View&#xff0c;路由&#xff0c;和模版。并对内容进行渲染。 创建视图 在我们的投票应用中&#xff0c;我们需要下列几个视图&#xff1a; 问题索引页—…

阶跃信号与冲击信号

奇异信号&#xff1a;信号与系统分析中&#xff0c;经常遇到函数本身有不连续点&#xff08;跳变电&#xff09;或其导函数与积分有不连续点的情况&#xff0c;这类函数称为奇异函数或奇异信号&#xff0c;也称之为突变信号。以下为一些常见奇异函数。 奇异信号 单位斜变信号 …

ABAP - SALV教程07 斑马纹显示和SALV标题

SALV设置斑马纹和标题 METHOD set_layout.DATA: lo_display TYPE REF TO cl_salv_display_settings. * 取得显示对象lo_display co_alv->get_display_settings( ).* 设置ZEBRA显示lo_display->set_striped_pattern( X ). * 设置Titlelo_display->set_list_he…

Linux网络字节序/IP地址转换

网络字节序&#xff1a; 大&#xff08;高&#xff09;端字节序&#xff08;网络字节序&#xff09;&#xff1a;低位地址存放高位数据&#xff0c;高位地址存放低位数据 小&#xff08;低&#xff09;端字节序&#xff08;主机字节序&#xff09;&#xff1a;低位地址存放低…

【OpenCV基础(三)】Ubuntu系统下EasyPR环境配置

环境配置 1、资源下载2、环境配置2.1、1、将EasyPR压缩包拷贝到Ubuntu 三种方法任选一种2.2、解压得到EasyPR文件夹(文件夹一层进入后EasyPR资源内容)2.3、终端命令修改权限**chmod -R 777 ./ EasyPR**2.4、查找EasyPR/include/easypr/config.h&#xff0c;使用gedit方式打开2.…

【yolov8部署实战】VS2019环境下使用C++和OpenCV环境部署yolo项目|含详细注释源码

一、前言 之前一阵子一直在做的就是怎么把yolo项目部署成c项目&#xff0c;因为项目需要嵌套进yolo模型跑算法。因为自己也是本科生小白一枚&#xff0c;基本上对这方面没有涉猎过&#xff0c;自己一个人从网上到处搜寻资料&#xff0c;写代码&#xff0c;调试&#xff0c;期间…

【机器学习】CIFAR-10数据集简介、下载方法(自动)

【机器学习】CIFAR-10数据集简介、下载方法(自动) &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望得到您的订阅和支…

SDN和SD-WAN的使用场景分别是什么?

软件定义网络&#xff08;SDN&#xff09;和软件定义广域网&#xff08;SD-WAN&#xff09;是两种创新的网络技术&#xff0c;它们在不同的使用场景中发挥着重要作用。本文将深入探讨SDN和SD-WAN 的使用场景&#xff0c;并比较它们的应用范围。 SDN的使用场景&#xff1a; 数据…

第四十八回 解珍解宝双越狱 孙立孙新大劫牢-Python模块和包概念与使用

吴用对宋江说&#xff0c;有个人&#xff0c;他是石勇的关系&#xff0c;与祝家庄的峦廷玉关系好&#xff0c;还是杨林、邓飞的老相识&#xff0c;他有一计.... 原来在宋江攻打祝家庄的时间段&#xff0c;山东海边登州也发生了一件事。登州山下有一家猎户&#xff0c;弟兄两个…