线程安全问题与解决方法~

本文内容仅供对线程安全问题、锁的认识和使用等,进行一个介绍。适合小白的文章!

目录

一、线程安全问题

1.什么是线程安全问题

2.解释上述安全问题

3.线程安全的五大原因

二、使用锁解决线程安全问题

1.介绍锁

2.加锁操作


一、线程安全问题

在多线程代码实现中,最重要最核心的部分也就是线程安全问题,非常值得我们去认识和理解。线程安全问题及其影响程序运行的结果,也就是出现的bug。

1.什么是线程安全问题

1.1.啥样的称为安全问题

(1)在程序运行的结果中,只要结果有一点点和预期不一样,那就是该程序有bug,不算合格程序。

(2)在多线程代码中产生的bug,我们就称为“线程安全问题”。

1.2.为啥产生线程安全问题

(1)多线程在执行的时候,是“抢占式”的,“随机调度”的,进而很容易产生一系列的线程安全问题。

(2)如果不对代码进行限制,很容易产生和预期不一样的结果。

1.3.一个线程不安全的多线程例子

代码描述:分别两个线程t1、t2,同时对count++五万次,最后输出count的值

 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.join();
        t2.join();
        System.out.println("count="+count);
    }

程序预期结果:count=100000

实际运行结果:

我去?这么离谱,跟100000差了这么多,到底是道德的沦丧,还是人格的扭曲?不不不,都不是,这是多线程产生的安全问题。

下面我们通过执行指令的三个过程来解析上述代码所出现的线程安全问题。

2.解释上述安全问题

解释上面代码出现的线程安全问题,我们需要使用到前面介绍到的,cpu是如何执行指令的。像上述出现的问题,可以直接定位到count++这个指令上面。下面解析:

2.1.count++代码背后的指令

(1)count++这一句代码,在cpu上其实是三条指令,相当于分成三步去执行。

(2)执行count++代码需要三步

1)把内存count中的数值,读取到cpu寄存器中(这一步,我们暂且使用load这样的名字来代替)

2)把寄存器中的值+1(暂且称为add)

3)最后把寄存器上述计算后的值,写回到内存中的count中,也就是更新结果(暂且称为save)

以上就是执行count++这一句代码,在cpu上面发生的大致过程,现在把他抽象出来。

2.2.指令执行过程

(1)按理来说,正确执行顺序应该是这样,或者 t1:4 - 5 - 6 、t2:1 - 2 - 3

但是由于多线程是随机调度、抢占式执行的,这样的原因就会导致在cpu上执行t1线程的某一条指令时,t1线程随时会被从cpu上调离而走,进而执行t2线程。所以,在多线程代码中,指令的执行顺序是不确定的,随机的。

(2)不正常的执行顺序

像不正常的指令执行顺序有无数种,但是正确的执行顺序只有两种。

为什么不正常的指令执行顺序就会产生线程安全问题呢?请听下文解析。

2.3.指令所产生的问题

有请我们的凹凸曼同志

那么我们现在是两个线程同时执行+1操作,比如:t1将count+1后变成2并更新到内存中,此时t2线程也操作后了,随机也将内存中的count更新成2,这样的操作就会产生问题了。也就是所谓的执行覆盖问题,在多线程中很常见。

当然,上述的指令执行也不一定是这样;总之,是因为指令的执行顺序不一样,导致产生的结果会被覆盖,进而导致结果和预期不一样。

像上面的指令执行顺序,只要是一个线程的load指令执行顺序在另一个线程的save指令执行顺序后面,就是线程安全的。

下面罗列五大线程不安全的原因

3.线程安全的五大原因

3.1.线程在系统中随机调度,是抢占式执行的

这是出现线程安全问题最本质的原因,但是这种原因,我们是无法修改和干预的

3.2.在代码中,存在多个线程同时修改同一个变量

(1)一个线程修改同一个变量,不存在线程安全问题

(2)多个线程读取同一个变量,不存在线程安全问题

(3)多个线程修改不同的变量,不存在线程安全问题

(4)多个线程修改同一个变量,存在线程安全问题(*)

3.3.线程针对变量的操作,不是“原子性”的

(1)像上述的指令执行过程,就不是一个原子性的

(2)要想将上述三个指令打包成一个原子,则需要进行加锁操作,也就是本节课所需要介绍的内容。

(3)不一定所有的代码语句都不是原子性的,例如赋值操作,就是一个原子性的操作。

3.4.内存可见性问题

此类问题是由于jvm的优化而产生的问题,后续文章介绍


3.5.指令重排序

此类问题也是由于编译器而产生的问题,后续文章介绍

在这里,我们介绍加锁操作,也就是针对第三条原因而进行的措施。

二、使用锁解决线程安全问题

这里的操作适用于第三个原因,其他的线程安全问题,不一定会适用。

1.介绍锁

1.1.锁的介绍内容

(1)在这里,会介绍锁操作的两个方面----加锁和解锁

(2)这里是利用锁的一个性质,当一个线程对一个箱子加锁之后,另一个线程再想对其加锁,就会产生阻塞,也就是排斥的效果。

(3)例如:当t1线程对A加锁之后,t2线程再对A加锁,t2线程就会阻塞等待(也就不会干扰到t1线程执行指令);当t1线程对A解锁之后,t2线程才有机会拿到加锁的机会,也就是加锁成功。

1.2.对锁的举例

(1)两个线程,我们比如成两个人。对象,我们比如成厕所。

(2)当两个人争取一个厕所时,一个人先进入了厕所并进行加锁操作,此时另一个就需要阻塞等待。

(3)如果两个人争取不同的厕所,则不会产生任何的阻塞等待。

2.加锁操作

注意:两个线程对同一个对象加锁,才能起到作用,否则和没有一样 

2.1.锁的语法

(1)锁的关键字:synchronized

(2)加锁的前提是:有对象。对象就相当于是一个物品,任何对象都可以被加锁

(3)基本加锁结构

出了锁范围自动解锁 

2.2.关于锁和对象的注意实现(重点)

(1)锁的用途是让多个线程之间产生阻塞

(2)要想某几个线程有序的执行,那么,这几个线程必须对同一个对象进行加锁操作,否则就是加了一个寂寞

(3)当两个线程对同一个对象加锁时,就会产生锁竞争/锁冲突/互斥的效果,进而会引起线程阻塞,也就是前面所提到的线程BLOCKED状态

(4)至于加锁的对象,对象是什么类型、和该对象有什么变量和方法,没有任何关系;对一个对象加锁,不会对该对象产生任何的效果和影响。

2.3.对上面代码进行加锁操作

(1)代码: 

public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        String s = "锁对象的外貌没有任何影响";
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (s) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (s) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }

运行结果:

此时: 

这样的操作之后,count++操作就会被打包一个操作,也就是原子性,此时三条执行的执行顺序是一体的,也就是不会再产生上述的线程不安全问题了。

(2)加锁后的注意点

1)两个线程对同一个对象加锁之后,当t1先拿到锁,也就是先执行代码,即使t1线程执行到一半,被cpu调度走;此时t2线程也无法进行拿到锁。

2)加锁后,count++语句是串行执行的,而for循环语句是并行执行。

3)当t1释放锁之后,t1线程和t2线程还是会同时争夺这把锁,也就是说他们的拿到锁的顺序也是不确定的。

2.4.锁的几处场景

上面的是直接对一个对象引用进行加锁,也就是对普通语句加锁。下面总结几种

(1)对普通语句加锁

(2)对方法加锁

 普通方法加锁

或者:

如果,锁的声明周期需要跟方法的声明周期一样,那么锁就加在外面。对普通方法加锁时,只有当两个线程同时调用到该方法时,才会产生阻塞,也就是起到作用。也就是只对this加锁,调用方法才会加锁

对静态方法加锁:

或者:A.class是拿到了A这个类的对象,也就是类对象

对静态方法加锁后,针对的是这个类对象,也就是说,多个线程同时实例化类对象,就会产生阻塞。

2.5.关于锁的小结

(1)锁的两个操作:加锁和解锁

(2)锁的特点:互斥

(3)两个线程对同一个对象加锁,就可能产生:阻塞/锁竞争/锁冲突;如果不是,就不会产生阻塞

(4)对普通方法加锁,相当于对this加锁;对静态方法加锁,相当于对 类对象 加锁

(5)一张个人对锁理解的草稿图


本文结束,线程安全问题的解决方式完全不止以上锁描述的,对锁的描述也不止上述所介绍的。上面的内容仅供入门。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【吊打面试官系列】Redis篇 - 使用过 Redis 分布式锁么,它是什么回事?

大家好&#xff0c;我是锋哥。今天分享关于 【使用过 Redis 分布式锁么&#xff0c;它是什么回事&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 使用过 Redis 分布式锁么&#xff0c;它是什么回事&#xff1f; 先拿 setnx 来争抢锁&#xff0c;抢到之后&#…

C语言中的字符与字符串:魔法般的函数探险

前言 在C语言的世界里&#xff0c;字符和字符串是两个不可或缺的元素&#xff0c;它们像是魔法般的存在&#xff0c;让文字与代码交织出无限可能。而在这个世界里&#xff0c;有一批特殊的函数&#xff0c;它们如同探险家&#xff0c;引领我们深入字符与字符串的秘境&#xff0…

阿里云租用GPU服务器多少钱?

阿里云GPU服务器租用价格表包括包年包月价格、一个小时收费以及学生GPU服务器租用费用&#xff0c;阿里云GPU计算卡包括NVIDIA V100计算卡、T4计算卡、A10计算卡和A100计算卡&#xff0c;GPU云服务器gn6i可享受3折优惠&#xff0c;阿里云服务器网aliyunfuwuqi.com分享阿里云GPU…

【51单片机入门记录】A/D、D/A转换器PCF859应用

目录 一、IIC初始化代码 二、开发板电路图 三、PCF8591读/写字节操作流程及相关函数 &#xff08;1&#xff09;PCF8591&#xff08;AD&#xff09;读操作流程及代码 &#xff08;2&#xff09;PCF8591&#xff08;AD&#xff09;写操作流程及代码 四、应用示例-显示电压…

微信小程序 电影院售票选座票务系统5w7l6

uni-app框架&#xff1a;使用Vue.js开发跨平台应用的前端框架&#xff0c;编写一套代码&#xff0c;可编译到Android、小程序等平台。 框架支持:springboot/Ssm/thinkphp/django/flask/express均支持 前端开发:vue.js 可选语言&#xff1a;pythonjavanode.jsphp均支持 运行软件…

JS继承与原型、原型链

在 JavaScript 中&#xff0c;继承是实现代码复用和构建对象关系的重要概念。本文将讨论原型链继承、构造函数继承以及组合继承等几种常见的继承方式&#xff0c;并提供相应的示例代码&#xff0c;并分析它们的特点、优缺点以及适用场景。 在开始讲解 JavaScript 的继承方式之…

RDD算子(四)、血缘关系、持久化

1. foreach 分布式遍历每一个元素&#xff0c;调用指定函数 val rdd sc.makeRDD(List(1, 2, 3, 4)) rdd.foreach(println) 结果是随机的&#xff0c;因为foreach是在每一个Executor端并发执行&#xff0c;所以顺序是不确定的。如果采集collect之后再调用foreach打印&#xf…

ADB(Android Debug Bridge)操作命令详解及示例

ADB&#xff08;Android Debug Bridge&#xff09;是一个强大的命令行工具&#xff0c;它是Android SDK的一部分&#xff0c;主要用于Android设备&#xff08;包括真实手机和平板电脑以及模拟器&#xff09;的调试、系统控制和应用程序部署。 下面是一些ADB的常用命令&#xff…

全面解析找不到msvcr110.dll,无法继续执行代码的解决方法

MSVCR110.dll的丢失可能导致某些应用程序无法启动。当用户试图打开依赖于该特定版本DLL文件的软件时&#xff0c;可能会遭遇“找不到指定模块”的错误提示&#xff0c;使得程序启动进程戛然而止。这种突如其来的故障不仅打断了用户的正常工作流程&#xff0c;也可能导致重要数据…

[中级]软考_软件设计_计算机组成与体系结构_08_输入输出技术

输入输出技术 前言控制方式考点往年真题 前言 输入输出技术就是IO技术 控制方式 程序控制(查询)方式&#xff1a;分为无条件传送和程序查询方式两种。 方法简单&#xff0c;硬件开销小&#xff0c;但I/O能力不高&#xff0c;严重影响CPU的利用率。 程序中断方式&#xff1…

机器学习第33周周报Airformer

文章目录 week33 AirFormer摘要Abstract一、论文的前置知识1. 多头注意力机制&#xff08;MSA&#xff09;2. 具有潜变量的变分模型 二、文献阅读1. 题目2. abstract3. 问题与模型阐述3.1 问题定义3.2 模型概述3.3 跨空间MSA&#xff08;DS-MSA&#xff09;3.4 时间相关MSA&…

特定领域软件体系结构

1.DSSA的定义 简单地说&#xff0c;DSSA&#xff08;Domain Specific Software Architecture&#xff09;就是在一个特定应用领域中为一组应用提供组织结构参考的标准软件体系结构。 从功能覆盖的范围的角度有两种理解DSSA中领域的含义的方式&#xff1a; &#xff08;1&#x…

微信小程序生命周期管理:从数据初始化到事件绑定

作为一个独立的应用开发平台,微信小程序提供了自己的生命周期机制,与我们熟悉的Vue.js框架有一些差异。掌握小程序生命周期的特点和使用技巧,对于开发高质量的小程序应用至关重要。深入理解和掌握小程序生命周期的使用技巧,将有助于我们构建出更加健壮和可维护的小程序应用。 小…

c语言数据结构(10)——冒泡排序、快速排序

欢迎来到博主的专栏——C语言数据结构 博主ID&#xff1a;代码小豪 文章目录 冒泡排序冒泡排序的代码及原理快速排序快速排序的代码和原理快速排序的其他排序方法非递归的快速排序 冒泡排序 相信冒泡排序是绝大多数计科学子接触的第一个排序算法。作为最简单、最容易理解的排序…

【软件测试】测试常见知识点汇总

测试常见知识点汇总 一、什么是测试1.1 测试和调试的区别1.2 什么是需求1.2.1 用户需求1.2.2 软件需求 1.3 测试用例要素1.4 软件的生命周期及各阶段概述1.5 开发模型和测试模型&#xff08;记住特点和适用场景&#xff09;1.5.1 开发模型1.5.1.1 瀑布模型&#xff08;自上而下…

解密项目管理工具数据安全:防火防盗,保密有招

相关数据显示&#xff0c;2021年中国数字经济规模总量达到45.5万亿元&#xff0c;占到国内GDP总量的39.8%。数字经济已经渗入我们工作生活的方方面面&#xff0c;项目管理工具就是其中之一&#xff0c;在数据安全备受重视的今天如何保证项目管理工具的数据安全性&#xff1f;Zo…

Linux+HA高可用24X7的安全保证

一&#xff0e; 介绍作为服务器&#xff0c;需要提供一定的24X7的安全保证&#xff0c;这样可以防止关键节点的宕机引起系统的全面崩溃。利用OpenSource开源软件&#xff0c;完成系统的高可靠双机热备方案。基于linux的 HA软件可靠稳定&#xff0c;比使用商业版本的HA软件降低成…

微信小程序python+uniapp高校图书馆图书借阅管理系统ljr9i

根据日常实际需要&#xff0c;一方面需要在系统中实现基础信息的管理&#xff0c;同时还需要结合实际情况的需要&#xff0c;提供图书信息管理功能&#xff0c;方便图书管理工作的展开&#xff0c;综合考虑&#xff0c;本套系统应该满足如下要求&#xff1a; 首先&#xff0c;在…

人工智能基础概念5:使用L1范数惩罚进行Lasso回归(正则化)解决机器学习线性回归模型幻觉和过拟合的原理

一、引言 在老猿CSDN的博文《人工智能基础概念3&#xff1a;模型陷阱、过拟合、模型幻觉》中介绍了通过L1或L2正则化来限制模型的复杂度来解决过拟合的问题&#xff0c;老猿当时并不了解这背后的原理&#xff0c;这2天通过查阅资料终于明白了相关知识&#xff0c;在此一L1正则…

Linux故障排查(亲身经历),Linux运维开发6年了

这里输入数字时注意不要按小键盘&#xff0c;要按键盘字母区上面的那排数字键&#xff1b; 比如我们要关闭pid为2的进程&#xff0c;输入2后按回车&#xff0c;会出现以下提示&#xff0c;此时再按回车就ok 注意 如果执行top命令后&#xff0c;发现没有cpu占用率较高的进程&a…