<多线程章节四>如何使用synchronized解决线程不安全问题(原子性、内存可见性…)等等

news2024/11/18 19:38:15

文章目录

  • 💡专栏导读
  • 💡文章导读
  • 💐线程不安全示例
  • 💐锁的特性
  • 💐产生线程不安全的原因:
  • 💐加锁的三种方式:

💡专栏导读

本篇文章收录于多线程,也欢迎翻阅博主的其他文章,可能也会让你有不一样的收获😄
🍁多线程 🌴数据结构 🌾JavaSE

💡文章导读

本片文章主要通过讲解线程不安全的例子,进而得出线程产生不安全的原因以及解决方法,同时,也列出了三种synchronized的使用方法,请一定按照章节一章一章的看,看完之后,所有的知识就会连接起来,形成属于你的知识结构;

💐线程不安全示例

对于线程安全问题,这里用一个例子进行讲解👇:

我现在定义一个变量count, 使用两个线程对这个count进行自增,每一个线程中,count都自增10000, 那么,两个线程执行结束之后,最后的count应该是20000,下面验证一下:

public class Demo1 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                //count自增10000次
                count++;
            }
        });
        Thread thread2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++){
                //count自增10000次
                count++;
            }
        });
        
        thread1.start();
        thread2.start();
        //让主线程阻塞等待
        thread1.join();
        thread2.join();
        //两个线程执行完毕后,打印最后结果
        System.out.println("count:"+count);
    }
}

第一次打印:

在这里插入图片描述

第二次打印:

在这里插入图片描述

第三次打印:

在这里插入图片描述

通过以上代码的验证得到,最后的结果与我们预期的结果完全不一样,而且,每次运行都会产生不一样的结果;以上代码如果放在一个线程中去运行,那是没有任何问题的,放在多线程中就出现了问题,以上情况就是一个非常典型的线程安全问题;

那么我们修改一下代码:

将thread1的join放在thread1的start后面,最后运行的结果就是对的!!!

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

这个同时执行是很关键的,既然同时执行时会产生线程不安全问题,那么有没有办法解决这样的问题呢?答案是:有的

首先,count++ 这个操作,站在CPU的角度上是由三步操作完成的:

1.load操作,把数据从内存中拿出来放到寄存器中

2.add操作,把count的值进行+1

3.save操作,把寄存器中的值放到内存中

以两个线程为例,因为线程是随机调度执行的,所以这三步操作就有以下的操作顺序:

在这里插入图片描述

上述情况,有些是对的,有些是错误的,例如,第一和第二种情况就是正确的,下面我们演示一下以上这三步在CPU核心上的操作步骤,观察以下count是如何完成自增操作的;

假设,thread1 和 thread2这两个线程是在两个CPU核心上执行的,当然,在一个CPU核心上也是没有问题的,因为会有对应的上下文进行记录,为了好理解,下面我们假设在两个CPU核心上:

在这里插入图片描述

交叉执行的情况就会产生bug👇

在这里插入图片描述

由于在这20000次中,我们不知道有多少次是按照1、2这种正确的方式自增的,又有多少次按照错误的方式自增的,所以最后就会产生一个小于20000的随机值;对于上述情况,就可以使用加锁操作👇

💐锁的特性

在Java中,对线程加锁主要就是使用synchronized;

synchronized在使用时,需要修饰一个代码块,例如以下:

在这里插入图片描述

锁的特性主要分为两点:

(1)互斥特性

  • 进入synchronized修饰的代码块,相当于加锁
  • 退出synchronized修饰的代码块,相当于解锁

一个线程在加锁情况下,另一个线程也想要进行加锁,这样就会产生“锁冲突/锁竞争”,获得锁的线程就会继续执行,没有获得锁的线程就会阻塞等待,直到得到锁的线程释放锁为止;

如何理解阻塞等待?

例如这样一个场景,我们在排队上厕所时,当张三进入厕所后,就会将们锁住,这时候,李四和王五就只能在原地等待,等到张三出来以后,李四和王五才能去上厕所;

注意:

1.张三出厕所以后,李四和王五并不会按照顺序上厕所,而是会进行一个抢厕所,这就相当于,虽然现在有多个线程,但是在获取锁时,并不会按照先来后到的顺序获取,而是会进行争抢

2.上一个线程释放锁之后,下一个线程并不是直接就可以获取锁,而是需要操作系统进行一个“唤醒”的过程,这个过程是操作系统线程调度的工作部分

(2)刷新内存

synchronized的工作过程:

1.尝试获得锁

2.从主内存拷贝变量最新内容到工作内存

3.执行代码

4.将更改后的变量刷新到主内存中

5.释放锁

所以,synchronized也可以保证内存可见性,关于内存可见性,请看这篇文章👉volatile解决内存可见性问题

对上述自增例子的代码进行加锁:

public class Demo1 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //对代码进行加锁
                for(int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (obj) {
                for(int i = 0; i < 10000; i++){
                    //count自增10000次
                    count++;
                }
            }
        });

        thread1.start();
        thread2.start();
        //两个线程执行完毕后,打印最后结果
        System.out.println("count:"+count);
    }
}

在这里插入图片描述

💐产生线程不安全的原因:

1.线程的调度顺序是随机的,是抢占式执行的

2.两个线程针同一个变量进行修改

1.两个线程针对两个不同的变量进行修改

2.一个线程针对一个变量进行修改

3.两个线程针对同一个线程进行读取

以上改正后的三点就不会发生不安全问题;

3.修改操作不是原子的

什么是原子性:一个线程的操作不能被其他线程打断,同一时间只有一个线程对一个变量进行操作,在多线程模式下,每个线程执行结果不受其他线程的干扰;,

4.内存可见性问题👉内存可见性问题

5.指令重排序问题👉指令重排序问题

那么,如何将不安全改成安全的呢?

首先,第一条原因是没办法修改的,因为线程的抢占式执行是由操作系统决定的,我们是无法修改的,第二条原因的话,针对有些代码,可能可以经过代码的调整来避免,但是,有些代码可能无法调整,就比如我们写的这个,我们的目的就是让count在两个线程中进行自增,所以第二条原因需要针对代码可以调整的情况,所以,第三条原因就可以进行修改,将不是原子的操作改为原子的;

所以,通过加锁就可以解决上述非原子性问题👇

💐加锁的三种方式:

1.synchronozed修饰代码块👇

  Object obj = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (obj) {
                //对代码进行加锁
                for(int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        });

2.修饰实例方法👇那个对象调用increase,锁的就是哪个对象

class Counter{
    public static int count;
    //谁调用这个方法,谁就是锁对象
    //写法一:
    synchronized public void increase() {
        count++;
    }
    //写法二:
    public void increse() {
        synchronized(this) {
            count++;
        }
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
       Counter counter = new Counter();
        
       Thread thread1 = new Thread(() -> {
           for(int i = 0; i < 10000; i++) {
               counter.increase();
           }
       });

        thread1.start();
        System.out.println(Counter.count);
    }
}

在这里插入图片描述

3.修饰静态方法👇

如果是修饰静态方法,相当于是对“类对象”进行了加锁;

class Counter{
    public static int count;
    //写法一:
    synchronized public static void increase1() {
        count++;
    }
	//写法二
    public static void increase2() {
        //对类对象加锁
        synchronized (Counter.class) {
            count++;
        }
    }
}

在这里插入图片描述

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

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

相关文章

mavros黑白名单设置

链接: mavros设置黑白名单 设置mavros黑白名单主要是通过插件的参数进行设置&#xff0c;如下&#xff1a; 这里是在px4_pluginlists.yaml参数文件中设置 plugin_blacklist: # common - safety_area - 3dr_radio - actuator_control - hil_controls - ftp - global_position …

表的约束【MySQL】

文章目录 什么是约束DEFAULT&#xff08;默认约束&#xff09;NULL 与 NOT NULL&#xff08;非空约束&#xff09;COMMENT&#xff08;注释约束&#xff09;ZEROFILL&#xff08;零填充约束&#xff09;UNIQUE&#xff08;唯一键约束&#xff09;*PRIMARY KEY&#xff08;主键约…

Windows 10/11如何恢复永久删除的文件?

数据丢失在我们的工作生活中经常发生。当你决定清理硬盘或U盘时&#xff0c;你会删除一些文件夹或文件。如果你通过右键单击删除文件&#xff0c;则可以很容易从回收站恢复已删除的文件。但是&#xff0c;如果你按Shift Delete键、清空回收站或删除大于8998MB的大文件夹&#…

“停车费”用英语怎么说?千万不要说Stop car money!柯桥BEC商务英语学习

“200块的大餐说吃就吃 20块的停车费不是要我命吗” 年轻人主打一个该省省、该花花 说到“停车费”你知道用英语怎么说吗 难道是“stop car money”? 哈哈&#xff0c;这样说老外表示15857575376太中式啦&#xff01; C姐教你地道的英语表达 01 “停车费”用英语怎么说&a…

Python中如何使用ThreadPoolExecutor一次开启多个线程

目录 一、ThreadPoolExecutor的创建与使用 二、处理并发执行结果 三、异常处理 五、使用多线程注意事项 总结 在Python中&#xff0c;ThreadPoolExecutor是concurrent.futures模块提供的一种线程池类。它能够以线程的形式执行可调用对象&#xff0c;并允许您在执行过程中获…

教你如何帮助孩子做好时间管理,不再需要重复提醒!

给大家推荐一个小工具&#xff0c;不起眼 但是经过几个月的使用 发现相见恨晚 我家熊孩子怎么也改不掉的拖沓毛病 竟然被它治好了 经常会听到姐妹抱怨 自己家娃有“假期拖延症” 明明十几分钟就能写完的作业 一会说肚子疼想上厕所 一会又拿出铅笔刀要削铅笔 非得拖拖拉…

安卓开发实例:首页

导航菜单&#xff0c;点击按钮跳转。 activity_main.xml <?xml version"1.0" encoding"utf-8"?> <androidx.constraintlayout.widget.ConstraintLayoutxmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools&quo…

c++ 构造函数与析构函数(详解)

目录 目录&#xff1a; 1:构造函数 2:析构函数 前言&#xff1a;我们知道c使用起来是比c语言方便的&#xff0c;那么为啥比他使用起来简单&#xff0c;这里我们就不得不提及我们c中默认成员函数了&#xff0c;是它们默默的承担了所有。 下面让我们先来学习前两个默认成员函数:…

JavaScript基础知识18——逻辑运算符之短路运算

哈喽&#xff0c;大家好&#xff0c;我是雷工。 本节学习JavaScript基础知识——逻辑运算符中的短路运算&#xff0c;以下为学习笔记。 规则&#xff1a; 1、如果是&&运算&#xff0c;只要遇到false&#xff0c;就立即短路&#xff0c;不会再执行了&#xff0c;直接返回…

在声明和定义的一些小坑

1、静态成员变量的初始化 静态成员变量声明在 .h 头文件文件中&#xff0c;初始化应该在 .cpp 源文件中 就会出现"找到一个或多个多重定义的符号",下面的错误 class MyString{public:typedef char* iterator;typedef const char* const_iterator;iterator begin();…

复古风再现:探索70年代风格的室内设计在当今的复兴之路

20 世纪 70 年代的室内设计趋势正在卷土重来。大地色调、有趣的多色概念&#xff0c;以及低矮的软家具&#xff0c;都是当前的流行趋势。今年 2 月&#xff0c;许多室内设计师也表达了类似的观点&#xff0c;他们都在追捧备受诟病的棕色。当时的设计风趣、性感&#xff0c;但又…

YOLO目标检测——红外车辆行人数据集【(含对应voc、coco和yolo三种格式标签+划分脚本+训练教程】

实际项目应用&#xff1a;智能驾驶、智能监控、军事应用监控通过红外传感器采集车辆和行人的红外图像&#xff0c;然后使用目标检测算法对图像进行处理和分析&#xff0c;以识别道路上的车辆和行人。数据集说明&#xff1a;&#xff0c;真实场景的高质量图片数据&#xff0c;数…

Win11安装ise14.7~不需要虚拟机了~

之前一直无法在win11上安装ise14.7&#xff0c;网上搜索也无果&#xff0c;所有一直vmware虚拟机使用。直到最近看了水木上jesce的回复&#xff0c;试了下果然可以直接安装使用的。 步骤如下即可&#xff1a; 1.安装时切勿勾选最后一项&#xff0c;Enable WebTalk to send so…

Stable DiffusionAI绘画作品展示

参考链接&#xff1a; https://chat.xutongbao.top/

C语言实现图形界面创建按钮

文章目录 其他章节创建按钮指定按钮显示的文本 按钮样式 其他章节 开始之前&#xff0c;需要学习以下章节&#xff1a; 创建窗口 窗口过程函数 创建按钮 要在窗口上显示一个按钮&#xff0c;我们可以在窗口过程函数中处理 WM_CREATE 消息&#xff0c;在该消息中创建按钮并设…

我是如何走上测试管理岗的

最近有小伙伴问了一个问题&#xff1a;他所在的测试团队规模比较大&#xff0c;有 50 多个人&#xff0c;分成了 4 ~ 5 个小组。这位同学觉得自己的技术能力在团队里应该属于比较不错的&#xff0c;但疑惑的是在几次组织架构调整中&#xff0c;直属领导一直没有让他来管理一个小…

优先级反转,优先级继承和优先级天花板协议

优先级反转 优先级反转是RTOS&#xff08;实时操作系统&#xff09;会遇到的一个问题&#xff0c;简单来说就是由于调度原因&#xff0c;让原本优先级较高的任务慢于优先级较低的任务完成&#xff0c;比如下面这种情况&#xff0c;任务1、2、3的优先级依次升高&#xff0c;其中…

Jetpack:019-Jetpack的导航二(传递数据)

文章目录 1. 知识回顾2. 使用方法2.1 通过参数传递数据2.2 获取参数中的数据2.3 共享导航控制器 3. 示例代码4. 内容总结 我们在上一章回中介绍了Jetpack中导航相关的内容&#xff0c;本章回中 继续介绍导航相关的内容。闲话休提&#xff0c;让我们一起Talk Android Jetpack吧…

注意力机制、Transformer模型、生成式模型、目标检测算法、图神经网络、强化学习、深度学习模型可解释性与可视化方法等详解

采用“理论讲解案例实战动手实操讨论互动”相结合的方式&#xff0c;抽丝剥茧、深入浅出讲解注意力机制、Transformer模型&#xff08;BERT、GPT-1/2/3/3.5/4、DETR、ViT、Swin Transformer等&#xff09;、生成式模型&#xff08;变分自编码器VAE、生成式对抗网络GAN、扩散模型…

Go学习第十二章——Go反射与TCP编程

Go反射与TCP编程 1 反射1.1 基本介绍1.2 快速入门1.3 注意事项和细节说明1.4 最佳实践 2 Tcp Socket编程2.1 基本介绍2.2 入门案例2.3 服务器监听2.4 服务器接受客户端消息 1 反射 1.1 基本介绍 **反射&#xff1a;**在编译时静态类型语言中实现动态特性的一种机制。 Go语言…