JavaEE 初阶篇-深入了解多线程安全问题(出现线程不安全的原因与解决线程不安全的方法)

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 多线程安全问题概述

        1.1 线程不安全的实际例子

        2.0 出现线程不安全的原因

        2.1 线程在系统中是随机调度且抢占式执行的模式

        2.2 多个线程同时修改同一个变量

        2.3 线程对变量的修改操作不是“原子”

        2.4 内存可见性

        2.5 指令重排序

        3.0 解决线程不安全问题(使用锁机制)

        3.1 synchronized 关键字可以作用的地方

        3.1.1 同步代码块

        3.1.2 同步实例方法

        3.1.3 同步静态方法

        3.2 join() 方法与 synchronized 关键字的区别

        4.0 加锁不合理所引发的问题

        4.1 对于一个加锁与一个没有加锁的两个线程随机调度执行同一个代码块或者方法的情况

        4.2 嵌套相同的锁 - 可重入锁

        4.3 两个线程两把锁 - 死锁

        4.4 死锁的四个必要条件

        4.4.1 互斥条件

        4.4.2 不可剥夺条件

        4.4.3 请求保持条件

        4.4.4 循环等待条件

        4.5 如何避免死锁?


        1.0 多线程安全问题概述

        多线程安全问题是指在多线程环境下,多个线程同时访问共享资源可能导致的数据不一致、数据竞争、死锁等问题。

        1.1 线程不安全的实际例子

        在多线程中,很容易就会出现多线程问题,从而引发多线程安全问题。

先看以下代码:

    public static long count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        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);
    }

        一般来说,t1 和 t2 都对 count 这个变量进行 count++ 这个操作,那么 count 最后的输出结果按理来说应该为 10万。但是输出的结果是不一定为 10 万,而且等于 10 万的概率非常非常非常小。

运行结果:

        每次的运行结果都是不一样的,很大概率都是在 5 万到 10 万之间“徘徊”。也有可能会小于 5 万。

        出现了以上的结果,都是因为多线程抢占式执行随机调度,从而导致的结果。

具体分析:

        在 CPU 中执行 count++ 这一操作,大致需要执行三条指令:

        1)load:将内存中的数据读取加载到 CPU 的寄存器中。

        2)add:在寄存器中的值 +1 操作。

        3)save:把寄存器中的值写回到内存中。

        现在 t1 与 t2 两个线程并发的进行 count++ ,多线程的执行是随机调度,抢占式的执行模式。有可能会出现以下几种情况:

        出现可能 1 或者可能 2 这两次 count++ 的最终结果都是 2 ,因此是正确的。而对于可能 3 这种情况,虽然说,t1 与 t2 都完成了 count++ 的操作,但是,对于可能 3 这种情况,在 t2 完成 count++ 之后,count 由 0 改为 1 ,再把值写回到内存之后,但是 t1 来说,同样也是把 count 由 0 改为 1 ,写回到内存中。简单来说,t1 将 t2 线程中 count 进行了一次覆盖,重新赋值,所以 t2 这个线程的操作是无效操作。

        出现的情况有无数种,不可预计的。之所以说,最后出现的结果为 10 万的概率是非常非常小的,几乎没有可能吧。

对于出现小于 5 万的情况:

          按理来说,进行了三次 count++ 操作,最后的结果应该为: count == 3,但是这里最后的结果:count == 1。这就是有可能出现 count 小于 5万的可能,出现数越加越小了。

        以上就是属于多线程引发的线程安全问题。

        2.0 出现线程不安全的原因

        2.1 线程在系统中是随机调度且抢占式执行的模式

        这是导致线程不安全的“罪魁祸首,万恶之源”,不能去改变这个机制。

        2.2 多个线程同时修改同一个变量

        当多个线程同时修改同一个变量时,可能会导致数据竞争和结果不确定性。这种情况下,需要采取线程安全的措施来确保数据的一致性。

        2.3 线程对变量的修改操作不是“原子”

        count++ 这种,不是原子操作,在 cpu 执行 count++ 操作需要三条指令。

        2.4 内存可见性

        2.5 指令重排序

        3.0 解决线程不安全问题(使用锁机制)

        锁机制可以确保在任意时刻只有一个线程可以访问共享资源,从而避免数据竞争和保证数据的一致性。

        可以使用 synchronized 关键字等来实现锁机制。

代码如下:

    public static long count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                    synchronized (o){
                        count++;
                    }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                    synchronized (o){
                        count++;
                    }
            }
        });
        t1.start();
        t2.start();

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

        通过 synchronized 关键字,把 count++ 这个操作进行了加锁,对于 o 对象可以理解为一个标志,一个锁的标识,当进入 {} 那一刻,就会加上锁,执行完代码块中的代码后,退出 {} 时,会自动解锁。 

        现在对于 t1 与 t2 线程来说,在每一次 for 循环之后,会抢占 “加锁” 随机调度,两个线程抢占的机会是一样的,比如说 t1 抢占到了“加锁”,那么 t2 想要再次对 count++ 加锁时,先会判断,判断当前加锁的线程是哪一个线程,如果不是自己线程,那么就会阻塞等待 t1 线程。等待 t1 执行完毕之后,t1 与 t2 会继续抢占对 count++ 这个操作进行“加锁”处理,一直循环往复。

        这样就保证了 CPU 在执行 3 条指令的时候,不会被其他线程“打扰到”。每一次都是如

此:

最后的运行结果:

        此时多线程问题是安全的。

补充:

        1)锁本质上也是操作系统提供的功能,内核提供的功能,通过 API 给应用程序 JVM 对于这样的系统 API 又进行封装。

        2)锁对象的用途,有且只有一个,就是用来区分,判断两个线程是否是针对同一个对象加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待;如果不是,就不会出现锁竞争,也就不会阻塞等待。

        和对象的具体是什么类型,和它里面的属性、方法,对于接下来操作这个对象统统没有任何关系。所以可以将类似 o 对象简单理解为一个标识,一个工具。

        3)锁涉及的核心有两个:加锁、解锁

        主要的特性:互斥,一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待(锁竞争/锁冲突)。

        在代码中,可以创建出多个锁,只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁,则不会。

        3.1 synchronized 关键字可以作用的地方

        3.1.1 同步代码块

        使用 synchronized 关键字修饰代码块,可以指定对象作为锁,确保在同一时刻只有一个线程可以访问该代码块。其他线程需要等待获取锁后才能执行代码块。

    public static long count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //作用于代码块中
                synchronized (o){
                    count++;
                }
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //作用于代码块中
                synchronized (o){
                    count++;
                }
            }
        });

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

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

        System.out.println(count);
    }

        3.1.2 同步实例方法

        使用 synchronized 关键字修饰实例方法,可以确保在同一时刻只有一个线程可以访问该实例方法。其他线程需要等待当前线程执行完毕后才能访问。

代码如下:

public class demo11 {
    public synchronized void add(){
        count++;
    };

    public long get(){
        return count;
    };

    public static long count = 0;
    public static void main(String[] args) throws InterruptedException {
        demo11 demo = new demo11();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                demo.add();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                demo.add();
            }
        });

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

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

        System.out.println(demo.get());
    }
}

        3.1.3 同步静态方法

        使用 synchronized 关键字修饰静态方法,可以确保在同一时刻只有一个线程可以访问该静态方法。其他线程需要等待当前线程执行完毕后才能访问。

代码如下:

public class demo11 {
    public synchronized static void add(){
        count++;
    };

    public static long get(){
        return count;
    };

    public static long count = 0;
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                demo11.add();
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                demo11.add();
            }
        });

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

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

        System.out.println(demo11.get());
    }
}

        3.2 join() 方法与 synchronized 关键字的区别

        1)join() 是 Thread 类的方法,用于等待调用该方法的线程执行完成。当一个线程调用另一个线程的 join() 方法时,它会被阻塞,直到被调用的线程执行完成。

        2)synchronized 关键字用于实现线程同步确保在同一时刻只有一个线程可以访问某个代码块或方法。

         总的来说,join 用于线程之间的协作和等待,而 synchronized 用于实现线程之间的同步和互斥访问共享资源。

        4.0 加锁不合理所引发的问题

        4.1 对于一个加锁与一个没有加锁的两个线程随机调度执行同一个代码块或者方法的情况

        对于这种情况来说,同样会导致多线程安全问题。因为对于一个加锁与另一个没有加锁的情况,这两个线程之间没有锁竞争或者产生互斥,所以还是会出现多线程安全问题。

代码如下:

public class demo9 {

    public static long count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                    synchronized (o){
                        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);
    }
}

运行结果:

        以上代码中即使有一个线程加上了锁,同样也是跟没有加锁的代码本质是一样的。因此,需要对两个线程中且操作同一个代码块或者方法进行同时加锁处理,才会解决多线程的安全问题。

        4.2 嵌套相同的锁 - 可重入锁

        在同一个线程中,嵌套同一个锁被称为可重入锁

代码如下:

        在外层加完锁之后,在内层继续加了相同的锁。再来了解加锁的详细过程:两个线程随机调度执行,假设 t1 对该代码块加锁,而 t2 就不能加锁了,此时产生了锁竞争,t2 需要阻塞等待 t1 执行后解锁后,才能继续去“抢夺”上锁;对于 t1 来说外层加完锁之后,此时内层加锁之前需要判断当前是那个线程对当前的代码块上锁,如果是当前线程加锁了,那么内层加锁这个操作就是为无,可以继续往下执行,注意这里没有产生锁竞争;如果不是当前线程加锁了,就会阻塞等待。

所以可重入锁是安全的,运行结果: 

        补充:解锁是执行到外层 } 花括号结束之后,才会自动解锁,而不是执行到内层的 } 花括号解锁。所以,内层加锁其实是没有用的,正常来说,有最外面加锁就足够了,之所以要搞上述操作,就是担心不小心把代码写错从而搞出“死锁”,目的就是避免程序员粗心大意。

        4.3 两个线程两把锁 - 死锁

        死锁是系统中的多个线程或进程相互等待对方释放资源,从而陷入僵局无法继续执行的状态。

代码如下:

public class demo12 {
    public static void main(String[] args) {

        Object o1 = new Object();
        Object o2 = new Object();

        Thread t1 = new Thread(()->{
           synchronized (o1){
               //这里用到 sleep 方法的原因是因为,
               //保证 t2 线程执行完:对 o2 进行加锁操作
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }

               synchronized (o2){
                   System.out.println("正在执行 t1 线程");
               }
           }
        });

        Thread t2 = new Thread(()->{
           synchronized (o2){
               //这里用到 sleep 方法的原因是因为,
               //保证 t1 线程执行完:对 o1 进行加锁操作
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }

               synchronized (o1){
                   System.out.println("正在执行 t2 线程");
               }
           }
        });

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

        此时 t1 线程对 o1 加锁了,t2 线程对 o2 加锁了,对于 t1 来说,想要继续往下执行,需要 t2 对 o2 解锁,想要 t2 对 o2 解锁对话,需要 t1 对 o1 解锁。想要 t1 解锁的话,还是需要 t2 对 o2 解锁。。。此时成了很尴尬的情况,对方要需要对方的资源,双方都相互等待对方释放资源,从而僵持住了,无法执行下去。这样就造成了一个死锁。

通过 Jconsole 可执行程序观察:

已经检测到了死锁

运行结果:

        程序一直在运行中

        4.4 死锁的四个必要条件

        4.4.1 互斥条件

        资源只能被一个线程或进程所持有,其他线程无法同时访问。

        4.4.2 不可剥夺条件

        线程已经获取的资源在未使用完之前不能被其他线程所抢占。

        4.4.3 请求保持条件

        线程可以持有一些资源并继续请求其他资源。

        4.4.4 循环等待条件

        每个线程都在等待其他线程所持有的资源,形成一个循环等待的情况。

        4.5 如何避免死锁?

        只需要破环任意一个满足死锁的必要条件即可。

        1)对于互斥条件来说,不能破坏,所以不用考虑这种情况。

        2)对于不可剥夺条件,破坏该条件的方法是:如果一个线程无法获取资源,可以释放已经持有的资源,避免长时间占用资源。

        3)对于请求保持条件,破坏该条件的方法是:一次性获取所有需要的资源。

        4)对于循环等待条件,破坏该条件的方法是:按照固定顺序来获取资源,避免形成循环等待。

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

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

相关文章

C语言-编译和链接

目录 1.前言2.编译2.1预处理&#xff08;预编译&#xff09;2.1.1 #define 定义常量2.1.2 #define 定义宏2.1.3带有副作用的宏参数2.1.4宏替换规则2.1.5 #和##2.1.5.1 #运算符2.1.5.2 ## 运算符 2.1.6 命名约定2.1.7 #undef2.1.8 条件编译2.1.9 头文件的包含2.1.9.1 本地文件包…

基于RIP的MGRE综合实验

实验拓扑&#xff1a; 实验要求&#xff1a; 1、R5为ISP&#xff0c;只能进行IP地址配置&#xff0c;其所有地址均配为公有Ip地址; 2、R1和R5间使用PPP的PAP认证&#xff0c;R5为主认证方&#xff1b; R2与R5之间使用ppp的cHAP认证&#xff0c;R5为主认证方; R3与R5之间使用H…

Clip算法解读

论文地址&#xff1a;https://arxiv.org/pdf/2103.00020.pdf 代码地址&#xff1a;https://github.com/OpenAI/CLIPz 中文clip代码&#xff1a;https://gitcode.com/OFA-Sys/Chinese-CLIP/overview 一、动机 主要解决的问题&#xff1a; 超大规模的文本集合训练出的 NLP 模…

pbrt-v4 windows编译失败指南

cpu下编译成功很容易&#xff0c;但是gpu有点麻烦&#xff0c;主要有下面几个坑 安装optix 7&#xff0c;cmake build 要加上PBRT_OPTIX_PATH cmake cuda 版本要对应&#xff0c;不然会出现 cuda not found&#xff0c;或者generate的时候报错&#xff0c;导致最后pbrt.exe --…

FANUC机器人故障诊断—报警代码更新(三)

FANUC机器人故障诊断中&#xff0c;有些报警代码&#xff0c;继续更新如下。 一、报警代码&#xff08;SRVO-348&#xff09; SRVO-348DCS MCC关闭报警a&#xff0c;b [原因]向电磁接触器发出了关闭指令&#xff0c;而电磁接触器尚未关闭。 [对策] 1.当急停单元上连接了CRMA…

在react项目用echarts绘制中国地图

文章目录 一、引入echarts二、下载地图json数据三、编写react组件四、组件使用 一、引入echarts 安装&#xff1a;npm i echarts --save 二、下载地图json数据 由于echarts内部不再支持地图数据&#xff0c;所以要绘制地图需要自己去下载数据。建议使用阿里云的。 地址&…

接口自动化框架搭建(四):pytest的使用

1&#xff0c;使用说明 网上资料比较多&#xff0c;我这边就简单写下 1&#xff0c;目录结构 2&#xff0c;test_1.py创建两条测试用例 def test_1():print(test1)def test_2():print(test2)3&#xff0c;在pycharm中执行 4&#xff0c;执行结果&#xff1a; 2&#xff0…

Mysql连接报错:1130-host ... is not allowed to connect to this MySql server如何处理

我用navicat连接我的阿里云服务器的mysql服务器的时候,出现了1130的报错。&#xff08;mysql Server version: 5.7.42-0ubuntu0.18.04.1 (Ubuntu)&#xff09; 我来记录一下这个原因&#xff0c;以及修改过程&#xff01; 1.首先进入mysql -u root -p&#xff0c; mysql客户端…

车载电子与软件架构

车载电子与软件架构 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师 (Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎么看自己。江湖一碗茶,喝完再挣扎,出门靠自己,四…

Unity LineRenderer的基本了解

在Unity中&#xff0c;LineRenderer组件用于在场景中绘制简单的线条。它通常用于绘制轨迹、路径、激光等效果。 下面来了解下它的基本信息。 1、创建 法1&#xff1a;通过代码创建 using UnityEngine;public class CreateLineRenderer : MonoBehaviour {void Start(){// 创…

排序算法超详细代码和知识点整理(java版)

排序 1、冒泡排序 ​ 两层循环&#xff0c;相邻两个进行比较&#xff0c;大的推到后面去&#xff0c;一共比较“数组长度”轮&#xff0c;每一轮都是从第一个元素开始比较&#xff0c;每一轮比较都会将一个元素固定到数组最后的一个位置。【其实就是不停的把元素往后堆&#…

LLaMA-Factory参数的解答

打开LLaMA-Factory的web页面会有一堆参数 &#xff0c;但不知道怎么选&#xff0c;选哪个&#xff0c;这个文章详细解读一下&#xff0c;每个参数到底是什么含义这是个人写的参数解读&#xff0c;我并非该领域的人如果那个大佬看到有参数不对请反馈一下&#xff0c;或者有补充的…

我于窗中窥月光,恰如仰头见“链表”(Java篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测

时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测 目录 时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测&#xff08;完整源码和数据…

算法学习——LeetCode力扣动态规划篇6

算法学习——LeetCode力扣动态规划篇6 121. 买卖股票的最佳时机 121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 描述 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&…

三元组数据模型:构建知识图谱的基石

目录 前言1. 三元组数据模型概述1.1 定义与结构1.2 特点 2. 三元组在知识图谱中的应用2.1 知识表示2.2 知识推理2.3 数据整合 3 三元组的数据格式3.1 N-Triples &#xff1a;3.2 RDF/XML &#xff1a;3.3 Turtle &#xff08;又称为 Terse RDF Triple Language&#xff09;&…

编程语言|C语言——数组与指针

一、数组 同一类型的变量——元素&#xff08;element&#xff09;集中在一起&#xff0c;在内存上排列成一条直线&#xff0c;这就是数组&#xff08;array&#xff09;。 1.1 一维数组 一维数组的声明 int arr1[10]; int arr2[2 8];#define N 10 int arr3[N];int count 10;…

JavaScript的学习笔记

<script src"index.js" defer></script>&#xff0c;defer的作用是延迟加载index.js文件 定义变量 变量的类型分为两大类&#xff1a;基本类型和复合类型 JavaScript是一种弱类型语言&#xff0c;所以没有强类型语言所具有的int,float,char等等&#x…

无药可医还能怎么办?越没本事的人,越喜欢从别人身上找原因!——早读(逆天打工人爬取热门微信文章解读)

无药可医的病该怎么办呢&#xff1f; 引言Python 代码第一篇 洞见 《骆驼祥子》&#xff1a;越没本事的人&#xff0c;越喜欢从别人身上找原因第二篇 人民日报 来啦 新闻早班车要闻社会政策 结尾 “吾日三省吾身&#xff0c;而后深知自助者天助之。” 在人生的迷宫中 遭遇困境时…

域环境共享文件夹,容量配额管理

首先&#xff0c;我们先创建一个新的磁盘&#xff0c;必须在服务器关机的状态下创建&#xff0c;只有在关机状态下才能创建NVMe类型的磁盘。 打开此电脑&#xff0c;右击创建的磁盘&#xff0c;点击属性。 点击共享&#xff0c;点击高级共享。 将共享此文件夹勾选上&#xff0c…