线程安全问题(一)——锁的简单使用

news2024/11/24 12:56:06

多线程安全问题

  • 线程安全问题的引入
  • 案例引入
    • 多线程指令排序问题
  • 线程不安全的原因
  • 解决线程不安全的方法
  • 锁的引入
    • 上锁和解锁过程
    • 一个简单的锁Demo
    • 对这个案例进行几次修改
  • 总结

线程安全问题的引入

在前面的博文中,我们了解到通过Thread.join()的方法让线程进入等待,能够在一定程度上解决线程抢占式执行的问题。回忆点这里
那么由于多线程代码而导致的bug,这样的问题就是线程安全问题。

案例引入

在下面的代码中,我希望执行之后得到count=100000的结果。

private static long count;
    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);
    }

结果如图:通过结果,我们每次运行得到的答案都是不一样的。
在这里插入图片描述

多线程指令排序问题

在上面的demo中,执行count++的指令并不只有一条这么简单。它可以分为三个步骤:(1) load操作: 读取内存中count的数值,保存到cpu寄存器中。 (2) add操作: 将寄存器中的count+1 (3) save操作:将寄存器中count的数值存放回内存中。
看似三条指令的简单操作,在多线程并发执行中却容易导致许多问题。
在多线程随即调度的执行状况下,两个线程的指令执行相对顺序可能就会存在多种可能,下面我列出几种可能性。
在这里插入图片描述
在上面的顺序中,我们可以理解为 在第一次t1线程的count++之后,count=1的值存放于寄存器中,接下来t2线程count++的时候,load指令下读取的数值为count=0,之后save操作后count=1,最后执行t1线程的save后值不变。因此执行了两次count++操作后,count的值只加了一次,出现了覆盖现象
我们可以不断扩大到其他状况,如t1线程辛辛苦苦加了100次,但是t2线程最终存放count=1将值覆盖了。

线程不安全的原因

  • 线程在系统中是随即调度,抢占式执行的。
  • 多个线程同时修改同一个变量(参考上述例子)
  • 线程对变量进行修改操作,非原子指令
  • 内存可见性问题
  • 指令重排序问题

解决线程不安全的方法

对于原因2,我们可以通过join等方式防止这种情况的发生,但这样做并不普适,属于少有的情况。

锁的引入

对于案例中的count++操作,我们清楚它不是一个原子操作,因此,程序猿想出来了一个办法:将上述的一系列“非原子”操作打包成一个“原子”操作。这样就能够避免线程不安全的问题。
基于这样的背景,锁被创建出来了。

上锁和解锁过程

假设存在两个线程t1和t2,
(1)上锁:我们首先给t1加上锁(lock),t2也尝试加同一把锁,那么这时候t2线程就会阻塞等待,在Java中该线程处于Blocked状态。
(2)解锁:当线程t1执行完锁住的部分后,线程t1解锁,接着由线程t2通过锁竞争拿到该锁(lock),加锁成功,t2线程转变为Runnable状态。
通过锁的存在,使得线程之间存在互斥的关系。在两个线程之间尚且都要通过锁竞争,而存在多个线程的情况下自然也要通过竞争的方式占据锁。这里必须要明确一个条件:线程之间竞争的必须要是同一把锁

一个简单的锁Demo

通过下面的代码块进行简单的解释。

  • 首先new一个Object类的对象object1.我们要把这个对象作为锁。看到这里我们就可以清楚,锁不必是某种特定的类,他只是一个标识,只是一个对象即可。
  • 在这段案例中,存在main、t1、t2三个线程,main线程十分简单,通过Thread.join()的方法等待t1和t2两个线程运行结束之后打印出结果即可。
  • 在线程t1和t2中,在for循环中,我们可以看到synchronized (object1)。其中synchronized就是锁的关键字。在t1和t2都使用了这段语句,即在进行count++操作之前,需要进行锁竞争,只有拥有锁的一方才可以进行count++操作。
  • 在这段代码块中,t1和t2在for循环的时候是并行执行的,而在锁竞争的时候是串行执行的。这样计算下来比单线程所花费的时间要少许多。
private static int count;
public static void main(String[] args) throws InterruptedException {
        Object object1 = new Object();
        Thread t1 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    synchronized (object1) {
                        count++;
                    }
                }
        });
        Thread t2 = new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    synchronized (object1) {
                    count++;
                    }
                }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+count);
    }

运行结果如下图:
在这里插入图片描述

对这个案例进行几次修改

(1)接下来我们可以考虑,当锁对象为两个:object1和object2时,两个线程分别竞争两个不一样的锁,会出现什么情况
在这里插入图片描述
结果如下:
在这里插入图片描述
通过这个改变,我们可以理解不同的锁对象之间不存在互斥关系,因此二者之间也就不会发生锁竞争。

(2)将synchronized放在for循环外面的情况
在这种条件下,意味着当t1或t2某一个线程拿到这把锁之后,只有等循环结束以后才能释放了,很明显这样的情况所花费的资源甚至多于单线程。

Thread t1 = new Thread(()->{
            synchronized (object1) {
            for (int i = 0; i < 50000; i++) {
                        count++;
                    }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (object1) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                    }
                }
        });

(3)在下面的代码中,设计Counter类进行add和get操作,在上面的代码中,我们已经知道锁对象只是一个标识,不关心它是怎样的存在,因此在这里,我们大胆的把counter对象作为锁对象。

class Counter {
    public int count;

    public  void add() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
public class Demo3 {
    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("count = "+counter.getCount());
    }
}

运行结果如下图:
在这里插入图片描述
(4)如果我们把锁加到add()方法中,我们通过this来指代对应的对象,这样做的情况是当多个线程调用该方法的时候,如果使用的是同一个对象会进行竞争,如果是不同对象的话则不会进行竞争。
同含义的写法为:synchronized public void add()

class Counter {
    public int count;

     public void add() {
     	synchronizedthis){
        	count++;
        }
    }

    public int getCount() {
        return count;
    }
}

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();
            }
        });

(5)对静态方法加锁
与(4)不同的是,加锁的对象为Counter这个类对象,因此如果多个线程调用func方法,则这些线程之间都会进行锁竞争

//第一种写法
public static void func(){
  synchronized (Counter.class) {
    //func
    }
}
//第二种写法
synchronized public static void func(){
  //func
}
   

总结

对于锁的概念需要逐渐深入,在本文中讲解了锁引入的原因以及锁的几种写法。
本文中使用的源码请戳此处

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

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

相关文章

谷歌云(GCP)4门1453元最热门证书限时免费考

谷歌云(GCP)最新活动&#xff0c;完成免费官方课程&#xff0c;送4门最热门考试免费考试券1张(每张价值200刀/1453元)&#xff0c;这4门也包括最近大热的AI/ML考试&#xff0c;非常值得学习和参加&#xff0c;活动7/17截止 谷歌云是全球最火的三大云计算厂商(前两名AWS, Azure…

Electron 整理文档

Electron 简介 Electron 是一个使用 JavaScript、 HTML 和 CSS 构建桌面应用程序的框架。通过将 Chromium 和 Node.js 嵌入到它的二进制文件中&#xff0c;Electron 允许你维护一个 JavaScript 代码库&#xff0c;并创建可以在 Windows、 macOS 和 Linux 上运行的跨平台应用程序…

pandas将dataframe展开/拉伸成一个series

pandas提供了一个函数实现这个操作&#xff1a; dataframe.stack()示例程序&#xff1a; import pandas as pd import numpy as npdf pd.DataFrame(np.random.randint(0, 10, size(2, 4)), columns[col_1, "col_2", "col_3", "col_4"]) # 展…

详解 ClickHouse 的分片集群

一、简介 分片功能依赖于 Distributed 表引擎&#xff0c;Distributed 表引擎本身不存储数据&#xff0c;有点类似于 MyCat 之于 MySql&#xff0c;成为一种中间件&#xff0c;通过分布式逻辑表来写入、分发、路由来操作多台节点不同分片的分布式数据 ClickHouse 进行分片集群的…

优雅谈大模型13:LangChain Vs. LlamaIndex

实时了解业内动态&#xff0c;论文是最好的桥梁&#xff0c;专栏精选论文重点解读热点论文&#xff0c;围绕着行业实践和工程量产。若在某个环节出现卡点&#xff0c;可以回到大模型必备腔调或者LLM背后的基础模型重新阅读。而最新科技&#xff08;Mamba,xLSTM,KAN&#xff09;…

算法训练营day20--235. 二叉搜索树的最近公共祖先+701.二叉搜索树中的插入操作 +450.删除二叉搜索树中的节点

一、235. 二叉搜索树的最近公共祖先 题目链接&#xff1a;https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/ 文章讲解&#xff1a;https://programmercarl.com/0235.%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E7%9A%84%E6%9C%80%E8%BF%91…

小阿轩yx-用户管理与高级SQL语句

小阿轩yx-用户管理与高级SQL语句 MySQL 进阶查询 运维工作中可以提供不小的帮助&#xff0c;运维身兼数职&#xff0c;可能会有不少数据库的相关工作 常用查询介绍 对查询的结果集进行处理 按关键字排序 使用 SELECT 语句可以将需要的数据从 MySQL 数据库中查询出来 对结…

调幅信号AM的原理与matlab实现

平台&#xff1a;matlab r2021b 本文知识内容摘自《软件无线电原理和应用》 调幅就是使载波的振幅随调制信号的变化规律而变化。用音频信号进行调幅时&#xff0c;其数学表达式可以写为: 式中&#xff0c;为调制音频信号&#xff0c;为调制指数&#xff0c;它的范围在(0&…

攻克PS之路——Day1(A1-A8)

#暑假到了&#xff0c;作为可能是最后一个快乐的暑假&#xff0c;我打算学点技能来傍身&#xff0c;首先&#xff0c;开始PS之旅 这个帖子作为我跟着B站up主学习PS的记录吧&#xff0c;希望我可以坚持下去&#xff01; 学习的链接在这里&#xff1a;A02-PS软件安装&#xff0…

Linux显示服务器Wayland切换到X11

1、临时切换 &#xff1a; 注销当前用户账户&#xff0c;返回到登录屏幕。 在登录屏幕上&#xff0c;选择您要登录的用户账户。 在输入密码之前&#xff0c;在登录屏幕的右下角可能有一个齿轮图标&#xff0c;点击它以展开更多选项。 在选项中选择“Ubuntu on Xorg”或“Ubu…

Matlab|风光及负荷多场景随机生成与缩减

目录 1 主要内容 计算模型 场景生成与聚类方法应用 2 部分程序 3 程序结果 4 下载链接 1 主要内容 该程序方法复现了《融合多场景分析的交直流混合微电网多时间尺度随机优化调度策略》3.1节基于多场景技术的随机性建模部分&#xff0c;该部分是随机优化调度的重要组成部分…

web自动化(一)selenium安装环境搭建、DrissionPage安装

selenium 简介 selenium是企业广泛应用的web自动化框架 selenium 三大组件 selenium IDE 浏览器插件 实现脚本录制 webDriver 实现对浏览器进行各种操作 Grid 分布式执行 用例同时在多个浏览器执行&#xff0c;提高测试效率 问题&#xff1a;环境搭建复杂&#xff0c;浏览器版…

2-16 基于matlab的动载荷简支梁模态分析程序

基于matlab的动载荷简支梁模态分析程序&#xff0c;可调节简支梁参数&#xff0c;包括截面宽、截面高、梁长度、截面惯性矩、弹性模量、密度。输出前四阶固有频率&#xff0c;任意时刻、位置的响应结果。程序已调通&#xff0c;可直接运行。 2-16 matlab 动载荷简支梁模态分析 …

基于SpringBoot和PostGIS的某国基地可视化实战

目录 前言 一、Java后台开发设计与实现 1、模型层实现 2、控制层设计 二、WebGIS界面实现 1、列表界面的定义 2、全球基地可视化 三、成果展示 1、全球部署情况 2、亚太地区 3、欧洲基地分布 4、中东的部署 四、总结 前言 在之前的博客中&#xff0c;我们曾经对漂亮…

【安卓13 源码】RescueParty救援机制

RescueParty机制正是在这个背景下诞生的&#xff0c;当它注意到系统或系统核心组件陷入循环崩溃状态时&#xff0c;就会根据崩溃的程度执行不同的救援行动&#xff0c;以期望让设备恢复到正常使用的状态。 开机后会自动重启&#xff0c;进入Recovery界面。经查找&#xff0c;是…

软考中级--数据库系统工程师备考建议和考试注意事项

相关资料&#xff1a;《数据库系统工程师》15-21年真题、考试说明 今日软考出分&#xff0c;顺利通过。 备注&#xff1a;本人是计算机专业学生&#xff0c;之前系统学习过408和数据库理论&#xff0c;以下仅为本人的视角&#xff0c;仅供参考。 1. 备考规划 1.1 备考资料 《…

Javaweb配置tomcat

Tomcat 9版本链接 链接&#xff1a;https://pan.baidu.com/s/1u-eDur5KlqlXM_IM50Ahtg?pwd1njm 提取码&#xff1a;1njm 1、打开idea&#xff0c;创建maven项目 2023版IDEA 2、 目录结构 ps: 如果结果不完整,选中main右键 新建对应的文件夹 3、 web项目设置Tomcat(部署项目…

SelfReg-UNet:解决UNet语义损失,增强特征一致性与减少冗余的优化模型

SelfReg-UNet&#xff1a;解决UNet语义损失&#xff0c;增强特征一致性与减少冗余的优化模型 提出背景拆解类比&#xff1a;整理书架语义一致性正则化内部特征蒸馏为什么 UNet 会有语义损失&#xff1f; 提出背景 论文&#xff1a;https://arxiv.org/pdf/2406.14896 代码&…

YOLOv8关键点pose训练自己的数据集

这里写自定义目录标题 YOLOv8关键点pose训练自己的数据集一、项目代码下载二、制作自己的关键点pose数据集2.1 标注(非常重要)2.1.1 标注软件2.1.2 标注注意事项a.多类别检测框b.单类别检测框2.2 格式转换(非常重要)2.3 数据集划分三、YOLOv8-pose训练关键点数据集3.1 训练…