如何解决线程安全问题(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次中,我们不知道有多少次是按照正确的方式自增的,又有多少次按照错误的方式自增的,所以最后就会产生一个小于20000的随机值;对于上述情况,就可以使用 **“加锁操作“**👇

💐锁的特性

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

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

在这里插入图片描述

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

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)互斥特性

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

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

如何理解阻塞等待?

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

注意:

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

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

(2)刷新内存

synchronized的工作过程:

1.尝试获得锁

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

3.执行代码

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

5.释放锁

所以,synchronized也可以保证内存可见性

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

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

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

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

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

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

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

3.修改操作不是原子的

什么是原子性:就像最开始讲的 load,add,save 指令,它们是三个指令,需要一个一个执行,所以它们不是原子的,如果它们三个指令发生的动作能够一个指令就完成,或者说在它们三个指令执行的过程中,不会被其他的指令进行穿插,这就是原子的。

4.内存可见性问题

5.指令重排序问题

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

首先,第一条原因是没办法修改的,因为线程的抢占式执行是由操作系统决定的,我们是无法修改的,第二条原因的话,针对有些代码,可能可以经过代码的调整来避免,但是,有些代码可能无法调整,就比如我们写的这个,我们的目的就是让count在两个线程中进行自增,所以第二条原因需要针对业务的情况,第三条原因就可以进行修改,将不是原子的操作改为原子的,通过加锁就可以解决上述非原子性问题,内存可见性和指令重排序问题也可以通过 Volaite 来解决。

💐加锁的三种方式

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++;
        }
    }
}

在这里插入图片描述

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

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

相关文章

Image Fusion via Vision-Language Model【文献阅读】

阅读目录 文献阅读AbstractIntroduction3. Method3.1. Problem Overview3.2. Fusion via Vision-Language Model 4. Vision-Language Fusion Datasets5. Experiment5.1Infrared and Visible Image Fusion 6. Conclusion个人总结 文献阅读 原文下载&#xff1a;https://arxiv.or…

串及BF朴素查找算法(学习整理):

关于串的相关定义&#xff1a; 串&#xff1a;用‘ ’表示的字符序列空串&#xff1a;包含零个字符的串子串&#xff1a;包含传本身和空串的子串 eg: abc(,a,b,c,ab,bc,ac,abc)共7个&#xff1a;串的长度的阶乘1&#xff08;空串&#xff09;真子串&#xff1a;不包含自身的所…

Java进阶-IO(3)

话接上回&#xff0c;继续java IO的学习。上一次说完了字符流的读写数据&#xff0c;这次将基础部分剩余的一点内容看完。 一、流按功能分类 1、系统流 1.1 概述 系统流的类为 java.lang.System。Sytem 类封装了 Java 程序运行时的 3 个系统流。 System.in&#xff1a;标…

腾讯云幻兽帕鲁服务器中,如何检查并确保所有必要的配置文件(如PalWorldSettings.ini和WorldOption.sav)正确配置?

腾讯云幻兽帕鲁服务器中&#xff0c;如何检查并确保所有必要的配置文件&#xff08;如PalWorldSettings.ini和WorldOption.sav&#xff09;正确配置&#xff1f; 登录腾讯云控制台&#xff1a;登录轻量云控制台&#xff0c;找到部署了幻兽帕鲁的服务器&#xff0c;单击实例卡片…

基于BP-Adaboost的预测与分类,附MATLAB代码免费获取

今天为大家带来一期基于BP-Adaboost的预测与分类。代码中的BP可以替换为任意的机器学习算法。 原理详解 BP-AdaBoos模型先通过 AdaBoost集成算法串行训练多个基学习器并计算每个基学习 器的权重系数,接着将各个基学习器的预测结果进行线性组合,生成最终的预测结果。关于更多的原…

关于编写测试用例的一些思考

测试用例是QA同学的基本功&#xff0c;每个人都有一套编写测试用例的体系&#xff0c;本文是作者结合自身的工作经验以及阅读一些测试相关的书籍后的一些看法&#xff0c;欢迎大家一起讨论学习。 测试设计 测试用例格式 面试中一些常见的问题 1.APP测试与服务端测试的区别&am…

计算机设计大赛 深度学习火车票识别系统

文章目录 0 前言1 课题意义课题难点&#xff1a; 2 实现方法2.1 图像预处理2.2 字符分割2.3 字符识别部分实现代码 3 实现效果4 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 图像识别 火车票识别系统 该项目较为新颖&#xff0c;适…

StarRocks实战——首汽约车实时数仓实践

目录 前言 一、引入背景 二、OLAP引擎选型 三、架构演进 四、实时数仓构建 五、业务实践价值未来规划 原文大佬的这篇首汽约车实时数仓实践有借鉴意义&#xff0c;这里摘抄下来用作学习和知识沉淀。 前言 首汽约车&#xff08;以下简称“首约”&#xff09;是首汽集团打造…

滑动窗口问题

日升时奋斗&#xff0c;日落时自省 目录 一、长度最小的子数组 二、无重复字符的最长子串 三、最大连续1的个数III 四、将x减到0的最小操作数 五、水果成篮 六、找到字符串中所有字母异位词 七、串联所有单词的⼦串 八、最小覆盖子串 注&#xff1a;滑动窗口其实很类似…

图片按照宽度进行居中裁剪,缩放大小

要求 文件存放在img_folder_path中 裁剪要求&#xff1a; 图片大小以高度为基准。居中裁剪 缩放要求&#xff1a; 图片缩放到512大小 图片另存到save_file_path路径中 代码 import numpy as np import cv2 import os from tqdm import tqdm#原图片存放位置 img_folder_p…

操作系统原理与实验——实验三优先级进程调度

实验指南 运行环境&#xff1a; Dev c 算法思想&#xff1a; 本实验是模拟进程调度中的优先级算法&#xff0c;在先来先服务算法的基础上&#xff0c;只需对就绪队列到达时间进行一次排序。第一个到达的进程首先进入CPU&#xff0c;将其从就绪队列中出队后。若此后队首的进程的…

Spring重点记录

文章目录 1.Spring的组成2.Spring优点3.IOC理论推导4.IOC本质5.IOC实现&#xff1a;xml或者注解或者自动装配&#xff08;零配置&#xff09;。6.hellospring6.1beans.xml的结构为&#xff1a;6.2.Spring容器6.3对象的创建和控制反转 7.IOC创建对象方式7.1以有参构造的方式创建…

【硬件相关】RDMA网络类别及基础介绍

文章目录 一、前言1、RDMA网络协议2、TCP/IP网络协议 二、RDMA类别1、IB2、RoCE3、iWARP 三、RDMA对比1、优缺点说明a、性能b、扩展性c、维护难度 2、总结说明 一、前言 roce-vs-infiniband-vs-tcp-ip RoCE、IB和TCP等网络的基本知识及差异对比 分布式存储常见网络协议有TCP/IP…

【【C语言简单小题学习-1】】

实现九九乘法表 // 输出乘法口诀表 int main() {int i 0;int j 0;for (i 1; i < 9; i){for (j 1; j < i;j)printf("%d*%d%d ", i , j, i*j);printf("\n"); }return 0; }猜数字的游戏设计 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdi…

c语言--qsort函数(详解)

目录 一、定义二、用qsort函数排序整型数据三、用qsort排序结构数据四、qsort函数的模拟实现 一、定义 二、用qsort函数排序整型数据 #include<stdio.h> scanf_S(int *arr,int sz) {for (int i 0; i < sz; i){scanf("%d", &arr[i]);} } int int_cmp(c…

点云数据结构化与体素化理论学习

一、PCD点云数据存储格式的进一步认识 &#xff08;一&#xff09;PCD点云存储格式相较于其它存储格式&#xff08;如PLY、STL、OBJ、X3D等&#xff09;的优势[1] &#xff08;1&#xff09;具有存储和处理有组织的点云数据集的能力&#xff0c;这对于实时应用和增强现实及机器…

GEE入门篇|图像处理(三):阈值处理、掩膜和重新映射图像

阈值处理、掩膜和重新映射图像 本章前一节讨论了如何使用波段运算来操作图像&#xff0c; 这些方法通过组合图像内的波段来创建新的连续值。 本期内容使用逻辑运算符对波段或索引值进行分类&#xff0c;以创建分类图像。 1.实现阈值 实现阈值使用数字&#xff08;阈值&#xf…

YOLOv9独家原创改进|增加SPD-Conv无卷积步长或池化:用于低分辨率图像和小物体的新 CNN 模块

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; 一、文章摘要 卷积神经网络(CNNs)在计算即使觉任务中如图像分类和目标检测等取得了显著的成功。然而&#xff0c;当图像分辨率较低或物体较小时&…

电源通常向计算机内部的各种组件提供的三种电压:1

本文将与您分享电源通常为计算机内部各个组件提供的三种电压是什么。 小编觉得还是比较实用的&#xff0c;所以分享给大家&#xff0c;作为参考。 下面就跟随小编一起来看看吧。 电源通常为电脑内部的各个部件提供三种电压&#xff1a; 1&#xff0e; 5V&#xff0c;主要供给主…

【k8s管理--两种方式安装prometheus】

1、k8s的监控方案 1.1 Heapster Heapster是容器集群监控和性能分忻工具&#xff0c;天然的支持Kubernetes和CoreOS。 Kubernetes有个出名的监控agent–cAdvisor。在每个kubernetes Node上都会运行cAdvisor&#xff0c;它会收集本机以及容器的监控数(cpu,memory,filesystem,ne…