JavaEE 第4节 线程安全问题

news2024/9/28 3:19:31

小贴士:

本节题目所述的主题其实非常的庞大,如果要细讲起来,一篇博客远远不够,本篇博客只会每个方面的内容做一个简要描述详细的内容在后续同专栏博客中都会涉及到的,如果有需要可以一步到本专栏的其他博客

正文开始:

一、什么线程安全问题?

示例演示:

这里用一个直观的代码来展示一个经典的线程安全问题:

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


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

        //分别通过两个线程对count++

        t1.start();
        t2.start();
        t1.join();//join方法的作用是主线程运行到这了,先等待t1线程结束,在回来运行主线程。
        t2.join();//t2线程同理

        System.out.println("count="+count);//输出最后的count值
    }
}

按照正常的逻辑,count值因该是10000,可是:

很奇怪😕

这实际上就是出现了线程安全问题。

是什么原因导致这些问题的呢?

我们接下来将会详细讲解。

二、造成线程安全问题的主要原因

主要原因:

1、操作系统对线程的调度,在程序运行角度看是“随机的”(抢占式执行)

2、代码结构,即多个线程同时修改同一个变量。

3、修改变量这个操作不是原子性*的。

4、指令重排列(Instruction Reordering,后序章节详细讲解)

5、内存可见性(Memory Visibility,后续章节详细讲解

6、线程饿死(Thread Starvation,后续章节详细讲解

7、死锁(Deadlock,后序章节详细讲解)


注:
原子性*的意思是对于一个操作,结果只能是做了和没做两种状态,不能出现第三种状态。

刚才示例出现线程安全问题的原因就是1、2、3点导致的:

在代码中我们知道t1和t2两个线程时并发执行的,并且都对count变量进行++操作

而在CPU的视角看,count++操作要分成三步(不是原子的):

1)load:把count对应的内存数据写入寄存器。

2)add:逻辑运算单元对数据进行++操作。

3)save:把新的值重新写入count变量的内存。

t1和t2两个线程并发执行,都在不断按照上面这三步指令执行,在系统“随机”调度的过程中就很可能出现这样一种情况:

某一时刻,t1和t2同时load了count的内存数据,并且两个线程load的count值时一样的,然后他们分别对count++,最后写入内存(save)。会过头来我们发现,在这两个线程都运行完一次后,count只进行了一次++操作!


在深入问大家一个问题,程序中的count有没有可能小于5000呢?

答案是可能的

可能的情况举例:

t1和t2都只能对count++5000次,倘若又这样一个情况,t1刚开始被调度,读取到的count值是0,然后由于抢占式执行,t2开始被调度并且被多次连续调度,导致最后t2线程执行了4999次,之后t1又开始被调度把count=0写回原来的内存(形成了覆盖),然后t2又被调度了把count=0读取到逻辑运算单元,这是又由于抢占式执行,t2停止运作,t1开始被连续调度执行了5000,count被修改成了5000

现在只剩下t2还没有执行了,t2把count=0(在t2的逻辑运算单元上)++,对count进行覆写,count竟然还变成了1!

三、线程安全问题的解决办法

1、给线程加锁

像刚才的示例,我们可以通过设置多个变量的方式进行解决:

public class demo1 {
    public static int count = 0;
    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 < 5000; i++) {
                count1++;
            }
        });


        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count2++;
            }
        });
        //分别通过两个线程对count++
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        count=count1+count2;
        System.out.println("count="+count);
    }
}

不过这不是JAVA解决线程安全问题的主流方式,了解即可。

在JAVA中,主流解决线程安全问题的方式是对线程进行“加锁”的操作。

什么是锁?

刚才讲解线程安全问题的原因时,我们提到了原子性、修改同一个变量这两个关键字,这里所说的锁实际上就是把一些非原子性的程序“锁”起来,让它变成原子性的,这样线程安全问题就被解决了。

比如刚才的t1和t2线程,都对count进行++操作。但是由于系统的“随机”调度,两个线程的load、add、save操作是相互穿插进行的,数据的修改很可能会出错。
而现在把[load、add、save]这个非原子性的++操作进行“上锁”,保证要么++操作成功,要么什么都没有操作,既++操作变成了原子性的。
通过锁的这种操作,两个线程的【load、add、save】🔒就不可能穿插执行了,因为必须完成【】🔒内的操作,才能去执行另一个线程的任务。
这样线程安全问题就得到了解决。

synchronized关键字(加锁的工具)

synchronized基本用法:

Java提供了 synchronized 关键字 (监视器锁-monitor lock)来完成加锁操作。

接下来通过synchronized关键字,解决上面的线程安全问题:

public class demo1 {
    public static int count = 0;
    public static Object object1 = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (object1) {//括号内填写一个实例对象,任何类性的对象都是可以的!!
                    count++;
                }
            }
        });


        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {//括号内填写一个实例对象,任何类性的对象都是可以的!!
                synchronized (object1) {//代码块中填写,需要原子化的程序。
                    count++;
                }
            }
        });
        //分别通过两个线程对count++
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count=" + count);
    }
}

此时运行结果count=10000

synchronized除了上述写法,还可以通过修饰静态方法或者成员方法的方式,实现加锁:

public class Threads {
    static int count = 0;

    //实现加锁
    private synchronized static void add() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread1=new Thread(()->{
            for (int i = 0; i <5000 ; i++) {
                add();
            }
        });
        Thread thread2=new Thread(()->{
            for (int i = 0; i <5000 ; i++) {
                add();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);//这样可以极大增大 先让thread1和thread2两个线程先执行完,在打印count的概率
        System.out.println(count);
    }
}

成员方法是同理的,这里就不做过多演示了。

synchronized的一些基本特性:
   1)互斥(Mutual Exclusion)

进入synchronized代码块内,相当于上锁
退出synchronized代码块,相当于 解锁
对于同一个对象,如果一个线程上了锁,那么其他线程必须等待这个线程解锁,才能运行:

锁外其他的线程就处在BLOCK的等待状态。


图中的同一个对象是什么意思?

在刚才的代码演示中,t1和t2两个线程的synchronized括号里,填写的都是同一个对象。

如果两个线程填写不同的对象,跟没加锁没有区别,最后的count大概率也不可能等于一万。

也就是说,对于同一个对象加锁,锁对于一个线程来说才是有效的,或者说是存在的。

比如,我们对上面的代码进行简单的修改,t1线程和t2线程两个锁对象不同:

public class demo1 {
    public static int count = 0;
    public static Object object1 = new Object();
    public static Object object2 = new Object();

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


        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (object2) {
                    count++;
                }
            }
        });
        //分别通过两个线程对count++
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count=" + count);
    }
}

其中一个运行结果:

2)可重入(Reentrant)

如果重复对同一个线程进行这种加锁会怎么样:

  Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (object1) {//第一次锁了
                    synchronized(object1){//第二次我在锁?
                        count++;
                    }
                }
                
            }
        });

我们来慢慢分析:

第一次加锁:

本来t1线程可以安全的上侧所的,但是他还不放心,于是synchronized代码块里,又上了一次锁:

所以根据上面的逻辑,重复对一个线程针对同一个对象加锁是会出现锁被“焊死”的情况的(也就是死锁

这种重复锁导致的死锁,只会出现在C++\Python等其他编程语言中,Java不会,因为synchronized关键字会对这个情况进行判断,不会对相同对象的相同线程进行重复上锁

具体代码举例:

//先清楚标志位,然后抛出异常
public class Threads {
    static int count = 0;

    //实现加锁
    private synchronized static void add() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            synchronized (Threads.class){
                synchronized (Threads.class){
                    System.out.println("在第二个锁的内部");
                }
            }
        });
        Thread.sleep(1000);
        thread.start();
        thread.join();

        System.out.println("thread线程结束");

    }

}

以上代码在逻辑上是错误的,因为对同一个对象同一个线程重复上锁了,但是程序并没有卡主:

原因就是synchronized关键字会自动识别是重复上锁,如果有只会上锁一次。

那么如果没有synchronized的这个特性,程序会怎么样呢?

如图:

程序将会永远的停留在第22行和23行之间,在第22行第一次上锁后,程序需要等待第一次锁的解锁,才能在23行位置进行在次上锁,这样就形成了一个逻辑闭环,循环依赖,永远无法退出!(C++\Python等这些语言就有可能出现这种状况)


额外知识补充:

synchronized关键字是JVM提供的功能,synchronized底层实现就是依靠JVM中C++代码调用操作系统的API来实现的。而这些操作系统的API又是通过CPU上特殊的指令来实现上锁、解锁的。

2、volatile关键字

这个关键字是专门解决内存可见性问题的,这里不做过多解释,同专栏后续博客有详细讲解。

3、wait和notify方法

这两个方法是Object类自带的,用于解决线程饿死问题,这里不做过多解释,同专栏后续博客有详细讲解。


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

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

相关文章

Flows.network

Flows.network是什么 Flows.network 是一个 Severless 平台&#xff0c;用 Rust 和 Wasm 构建 LLM Agent 和机器人。flows.network 平台允许开发者将像 ChatGPT 和 Claude 的大模型连接到不同的 SaaS 提供商。由于模型已经具备与 OpenAI 兼容的 API&#xff0c;我们可以像在 O…

Photoneo PhoXi 3D Scanner XS

静态场景快照式3D扫描仪 PhoXi 系列是Photoneo公司专为静态场景高分辨率和高精度扫描工业场景应用设计生产的3D扫描仪&#xff0c;PhoXi 3D Scanner XS 型号专为精确地输出而设计&#xff0c;主要应用于印刷电路板、小部件的检测等。 产品特点 红色激光扫描&#xff0c;所见即…

商家转账到零钱现金营销场景申请一次通过攻略

商家转账到零钱现金营销场景是微信支付商家申请最多的场景之一&#xff0c;微信支付平台对申请材料的要求较多从而导致商家很容易被驳回&#xff0c;根据我们上万次成功申请的经验&#xff0c;申请该功能时商家要确保一次过审&#xff0c;需要遵循以下详细步骤和注意事项&#…

AR眼镜:重型机械维修保养新利器

重型机械作为工业与建设领域的重要支柱&#xff0c;其稳定运行直接影响效率与成本。然而在偏远地区&#xff0c;面临复杂故障和高昂维修成本&#xff0c;传统维修方式常显得力不从心。如今&#xff0c;安宝特的AR远程协助解决方案结合Vuzix AR眼镜&#xff0c;正悄然改变这一现…

人工智能系统测试中,A/B测试的流程、技术方法与策略

模型的迭代和优化是推动人工智能技术进步的核心动力。如何准确地评估模型更新前后的性能提升&#xff0c;是人工智能测试中的一个难点。A/B测试&#xff0c;作为一项科学且客观的测试方法&#xff0c;在此过程中发挥着至关重要的作用。本文我们继续为大家介绍人工智能系统测试方…

数据中台之数据开发,数据开发概述与数据计算能力的类型

目录 一、数据开发概述 二、数据计算能力的类型 2.1 概述 2.2 批计算 2.2.1 概述 2.2.2 批计算模型 2.2.2.1 传统数据处理方案的问题 2.2.2.2 MapReduce模型 2.2.2.3 Spark框架 2.3 流计算 2.4 批流一体 2.5 在线查询 2.6 即席分析 一、数据开发概述 数据开发是数…

做空日经指数的策略与时机

一、市场背景分析 在全球股市的剧烈波动中&#xff0c;日本股市的表现尤为引人关注。日经225指数在经历一轮暴跌后&#xff0c;又出现了大幅反弹&#xff0c;这种剧烈的波动为投资者提供了做空日经指数的机会。近期&#xff0c;日本股市受到日元汇率波动、日本央行货币政策以及…

文章复现 创新点 代码改进跑通 深度学习

文章复现 创新点 代码改进跑通 深度学习 SCI代码复现 文章复现 数据处理、数据分析、算法、数据结构data structure、机器学习machine learning、深度学习deep learning。创新改进&#xff0c;算法提升 python环境配置 &#xff0c;深度学习代码调试 、代码复现&#xff0c;py…

WebStorm格式化JSON,将一行很长的JSON展开

webstorm json格式化插件将一行很长的json展开 在WebStorm中&#xff0c;要展开很长的JSON行&#xff0c;可以使用内置的JSON格式化功能。 打开WebStorm&#xff0c;并打开包含JSON的文件。 选择JSON文件中的任意部分。 按下快捷键 CtrlAltL (Windows/Linux) 或 CmdAltL (Ma…

用4点结构标定3点结构的顺序

在行列可自由变换的条件下&#xff0c;平面上的4点结构只有16个 (A,B)---6*30*2---(0,1)(1,0) 让A分别是4a1&#xff0c;2&#xff0c;…&#xff0c;16&#xff0c;让B全是0。当收敛误差为7e-4&#xff0c;收敛199次取迭代次数平均值&#xff0c;得到 迭代次数 搜索难度 1 …

免费开源的高科技行业ERP解决方案

引言 协助高科技企业信息化转型升级&#xff0c;通过开源智造Odoo标准模块与拓展模块&#xff0c;实现精细化、数字化与智能化的管控&#xff0c;将线性供应链转变为智能供应网络&#xff0c;形成整合与集成化的管理系统&#xff0c;提高对市场的响应速度。 业务挑战 项目管控难…

PDF文档处理技巧:如何旋转 PDF 文档

你有没有遇到过一个PDF文档&#xff0c;有些页面是侧向的&#xff0c;而其他页面却显示正确&#xff1f;这可能会令人沮丧&#xff0c;尤其是在信息至关重要的情况下。好消息是&#xff0c;您可以通过旋转受影响的页面轻松解决此问题。本指南将重点介绍如何使用奇客PDF 旋转 PD…

进程无响应任务管理器关不掉解决办法

第一步&#xff1a;打开任务管理器——>详细信息 第二步&#xff1a;打开运行窗口 第三步&#xff1a;输入taskkillPID(PID在图一的位置上看)

c语言小知识点小计

c语言小知识点小计 1、运算符的优先级 运算符的优先级是和指针解引用*的优先级相同的&#xff0c;但在代码运行中执行顺序是从后往前的。因此下面代码 int a[10] {1,2,3,4}; int* arr a; printf("%d",*arr);//访问的值是2 //注意&#xff1a;printf("%d&qu…

​私有化地图离线部署详细方案

我们在《私有化地图离线部署整体解决方案》一文中&#xff0c;分享了不包含硬件的私有化部署整体解决方案。 现在&#xff0c;再为你分享一下从硬件配置、系统选型、系统部署到地图数据加载、导出和更新管理等一整套详细方案。 地图服务器与存储设备 全球私有地图离线部署对…

餐饮业的数字化突围:价格战下的转型与新生

原文链接&#xff1a;https://tecdat.cn/?p37241 餐饮业价格战升级了&#xff0c;越打越激烈。近日&#xff0c;各餐饮巨头也被迫纷纷下场。 “太二酸菜鱼客单价跌至七年前” “9.9元就可以点上海底捞的一份锅底” “必胜客推出人均20元的乐享店”…… 消费降级的时代潮水&am…

将 Tcpdump 输出内容重定向到 Wireshark

在 Linux 系统中使用 Tcpdump 抓包后分析数据包不是很方便。 通常 Wireshark 比 tcpdump 更容易分析应用层协议。 一般的做法是在远程主机上先使用 tcpdump 抓取数据并写入文件&#xff0c;然后再将文件拷贝到本地工作站上用 Wireshark 分析。 还有一种更高效的方法&#xf…

KVM安装与虚拟机中安装虚拟机教程

目录 一. 安装KVM 二. 图形化安装——虚拟机中安装虚拟机 一. 安装KVM 确保用root用户进入系统&#xff0c;如果已经是root登录的用户则不用做此步骤 查看ip连接终端 修改主机名用来区分 [rootlocalhost ~]# hostnamectl set-hostname kvm-server [rootlocalhost ~]# bash…

使用SpringBoot+Vue3开发项目(1)---- 设计用户注册和登录的接口及页面

目录 一.所用技术栈&#xff1a; 二.前端创建工程&#xff1a; 1.使用elementplus展开前端页面格式布局&#xff1a; 2.基于Vue3的使用来实现登录与注册&#xff1a; &#xff08;1&#xff09;定义数据模型&#xff1a; &#xff08;2&#xff09;:model绑定表单&#xf…

WPF学习(9)-CheckBox复选框+RadioButton单选框+RepeatButton重复按钮

CheckBox复选框 CheckBox继承于ToggleButton&#xff0c;而ToggleButton继承于ButtonBase基类。 案例 前端代码 <StackPanel Orientation"Horizontal" HorizontalAlignment"Center" VerticalAlignment"Center"><TextBlock Text"…