线程安全以及解决方案

news2024/12/23 17:37:05

文章目录

  • 1.线程安全的原因
      • ①抢占式执行
      • ②多线程修改同一个变量
      • ③修改的操作不是原子的
      • ④内存可见性
      • ⑤指令重排序
  • 2. 线程安全的解决方案
  • 3 synchronized的特性------可重入锁

1.线程安全的原因

①抢占式执行

操作系统对线程的调度是随机的,没有规律(主要原因)

例如:定义了一个变量count,执行count++这种操作,本质上是三个CPU指令,load(将count的值读入cpu寄存器中)、add(将寄存器的数据进行+1)、save(将寄存器中的数据读入到内存中),而CPU执行指令都是以一个指令为单位顺序进行的,试想,有两个线程同时执行count++操作,这些一个一个的指令就会抢占执行,线程一的add的操作刚完,线程二的add就抢占了下一个位置…

线程的调度是随机的,在有些调度下,代码的逻辑会出现问题,结果会与预计结果不同,但这个是内核实现的,没有办法改变

②多线程修改同一个变量

当多线程修改同一个变量时,会出现问题。一个线程修改一个变量,结果不会出现问题,多线程修改不同的变量也不会出现问题,多线程读取同一个变量也不会出现问题。

就像刚刚提到的抢占式执行的例子,如果一个变量count,进行count++这种操作,分load、add、save,要说线程一二修改不同变量倒也没事,互不干扰,然如果修改同一变量,就会出现以下情况:
在这里插入图片描述
如上,这两种是正常情况,这两种执行结果与预期结果相符,但更多的是出现下面的情况:
在这里插入图片描述
上面只是列举了两种异常情况,实际上的异常情况更多,线程调度的顺序是随机的,两个线程的执行顺序有无数种,在有些调度顺序下,代码逻辑就会出现问题,发生线程安全问题。

总结:这里确实可以通过调整代码,来避免线程安全问题,但是以及适用性不高;

③修改的操作不是原子的

原子表示不可分割的最小单位,CPU执行指令是一条一条执行的,这一条一条的指令就可以理解为原子,也正因为count++不是原子的才会引发上述的多线程修改同一变量会引发线程安全;

结论:既然上述1,2都没有方法很好的解决线程安全问题,那么咱就试试从这入手——修改操作,使其是原子的,也就是说,咱可以把这些多个原子操作包装成一个原子操作!(例如可以把刚刚所说的count++这个例子的的三条指令包装成一个);

④内存可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

例如,一个线程负责读数据,另一个线程负责修改数据:

 	private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (isQuit == 0) {   //次数过多编译器会进行优化,volatile防止JVM优化
                // 循环体里啥都没干.
                // 此时意味着这个循环, 一秒钟就会执行很多很多次.
            }
            System.out.println("t1 退出!");
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            System.out.println("请输入 isQuit: ");
            Scanner scanner = new Scanner(System.in);
            // 一旦用户输入的值, 不为 0, 此时就会使 t1 线程执行结束.
            isQuit = scanner.nextInt();
        });
        t2.start();
    }

这里的while(isQuit == 0),就是要先从内存中读取isQuit 的值(LOAD操作),再到寄存器中读取isQuit 的值与0进行比较(CMP操作),这里while会循环的进行这个操作(非常快),而我们知道的是,CPU读写数据最快,内存次之(与CPU差3 ~ 4个数量级),硬盘最慢(与内存差3 ~ 4个数量级);所以LOAD从内存中读取数据操作的速度相对于在CPU上进行CMP操作就要慢的多,那么编译器就要偷懒了,既然频繁的LOAD读取isQuit 这个数据,多次执行的结果还都是一样,干脆LOAD就只执行一次将CPU读内存的操作变成读取寄存器,减少读取内存的操作,也可以提高整体程序的效率。
在这里插入图片描述
运行上述代码:
在这里插入图片描述

分析:这时可以发现, 当输入数字5时,相当于修改了isQuit 这个变量的值为5,按理来说t1线程的run方法中isQuit 只要不等于0就会停下来,可是程序依旧没有停止,就出现了内存可见性问题,直接读取寄存器的值,而没有读取我们修改之后的值;

编译器优化,在多线程情况下可能存在误判——使用volatile关键字,可以告诉JVM不允许优化

private static volatile int isQuit = 0;

在这里插入图片描述
可以看到,当我们线程2一修改isQuit的值,线程1就停止运行了。

volvatile 关键字有如下两大作用:

  1. 禁止指令重排序:保证指令执行的顺序,防止 JVM 出于优化而修改指令执行顺序,引发线程安全问题。
  2. 保证内存可见性:也就是说,保证了我们读取到的数据是内存中的数据,而不是缓存,具体的,当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。

Java 内存模型 (JMM):
Java虚拟机规范中定义了Java内存模型. 目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

⑤指令重排序

一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

总结:JVM的代码优化在多线程情况下,也会带来一些BUG;

2. 线程安全的解决方案

上面提到操作不是原子的,我们可以从这里入手,将count++这个操作的三个布置包装成一个步骤变成原子的,如何做呢——“加锁”;count++之前加锁,count++之后再解锁,别的线程若是想在加锁和解锁之间进行需修改,很抱歉,修改不了,别的线程只能处于阻塞等待的线程状态(BLOCKED状态);

Java的代码中如何进行加锁呢?

使用synchronized关键字,synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待(BLOCKED状态).

  • 进入 synchronized 修饰的代码块, 相当于加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁
    //对实例方法加锁
    synchronized public void increase(){
        count++;
    }

在这里插入图片描述

这个锁具体是怎么执行的呢?
锁具有抢占特性,如果这个锁没人加,有人想加,就可以立即加上,若这个锁以及被人加上了,加锁操作就会阻塞等待;如刚才的栗子,count++分三步进行,load、add、save,而线程调度是随机的过程,一旦这两个线程同时调用,这两组三个操作就会进行排列组合,就会产生线程不安全,现在使用锁,就可以使这三个操作串行执行了;如下
在这里插入图片描述
此时,并发执行就变成了串行执行,这个操作就会减慢执行效率,但是保证了线程安全

3 synchronized的特性------可重入锁

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

“不可重入锁”:

// 第一次加锁, 加锁成功 
lock(); 
// 第二次加锁, 锁已经被占用, 阻塞等待.  
lock();

一个线程没有释放锁, 然后又尝试再次加锁. 按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放,才能获取到第二个锁.但想要第一把锁解锁,需要执行完synchronized代码块,才可以加下一把锁,然而第二把锁一直在阻塞等待,所以第一把锁既不能解锁,第二把锁也不能加锁,就卡在这里了;
并且,有时候由于多次嵌套,无法直接观察出是否多次加锁:

   public static  Object locker = new Object();
   public  static void increase1(){
     synchronized (locker){
        }
    }
    public static void increase2(){
       increase3();
    }
    public static void increase3(){
        increase4();
    }
    public  static void increase4(){
         //可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
        synchronized (locker){  //synchronized属于可重入锁,防止多次加锁,产生死锁
        }
    }

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.

代码示例:在下面的代码中, increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的. 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)这个代码是完全没问题的. 因为 synchronized 是可重入锁.

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

snychronized实现可重入的底层原理:

在可重入锁的内部, 包含了 “线程持有者”“计数器” 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

计数器还未归0,程序就抛出异常,会不会死锁?

分析:若程序抛出异常,并且没有catch捕捉,程序就会脱离之前的代码块,一旦脱离这层加锁的代码块,计数器就会- -,脱离多层代码块,计数器减到0,也就解锁了;

总结:加锁时若出现异常,是不会死锁的,也是一个使得synchronized优秀到将他设计成关键字的原因了,若是C++/Python加锁解锁,都是通过对象来实现的,这时就有可能由于出现异常引起代码未执行完,解锁代码未执行引起死锁;

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

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

相关文章

什么便签好用又没广告 好用无广便签分享

身处这个快节奏的时代,我们时常被各种琐事和计划所包围。想象一下,你在办公桌前,电脑屏幕上杂乱无章地贴着各种纸质便签,有的记录着待办事项,有的则是灵感闪现时的几句诗句。每次想要查找某个信息,都得费力…

Golang | Leetcode Golang题解之第42题接雨水

题目&#xff1a; 题解: func trap(height []int) (ans int) {n : len(height)if n 0 {return}leftMax : make([]int, n)leftMax[0] height[0]for i : 1; i < n; i {leftMax[i] max(leftMax[i-1], height[i])}rightMax : make([]int, n)rightMax[n-1] height[n-1]for i…

CANoe-Vector Security Manager介绍

Vector Security Manager 是 Vector 公司提供的一种工具,它为像 CANoe 这样的 Vector 工具提供安全功能,例如安全相关的通信(SecOC)、诊断、认证等,这些功能都是在安全配置文件中进行管理的。所有支持的工具都采用统一的配置方式。 一个安全配置文件为工具提供了以下安全…

探索AI时代的新天地:LLAMA3引领人工智能革命

大家好&#xff01;相信大家对于AI&#xff08;人工智能&#xff09;的发展已经有了一定的了解&#xff0c;但你是否意识到&#xff0c;到了2024年&#xff0c;AI已经变得如此强大和普及&#xff0c;带来了我们从未想象过的便利和创新呢&#xff1f;让我们一起来看看AI在这个时…

如何安全高效地进行网点文件下发?

随着IT技术的飞速发展&#xff0c;以银行为代表的企业数字化技术转型带来了大量的电子化文档传输需求。文件传输数量呈几何级数增长&#xff0c;传统集中式文件传输模式在爆炸式的增长需求下&#xff0c;银行网点文件下发的效率、可靠性、安全性等方面&#xff0c;都需要重点关…

边缘计算的优势

边缘计算的优势 边缘计算是一种在数据生成地点附近处理数据的技术&#xff0c;而非传统的将数据发送到远端数据中心或云进行处理。这种计算模式对于需要快速响应的场景特别有效&#xff0c;以下详述了边缘计算的核心优势。 1. 降低延迟 边缘计算通过在数据源近处处理数据&…

2款摄像头录像软件,满足你的多种要求!

“有没有一款能够录制摄像头视频的软件呀&#xff1f;我计划录制一些生活小窍门和教学视频&#xff0c;想要能清楚地拍到自己的操作过程。但找了好多软件&#xff0c;都不太满意&#xff0c;真心希望大家能给我推荐几款好用的摄像头录像软件&#xff0c;最好能简单易上手的&…

GPU功能介绍简介

GPU功能介绍简介 随着计算需求的不断升级&#xff0c;尤其是在图形密集型和并行计算任务中&#xff0c;GPU已经从一个简单的图像渲染器件演变成一个强大的计算工具。本篇文章将深入探讨GPU的核心功能、架构以及其在多个领域中的应用。 一、GPU的起源与演进 GPU最初设计用于加速…

百兆集成网络链接器911105A

百兆集成网络链接器&#xff08;有时也称为百兆网卡&#xff09;是一种硬件设备&#xff0c;主要用于计算机与计算机网络之间的高速数据传输。它的主要功能包括&#xff1a; 1. 高速数据传输&#xff1a;百兆集成网络链接器支持100Mbps的数据传输速率&#xff0c;比之前的以太…

抖音老阳讲的选品师项目普通人能赚钱吗?

随着互联网的快速发展&#xff0c;电商行业也迎来了前所未有的繁荣。在这个背景下&#xff0c;选品师这一职业逐渐走进人们的视野。老阳作为行业内的知名人士&#xff0c;经常分享选品师的经验和项目。那么&#xff0c;普通人能否参与老阳讲的选品师项目并且赚钱吗?答案是肯定…

护眼台灯有辐射吗?曝光护眼台灯四大套路!

护眼台灯能够提供便利、健康的光线环境&#xff0c;但作为光学测评师&#xff0c;我观察到一些低品质的护眼台灯存在重大的隐患&#xff0c;这些由劣质材料生产而成的护眼台灯&#xff0c;在使用的过程中&#xff0c;有可能会释放对人体视力有害的辐射&#xff0c;甚至会导致黄…

idea插件快速搜索接口位置之RestfulTool平替Apipost-Helper-2.​0

需求 经常需要根据请求路径搜索某接口位置&#xff0c;特点是接口没有斜杠\&#xff0c;所以双击Shrift找不到接口 RestfulTool 和 RestfulToolkit-fix平替 这两个插件在idea2023.3中无法使用&#xff0c;使用的是Apipost-Helper-2.​0来代替&#xff0c;他也有自己的快捷键…

风险防不胜防?看YashanDB如何守护你的数据库安全(上篇)

数据库作为信息系统的核心&#xff0c;不仅承载着海量的关键数据&#xff0c;还负责向各类用户提供高效、可靠的信息服务&#xff0c;数据库的安全性显得尤为关键&#xff0c;已成为信息安全体系的重中之重。 什么是数据库安全&#xff1f; 数据库安全是数据安全的一个子集&a…

第9章:知识生成提示

知识生成提示&#xff0c;是告诉ChatGPT创造新信息的命令。 你可以告诉它&#xff1a;请生成关于【X】 的新信息。ChatGPT会利用已有知识生成内容。 你也可以同时提出具体要求&#xff0c;如内容类型、长度和风格等。 例1:知识生成 任务: 生成有关特定主题的新信息。指令: 生…

一则 TCP 缓存超负荷导致的 MySQL 连接中断的案例分析

除了 MySQL 本身之外&#xff0c;如何分析定位其他因素的可能性&#xff1f; 作者&#xff1a;龚唐杰&#xff0c;爱可生 DBA 团队成员&#xff0c;主要负责 MySQL 技术支持&#xff0c;擅长 MySQL、PG、国产数据库。 爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使…

正整数的性质:和与根

目录 数字和 数字和介绍 数字和简单应用 哈沙德数 最小元素各数位之和 数字根 数字根介绍 数字根简单应用 数字和 数字和介绍 简单来说&#xff0c;数字和即一个整数数字每一位数值相加求和所得的值&#xff0c;数字和可以为任意正整数&#xff0c;使用代码获取一个数值的数字和…

干货|交流非线性RCD负载的重要指标

在电力系统中&#xff0c;非线性负载是一个重要的问题。它们会对电力系统的稳定性和电能质量产生重大影响。因此&#xff0c;对非线性负载的研究和管理具有重要的实际意义。在交流非线性RCD&#xff08;Residual Current Device&#xff09;负载中&#xff0c;有几个重要的指标…

挤浆机液压系统比例阀放大器

挤浆机液压系统比例阀放大器是一种在制浆工程中用于洗涤浆料的设备&#xff0c;它通过机械压榨的方式&#xff0c;从纸浆中提取出黑液&#xff08;即造纸过程中产生的废液&#xff09;。主液压系统其主要功能是驱动挤浆机的两个辊子转动。这两个辊子负责将浆料中的水分挤出&…

Unity的旋转实现一些方法总结(案例:通过输入,玩家进行旋转移动)

目录 1. Transform.Rotate 方法 使用 2. Transform.rotation 或 Transform.localRotation 属性与四元数 使用方式&#xff1a; 小案例 &#xff1a;目标旋转角度计算&#xff1a;targetRotation&#xff08;Quaternion类型&#xff09; 玩家发现敌人位置&#xff0c;玩家…

SWCTF

easy_php 源码 <?php// flag is in flag.php highlight_file(__FILE__); ini_set(display_errors, 0); error_reporting(0);if (isset($_GET[myon1]) && isset($_GET[myon2]) && isset($_GET[myon3])) {$myon1 $_GET[myon1];$myon2 $_GET[myon2];$myon…