【Java 并发编程】解决多线程中数据错乱问题

news2024/12/23 14:20:29

前言


        承接上回,我们已经理解了线程的一些相关操作,本篇内容将会讲到如何去解决线程竞争导致的数据错乱。


前期回顾:线程操作


目录

前言

线程竞争的场景

竞态条件的详解

synchronized 关键字

 ReentrantLock 类

 

 

线程竞争的场景

概念:

        在大多数实际的多线程运用中,两个或者两个以上的线程需要共享存储相同的数据。如果两个线程存取同一个对象,并且每个线程分别调用了一个修改该对象状态的方法,那么会发生什么呢?这两个线程会相互覆盖。取决于线程访问数据的次序,可能会导致线程被破坏。这种情况通常称为 “竞态条件”。

        为了让大家更好的理解,这里先举两个例子


 例子一: 

        一个银行账户中有 1000 元,现在有 用户A、用户B分别从这个账户中取钱:那么必然包括两个操作:A向银行取钱,B向银行取钱,如果两次取钱都与银行存款相对应则证明银行的取钱操作是成功。

 ​​​

         试想一下,如果这两个操作不具备原子性,假设从 用户A  取走 100 元之后,操作突然终止了,那么 用户A 把钱取走了,但是银行账户的钱并没有减少。这种情况是很严重的,可能会致使银行破产。

 

上述操作有两个步骤,出现意外后导致取钱失败,说明没有原子性。 

原子性的解释:

  • 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 原子操作:即不会被线程调度机制打断的操作,没有上下文切换。

        现在我们可以用代码实现一下以上操作:

class Bank {
    // 一个账户有1000块钱
    static int money = 1000;
    // 柜台 Counter 取钱的方法
    public void Counter(int money) {// 参数是每次取走的钱
        Bank.money -= money;        // 取钱后总数减少
        System.out.println("A取走了" + money + "还剩下" + (Bank.money));
    }

    // ATM取钱的方法
    public void ATM(int money) {// 参数是每次取走的钱
        Bank.money -= money;    // 取钱后总数减少
        System.out.println("B取走了" + money + "还剩下" + (Bank.money));
    }
}

class PersonA extends Thread {
    // 创建银行对象
    Bank bank;

    // 通过构造器传入银行对象,确保两个人进入的是一个银行
    public PersonA(Bank bank) {
        this.bank = bank;
    }

    //重写run方法,在里面实现使用柜台取钱
    @Override
    public void run() {
        while (Bank.money >= 100) {
            bank.Counter(100);// 每次取100块
            try {
                sleep(100);    // 取完休息0.1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class PersonB extends Thread {
    // 创建银行对象
    Bank bank;

    // 通过构造器传入银行对象,确保两个人进入的是一个银行
    public PersonB(Bank bank) {
        this.bank = bank;
    }

    // 重写run方法,在里面实现使用柜台取钱
    @Override
    public void run() {
        while (Bank.money >= 200) {
            bank.ATM(200);// 每次取200块
            try {
                sleep(100);// 取完休息0.1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

class MainClass {
    public static void main(String[] args) {
        System.out.println("一个账户总共有1000块钱");
        // 实力化一个银行对象
        Bank bank = new Bank();
        // 实例化两个人,传入同一个银行的对象
        PersonA pA = new PersonA(bank);
        PersonB pB = new PersonB(bank);
        // 两个人开始取钱
        pA.start();
        pB.start();
    }
}

打印结果:

一个账户总共有1000块钱
B取走了200还剩下700
A取走了100还剩下900
A取走了100还剩下600
B取走了200还剩下600
B取走了200还剩下300
A取走了100还剩下500
B取走了200还剩下100
A取走了100还剩下0

        可以看到这里出现了几处错误:比如 B 是第一个取钱的,但是取完之后只剩下 700。程序在运行一段时间,我们发现有一段 A、B 取完钱之后,银行账户余额没有发生改变。如果一个银行采用的是这样的系统,你还会将钱存进这个银行吗?

 例子二: 

class Test {
    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);
    }

        以上程序运行的结果是什么?如果是单线程的话,估计你会立刻说出答案 -- 100000。但是这里是多线程,我们打印的结果往往是小于 100000 的数字

        关于以上问题的答案我们现在就来揭晓 ~

竞态条件的详解

        我们以第二个简单的例子进行讲解,由于 t1 线程在执行时会受到 t2 线程的干扰,所以这不是原子操作。那么 Count++ 这条语句可能就有如下三条指令

(1)把内存中的数据,读取到 cpu 寄存器里
(2)把 CPU 寄存器里的数据 +1
(3)把寄存器的值,写回内存

         现在假定 t1 线程执行步骤1和步骤2,然后,它的运行权被强占。这时 t1 寄存器刚从内存中读取到数据并加 1,只差修改内存数据了。再假设现在 t2 线程现在被唤醒,并完美的执行三个步骤:由于 t1 线程并没有修改内存所以 t2 寄存器从内存中读取到的值仍然是 0,然后加 1 修改内存,此时内存的值为 1。最后终于运行 t1 的步骤3,这个时候 t1 寄存器的值就会覆盖 t2 线程所作出的修改,其结果仍然是 1。这相当于运行了两次 for 循环,但是 Count 只增加了 1。

        那么如何解决上述问题呢? -- 通常有三种方法:

        方法一:由于程序是并发执行的,所以关联线程(两个线程共用一个变量)之间势必会发生竞争,那么我们只需让一个线程等待等一个线程执行完即可。使用 join() 就可以解决问题。

        上述代码是让线程 t2 阻塞等待 t1线程结束后再开始运行。结果表明这种方法是可行的。

        方法二:可以设置两个变量在不同的线程中运行,最后在整合起来。

class Test {
    public static int Count1 = 0;
    public static int Count2 = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for(int i = 0;i<50000;i++){
                Count1++;
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i<50000;i++){
                Count2++;
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(Count1+Count2);
    }
}

synchronized 关键字

        第三种方法是本章节重点要求掌握的 -- 同步锁。 Java 提供了一个关键字 synchronized 可以防止并发访问一个代码块。

synchronized 的介绍

        它的作用域默认是当前对象,这时锁就是对象,谁拿到这个锁谁就可以运行它所控制的那段代码如果这个对象有多个 synchronized 方法,其它线程就不能同时访问这个对象中任何一个 synchronized 方法。

class Test {
    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 (Test.class) {
                    Count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
              for(int i = 0;i<50000;i++){
                    synchronized (Test.class) {
                        Count++;
                    }
                }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(Count);
    }
}

        Test.class 的意思是引用当前类的对象。此时程序运行结果就是 100000,我们回去看看给例子一加锁答案将会如何:

程序运行结果:

一个账户总共有1000块钱
A取走了100还剩下900
B取走了200还剩下700
A取走了100还剩下600
B取走了200还剩下400
A取走了100还剩下300
B取走了200还剩下100
A取走了100还剩下0

 

 ReentrantLock 类

        synchronized 关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。除了 synchronized 能锁住线程外,Java 5还引入了 ReentrantLock 类。

        上述代用如果用 ReentrantLock 写的话,可以这么写:

class Test {
    public static int Count = 0;
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock myLock = new ReentrantLock();
        Thread t1 = new Thread(()->{
            for(int i = 0;i<50000;i++){
                myLock.lock();
                try{
                    Count++;
                }finally {
                    myLock.unlock();
                }
            }
        });
        Thread t2 = new Thread(()->{
                for(int i = 0;i<50000;i++){
                    myLock.lock();
                    try{
                        Count++;
                    }finally {
                        myLock.unlock();
                    }
                }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(Count);
    }
}

使用  ReentrantLock 保护代码块的基本结构如下:

              myLock.lock();
                try{
                    //Count++;
                }finally {
                    myLock.unlock();
                }

        这个结构确保了任何时刻只有一个线程进入临界状态。一旦一个线程锁定了对象,任何其他线程都无法通过 lock 语句。当其他线程调用 lock 语句时,它们会处于阻塞状态,直到第一个线程释放这个锁对象

        注意:因为 synchronized 是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock 是Java代码实现的锁,我们就必须先获取锁,然后在 finally 中正确释放锁(也就是将 unlock 操作包在 finally 语句中),否则其他线程将永远阻塞

         关于更多的锁操作,后续将继续介绍~

 

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

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

相关文章

异步场景: promise、async函数与await命令介绍

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 在鸿蒙的开发中&#xff0c;我们时常会遇到promise异步场景&#xff0c;有同学反馈说…

Adobe Acrobat Pro和Adobe Acrobat DC有什么区别?

主要区别 Adobe Acrobat Pro&#xff1a; 以单次购买的方式提供&#xff0c;用户需要一次性付费购买&#xff0c;之后即可永久使用该版本。不会频繁更新&#xff0c;通常只在发行新版本时进行更新。 Adobe Acrobat DC&#xff1a; 以订阅方式提供&#xff0c;用户需要每年支付…

《2024 国庆旅游数据洞察:活力与新趋势》

《2024 国庆旅游数据洞察&#xff1a;活力与新趋势》 一、国庆旅游市场整体态势 今年国庆假期&#xff0c;旅游市场的火爆程度令人瞩目。从出行人次来看&#xff0c;嘀嗒出行的国庆顺风出行预测显示&#xff0c;顺风出发高峰日预计为 9 月 29 日&#xff0c;环比 9 月出行峰值…

在Ubuntu 22.04上安装Ollama的两种方式

curl 安装 参考Linux上安装Ollama的官方文档&#xff1a;https://ollama.com/download/linux 在终端执行以下命令即可&#xff1a; curl -fsSL https://ollama.com/install.sh | shdocker 安装 官方 Ollama Docker 镜像可以直接在Docker Hub上进行拉取。 Docker Hub上的ol…

Java 方法的重载

1.重载&#xff1a;在一个类中&#xff0c;方法的函数名相同&#xff0c;但形参不同。 结果&#xff1a; 2&#xff0e;方法重载的规则&#xff1a; &#xff08;1&#xff09;方法名必须相同。&#xff08;例如&#xff1a;重名的人有很多&#xff09; &#xff08;2&#x…

数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(二).设置主键自增等特点

前言 在上一节中&#xff0c;主要介绍了 Navicat Premium 17 的使用以及创建一个基础的表格。当时只设置了给数据表补充字段&#xff0c;没有设置给数据表删除字段。现在补充一下。 ALTER TABLE student ADD test int(4); 给名为 student 的数据表添加 test 列&#xf…

正则表达式-“三剑客”(grep、sed、awk)

1.3正则表达式 正则表达式描述了一种字符串匹配的模式&#xff0c;可以用来检查一个串是否含有某种子串&#xff0c;将匹配的子串替换或者从某个串中取出符号某个条件的子串等&#xff0c;在linux中代表自定义的模式模版&#xff0c;linux工具可以用正则表达式过滤文本。Linux…

vavr Java的函数式编程神器-Part1

微信公众号&#xff1a;阿俊的学习记录空间 小红书&#xff1a;ArnoZhang wordpress&#xff1a;arnozhang1994 博客园&#xff1a;arnozhang CSDN&#xff1a;ArnoZhang1994 1. 介绍 Vavr&#xff08;前称 Javaslang&#xff09;是一个为 Java 8 提供的函数式库&#xff0c;…

红外探测算法!!!

一、红外探测的基本原理 红外探测基于红外辐射与物体的热状态之间的关系。物体温度越高&#xff0c;辐射能量越大。红外探测器通过接收物体发出的红外辐射&#xff0c;将其转换为电信号&#xff0c;进而实现对目标的探测和识别。 二、红外探测算法的主要类型 背景差分法&…

[自然语言处理]RNN

1 传统RNN模型与LSTM import torch import torch.nn as nntorch.manual_seed(8)def dm01():参数1&#xff1a;输入向量的维数参数2&#xff1a;隐藏层神经元的个数参数3&#xff1a;隐藏层的层数:return:rnn nn.RNN(5, 6, 1)参数1&#xff1a;句子长度sequence_length参数2&am…

九芯电子NVH/NVF语音芯片OTA升级操作方法

OTA&#xff08;Over-The-Air&#xff09;升级是指通过无线网络远程对设备进行软件升级的过程。对于九芯电子NVH/NVF语音芯片&#xff0c;OTA升级可以通过WiFi模组实现&#xff0c;支持MQTT、HTTP等协议&#xff0c;方便快捷‌。 具体操作步骤如下&#xff1a; 1.进入九芯“智…

计算机毕业设计 基于Django的学生选课系统的设计与实现 Python+Django+Vue 前后端分离 附源码 讲解 文档

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

处理Java内存溢出问题(java.lang.OutOfMemoryError):增加JVM堆内存与调优

处理Java内存溢出问题&#xff08;java.lang.OutOfMemoryError&#xff09;&#xff1a;增加JVM堆内存与调优 在进行压力测试时&#xff0c;遇到java.lang.OutOfMemoryError: Java heap space错误或者nginx报错no live upstreams while connecting to upstream通常意味着应用的…

重头开始嵌入式第四十七天(硬件 ARM裸机开发 RS232 RS4885 IIC)

目录 一.什么是RS232&#xff1f; 1. 历史背景&#xff1a; 2. 电气特性&#xff1a; 3. 连接器类型&#xff1a; 4. 通信特点&#xff1a; 5. 应用场景&#xff1a; 二.什么是RS485&#xff1f; 1. 电气特性&#xff1a; 2. 通信模式&#xff1a; 3. 传输距离与速率&…

技术路线图用什么画?用这个在线工具轻松完成绘制!

在当今快速发展的技术世界中&#xff0c;技术路线图已成为企业和团队不可或缺的战略规划工具。它不仅能够清晰地展示技术发展方向&#xff0c;还能帮助团队成员、利益相关者和投资者更好地理解和参与技术战略的制定过程。但不可否认的是&#xff0c;创建一个有效的技术路线图并…

如何免费为域名申请一个企业邮箱

背景 做SEO的是有老是会有一些网站来做验证你的所有权&#xff0c;这个时候&#xff0c;如果你域名对应的企业邮箱就会很方便。zoho为了引导付费&#xff0c;有很多多余的步骤引导&#xff0c;反倒是让不付费的用户有些迷茫&#xff0c;所以会写这个教程&#xff0c;按照教程走…

虚幻引擎GAS入门学习笔记(二)

虚幻引擎GAS入门(二) 学习位置UE5.3 GAS入门教程重置版 小明 MVC框架与技能初始化 让一开始创建的蓝图的基础GameplayAbility蓝图继承我们写好的BaseGameplayAbility类 创建一个库函数&#xff0c;写一些常用的函数在里面第一个得到玩家与玩家控制器 获取角色面对目标的方向…

c++11~c++20 thread_local

线程局部存储是指对象内存在线程开始后分配&#xff0c;线程结束时回收且每个线程有该对象自己的实例&#xff0c;简单地说&#xff0c;线程局部存储的对象都是独立各个线程的。实际上这并不是一个新鲜个概念&#xff0c;虽然C一直没因在语言层面支持它&#xff0c;但是很早之前…

Coggle数据科学 | 全球AI攻防挑战赛:金融场景凭证篡改检测 baseline

本文来源公众号“Coggle数据科学”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;全球AI攻防挑战赛&#xff1a;金融场景凭证篡改检测 baseline 赛题名称&#xff1a;全球AI攻防挑战赛—赛道二&#xff08;AI核身-金融场景凭证篡改…

集智书童 | FMRFT 融合Mamba和 DETR 用于查询时间序列交叉鱼跟踪 !

本文来源公众号“集智书童”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;FMRFT 融合Mamba和 DETR 用于查询时间序列交叉鱼跟踪 ! 鱼的生长、异常行为和疾病可以通过图像处理方法进行早期检测&#xff0c;这对工厂水产养殖具有重…