总结线程安全问题的原因和解决方案

news2025/4/17 13:17:23

一. 线程安全问题

概念

首先, 线程安全的意思就是在多线程各种随机调度的情况下, 代码不出现 bug 的情况. 如果在多线程调度的情况下, 出现 bug, 那么就是线程不安全.

二. 观察线程不安全的情况

下面我们用多线程来累加一个数, 观察线程不安全的情况:

用两个线程, 每个线程对 counter 进行5000次自增.预期结果10000.

Class Counter {
    public int count = 0;
    public void increase() {
        count++;
    }
}

public class Demo {
    public static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread( () -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase();
            }
        });

        Thread t2 = new Thread( () -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count : " + counter.count);
    }
}

那我们再来看运行结果:

 

 根据截图的结果我们可以看到, 每一次运行结果, count 的值都不一样, 反正不是10000.

那么为什么会出现这种问题呢?

首先我们要知道进行的 count++ 的操作, 底层 是三条指令在CPU上完成的.

  1. load -> 把内存的数据读取到 CPU 寄存器上
  2. add -> 把 CPU 中寄存器上的值进行 +1
  3. save -> 把寄存器中的值, 写回到内存中

因为当前是两个线程一起修改一个变量(修改共享数据), 每次的修改是三个步骤(不是原子的), 并且线程之间的调度顺序的不确定的.

什么是原子性?

我们设想一个场景, 大家在厕所方便的时候, 假设, A 这个人进去了之后还没出来, 而如果A没有锁门的话, 那么B是不是也可以进去呢. 这显然是不行的, (而这个锁门就是需要一定的机制来保证安全). 这里A就是不具备原子性的. 而 A 要是把门锁好, 那么B就进不去了, 这样就保证了原子性.

有的时候也把这个现象叫做同步互斥.

因此, 两个线程在真正执行这些操作的时候, 就有可能会有很多种执行的排列顺序.下面来看图:

 按照时间轴的形式画图, 在内存中真实存在的情况不止这几种, 而在上图的排列方式中, 没有问题的只有下面两种情况:

 表面上是并发执行, 其实差不多是串行执行了.

以下图为例, 这个时候多线程自增就会产生 "线程安全问题"!!!

 假设两个线程在两个CPU核心上运行:

按照上图的时间轴在内存中自增的话, 就发生问题了:

开始时 t1 线程先进行 load 操作, 

 然后 t2 线程进行 load 操作, 再 add, 然后再内存中 count++

 这个时候, t1线程也进行 add 和 save, 这个时候就出问题了, 看图:

 类似于这种情况, 就会出现线程安全问题了. 因此再最开始我们进行自增操作的时候, 得到的值不是10000.

三. 线程不安全的原因

1. 抢占式执行

线程不安全的罪魁祸首

我们都知道多线程的调度执行过程是随机的, 这是内核实现的, 咱们无能为力, 我们能做到的就是, 在写多线程代码的时候, 需要考虑到的就是, 在任意的调度情况情况下, 咱们的代码都能运行出正确的结果.

2. 多个线程修改同一个变量

有时可以从这里入手, 来规避线程安全问题, 但是普适性不高

注意我加颜色的汉字,  一个线程修改一个变量没事, 多个线程修改同一个变量没事, 多个线程修改不同变量还是没事, 但只要多个线程修改同一个变量的话, 问题就出来了. 

3. 修改操作不是原子的

解决线程安全问题, 最常见的办法, 就是从这入手

像上面 count++ 操作, 本质上是三条指令: load add save 

CPU 执行指令, 都是以 "一个指令" 为单位进行执行的, 一个指令就相当于 CPU 上的最小单位, 不会发生指令执行到一半, 线程被调度走了的情况. 

4. 内存可见性问题

也会产生线程不安全问题

这是 JVM 的代码优化引出的 BUG

编译器优化:

程序猿写代码写好之后在机器上运行, 因为程序猿水平参差不齐, 大佬们的代码正确又高效, 而我这样的菜鸟写代码经常出BUG效率还低, 这个时候, 写编译器的大佬, 就想办法, 让编译器有了一定的优化能力, 就是编译器把你代码里面的逻辑等价转换为另一种逻辑, 转换之后逻辑不变, 但是效率变高了.

举个栗子:

假设我老妈让我去超市里面买上图中的四样东西,中午做饭用,  如果我按照列表上的顺序1>2>3>4去买东西的话, 那么我就会东跑跑西跑跑, 而我要是从超市入口进去, 然后按着箭头的方向去买的话, 那么我就会少走一些路, 结果是一样的, 效率提升了, 这种过程就是编译器优化.

 

5. 指令重排序

也会引发线程不安全

假设我们这里有这样三行代码,  这是指令重排序前的情况:

在前面我就说有三种指令 LOAD ADD SAVE, 而发生指令重排序的话, 

 在这个过程中, 就可能发生线程不安全问题.

四. 解决线程不安全问题

还是继续来看开始的代码. 顺着三中的各种引发线程不安全的原因来看, 首先 抢占式执行 这个我们无能为力, 再看 多个线程修改同一个变量 也没有关系, 接下来就是原子性的问题了, 所以这里的解决办法就是用一些办法, 来使得 count++ 操作变成原子的.

加锁

而要使操作变得原子呢, 我们需要做的就是加锁.

举个例子, 大家回家了之后肯定要关门吧, 把门关上外人就进不来, 这就是加锁, 然后等你有事情要出门了, 你在把门打开出去, 这就是解锁. 在这个过程中, 外人进不去也就是互斥

解决上述线程不安全问题, 类似这样, 在 count++ 之前加锁, 等 count++ 完之后解锁. 在加锁和解锁这个时间里, 别的线程想要修改是不行的, 修改不了的, 只能阻塞等待, 这里阻塞等待的线程状态就是 BLOCKED.

synchronized

1.对方法加锁

在 Java 中, 进行加锁, 使用 synchronized 关键字

把 synchronized 加到increase()方法上,  这是最基本的使用, 使用 synchronized 来修饰一个普通方法, 当进入方法的时候会加锁, 方法执行完之后, 就解锁.

我们来看加上 synchronized 之后的代码执行效果:

这个时候我们的count结果就是10000了.

下面来看下具体这个锁是怎么执行的呢, 他的内部是怎么工作的呢?

加锁之后, 就是在线程前后多了两个操作, LOCK 和 UNCLOCK. 

假设 t1 线程先加锁, 那么 t2 线程就会出现阻塞, 这个时候和串行执行没啥区别了.

本来线程调度是随机的过程, 容易出现线程安全问题, 现在使用锁, 就使得线程能串行执行了.

加锁前我们要想好锁哪段代码, 锁的范围不一样, 对于代码的执行效果影响差距很大, 锁的代码越多, 我们称之为 "锁的粒度越大", 锁的代码越少, 称之为 "锁的粒度越小". 

下面我们试试一个线程加锁, 一个线程不加锁, 我们来看看是否线程安全?

class Counter {
    public int count = 0;
    public synchronized void increase() {
        count++;
    }
    
    public void increase2() {
        count++;
    }
}

public class Demo3 {
    public static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread( () -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase(); 
            }
        });

        Thread t2 = new Thread( () -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase2();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("count : " + counter.count);
    }
}

我们的代码变成这样, 让 t1 线程加锁, t2 线程不加锁.我们直接看运行结果:

可以看到结果不是10000, 出问题了.

这就说明, 只给一个线程加锁是没啥用的, 一个线程加锁的话, 不涉及锁竞争, 也就不会发生阻塞等待, 也就不会 并发修改->串行修改 

就好比, A 这个人追到了女神, 但是出来一个 B , 不讲武德, 他挖墙脚, 但要是 A 官宣了, 那么就相当于加锁了, 就是安全的, B 就需要阻塞等待.

2. 修饰代码块

也就是把需要加锁的逻辑放到 synchronized 修饰的代码块中, 也可以起到加锁的作用.

 其中 (this) 被我们称为锁对象, 就是针对当前对象加锁, 谁调用这个方法就对谁加锁.

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

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

相关文章

树--堆和优先权队列

数据结构中的堆和栈与操作系统内存划分中的堆和栈没有关系 一、堆的定义 一个大小为n的堆是一棵包含n个结点的完全二叉树&#xff0c;其根节点称为堆顶。 根据堆中亲子结点的大小关系&#xff0c;分为大堆和小堆&#xff1a; &#xff08;1&#xff09;最小堆&#xff1a;树中…

高斯原型网络原论文高质量翻译

论文地址&#xff1a;Gaussian Prototypical Networks for Few-Shot Learning on Omniglot 文章目录摘要1 引言1.1 Few-shot learning1.2 高斯原型网络2 相关工作3 方法3.1 编码器3.2 偶发性训练3.3 定义一个类3.4 评估模型4 数据集5 实验5.1 协方差估计的用法6 结论摘要 我们…

万字长文解读计算机视觉中的注意力机制(附论文和代码链接)

文中论文和代码已经整理&#xff0c;如果需要&#xff0c;点击下方公众号关注&#xff0c;领取&#xff0c;持续传达瓜货 所向披靡的张大刀 注意力机制是机器学习中嵌入的一个网络结构&#xff0c;主要用来学习输入数据对输出数据贡献&#xff1b;注意力机制在NLP和CV中均有使…

目标检测算法——YOLOv5/YOLOv7改进之结合无参注意力SimAM

目录 &#xff08;一&#xff09;前言介绍 1.摘要 2.不同注意力步骤比较 &#xff08;二&#xff09;相关实验 &#xff08;三&#xff09;YOLOv5结合无参注意力SimAM 1.配置.yaml文件 2.配置common.py 3.修改yolo.py SimAM&#xff1a;无参数Attention助力分类/检测/分…

点云应用——三维空间边界点排序+机器人轨迹引导(1)

三维空间边界点排序机器人轨迹引导一、前言二、思路一&#xff1a;利用重建思路三、思路二&#xff1a;利用PCL边界提取方法三维空间点排序四、后续一、前言 最近做了点云边界提取、并实时发送至机器人进行引导的研究&#xff0c;主要遇到了两个问题&#xff1a; 1&#xff09;…

Python画爱心——一颗会跳动的爱心~

节日就快到来了&#xff0c;给你的Ta一个惊喜吧~ 今天给大家分享一个浪漫小技巧&#xff0c;利用Python制作一个立体会动的心动小爱心 成千上百个爱心汇成一个大爱心&#xff0c;从里到外形成一个立体状&#xff0c;给人视觉上的冲击感&#xff01;浪漫极了↓ 微信扫码添加&a…

CUDA 从入门到放弃(一)

CUDA从入门到放弃系列包含内容 异构并行计算CUDA编程模型CUDA执行模型CUDA内存CUDA流和并发CUDA指令级原语GPU加速库多GPU编程 本文你将了解到 异构并行计算CUDA编程模型 温馨提示: 本文可能引发C/C零基础的读者不适,请谨慎观看. 一、聊聊异构并行计算 异构并行计算的本质是…

MQTT Broker mosquito配置以及使用tls证书登录附上Python调用代码

MQTT Broker mosquito配置以及使用tls证书登录 文章目录MQTT Broker mosquito配置以及使用tls证书登录1. 前言2. 安装3. mosquito相关命令3.1 运行停止查看状态3.2 创建可以登录mosquito的用户3.3 配置权限规则文件3.4 配置mosquito3.5 使用mosquito_pub和mosquito_sub测试3.5.…

web手势库Alloyfinger

前言 在上一篇文章 前端pdf预览、pdfjs的使用&#xff0c;我们使用pdf.js 来实现了pdf的预览。但是客户车间里的电脑是触摸屏&#xff0c;要求能够手势放大图纸&#xff0c;能够拖动图纸。最终决定使用 Alloyfinger 来解决手势的问题。 官方github https://github.com/Alloy…

计算机网络(一):计算机网络概念、功能、组成

计算机网络的概念 计算机网络&#xff1a;是一个将分散的、具有独立功能的计算机系统&#xff0c;通过通信设备与线路连接起来&#xff0c;由功能完善的软件实现资源共享和信息传递的系统 计算机网络是互连的、自治的计算机集合。 互连&#xff1a;互联互通 自治&#xff1a;无…

前端面经 前端优化

前端面经 前端优化 文章目录前端面经 前端优化HTTP/2 Web优化最佳实践DNS与解析如何使用CDN分发缓存策略页面渲染优化避免CSS、JS阻塞CSS阻塞JS的阻塞改变JS阻塞的方式使用字体图标iconfont代替图片图标降低CSS选择器的复杂性减少重绘和回流如何避免图片资源优化Webpack优化降低…

【Vue】VueCLI 的使用和单文件组件(1)

学习内容&#xff1a; 1&#xff09;了解一下 Vue 的脚手架工具&#xff1b; 2) 认识一下 Vue 里面的单文件组件。‍‍ 在真正开发大型项目的时候&#xff0c;实际上我们并不能通过一个这样的index点html的方式去编写一个比较大型的项目&#xff0c;‍‍ 所以我们要学习使用 V…

JavaScript 69 JavaScript Web API 69.3 Web History API

JavaScript 文章目录JavaScript69 JavaScript Web API69.3 Web History API69.3.1 History back() 方法69.3.2 History go() 方法69.3.3 History 对象属性69.3.4 History 对象方法69 JavaScript Web API 69.3 Web History API Web History API 提供了访问 windows.history 对…

Linux进程与操作系统详解

文章目录一&#xff1a;冯诺依曼体系二&#xff1a;操作系统三&#xff1a;进程和PCB四&#xff1a;进程相关的指令五&#xff1a;getpid和getppid系统调用六&#xff1a;fork系统调用七&#xff1a;进程状态八&#xff1a;Linux下进程状态大全8.1&#xff1a;R(running)8.2&am…

Hive3 - HiveSQL 特征及操作

一、HiveSQL特征 Hive SQL&#xff08;HQL&#xff09;与SQL的语法大同小异&#xff0c;基本上是相通的&#xff0c;对SQL掌握的可以非常快的上手使用Hive SQL。不过在一些细节上需要特别注意Hive自己特有的语法知识点&#xff0c;下面分别进行介绍。 1. 字段数据类型 Hive数…

【附源码】Python计算机毕业设计烹饪课程预约系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

电脑重装系统c盘如何备份资料

现在越来越多的用户都喜欢重装自己所喜欢的电脑系统&#xff0c;但需要在重新安装系统之前备份自己的电脑。因此有很多用户问重装系统c盘如何备份的文件。今天小编就教下大家重装系统c盘如何备份的教程。 工具/原料&#xff1a; 系统版本&#xff1a;windows7系统 品牌型号&…

多目标应用:非支配排序的鲸鱼优化算法NSWOA优化RBF神经网络实现数据预测(RBF隐藏层神经元个数可以自行设定)

一、非支配排序的鲸鱼优化算法 非支配排序的鲸鱼优化算法&#xff08;Non-Dominated Sorting Whale Optimization Algorithm&#xff0c;NSWOA&#xff09;由Pradeep Jangir和 Narottam Jangir于2017年提出。 NSWOA算法流程如下&#xff1a; 二、RBF神经网络 1988年&#xf…

Spring-Aop面向切面编程

文章目录一、简介1、作用2、AOP核心概念3、五种&#xff08;增强&#xff09;通知类型二、AOP入门小案例&#xff08;注解版&#xff09;1.导入坐标(pom.xml)2.制作连接点(原始操作&#xff0c;Dao接口与实现类)3:定义通知类和通知4:定义切入点5:制作切面6:将通知类配给容器并标…

Linux操作系统

Linux 基础入门 Linux操作系统 操作系统 首先&#xff0c;我们需要知道什么是操作系统&#xff1a; 操作系统是应用程序运行的基础支撑环境操作系统作用是管理与控制计算机系统硬件与软件资源&#xff08;软件与硬件的中间层&#xff09;Intel X86 架构上的常见的操作系统&…