【JavaEE初阶】多线程带来的风险~线程安全

news2024/11/19 8:50:56

目录

🌟观察线程不安全的现象

🌟线程不安全的原因 

🌈1、多个线程修改了同一个共享变量

🌈2、线程是抢占式执行的,CPU的调度是随机的

🌈3、指令执行时没有保证原子性

🌈4、多线程环境中内存的可见性问题

🌈5、代码的顺序性问题

🌟解决线程不安全的现象

🌈分析

🌈1、synchronized 关键字—监视器锁monitor lock。       

        1  使用

        2、 解释

         3、案例:理解下面两种情况

        4、存在的问题

        5、原因解释

        6、锁对象(重点)

        7、synchronized特性

        8、关于synchronized使用时的注意事项:

🌈2、volatile关键字

        1、通过代码观察内存不可见的现象

        2、volatile关键字的使用:用来修饰变量

        3、怎么实现内存可见性的?->MESI缓存一致性协议

        4、volatile可以解决有序性问题

        5、总结synochnized与volatile


🌟观察线程不安全的现象

        我们首先来观察一段代码,分别使用两个线程来对变量进行5w次的自增。理想情况下,使用两个线程同时自增的效率一定比单线程的效率高。

//定义自增操作的对象
    private static Counter counter = new Counter();
    //定义自增的对象
    public static void main(String[] args) throws InterruptedException {
        //定义两个线程,分别自增5w次
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });
        //启动线程
        t1.start();
        t2.start();
        //等待自增完成
        t1.join();
        t2.join();
        //打印结果
        System.out.println("count="+counter.count);
    }
}
class Counter{
    public int count = 0;
    //设置自增方法
    public void increment(){
        count++;
    }

       但是运行结果却不是我们预期的10w。这种现象我们就说这是线程不安全的。总结来说,如果在多线程环境下,代码的运行结果是符合我们预期的,即与单线程环境得到的结果相同,则说明这个线程是安全的。


🌟线程不安全的原因 

🌈1、多个线程修改了同一个共享变量

注意区分:

        多个线程修改同一个共享变量会线程不安全;✅

        多个线程读取同一个变量,不会线程不安全;❎

        多个线程修改不同的变量,不会线程不安全;❎

        单线程环境下,不会出现线程不安全。❎ 

🌈2、线程是抢占式执行的,CPU的调度是随机的

        多个线程在CPU上的调度是随机的,顺序是不可预知的。

🌈3、指令执行时没有保证原子性

        我们之前说过,原子性指的就是代码要么全执行,要么全都不执行。看图:

        因此,两个线程从CPU中操作count++的时候,由于是随机调度CPU的   ,因此会出现count计数结果不正确的现象发生。总结来说,这是由于没有保证指令执行的原子性导致多个线程在做了自增操作之后,互相不可见,从而覆盖了上个线程修改后的值。 

🌈4、多线程环境中内存的可见性问题

     内存可见性:指的是某一个线程在多线程环境下,修改了变量的值,而另一个线程没有感知到。这里我们要提到一个新的概念:Java内存模型(JMM)JMM的出现是为了解决缓存一致性的问题,即多个线程之间读取同一个变量的值是一致的。

 JMM内存模型:

1、JMM中最重要的是主内存和工作内存。

2、主内存指的是硬件的内存条,进程在启动的时候会申请一些资源,包括内存资源,用来保存所有的变量。

3、工作内存指的是线程独有的内存空间,它们之间不能相互访问,起到线程之间内存隔离的作用。每个工作内存之间是相互隔离的。

4、JMM规定,一个线程在修改某个变量的值的时候,必须把变量从主内存中加载到自己的工作内存,修改完成后再啥寻回主内存。


❓问题1:为什么要用JVM?

        Java虚拟机规范中定义了Java内存模型,Java是一个跨平台的语言,把不同的计算机设备和操作系统对内存的管理做了一个统一的封装。目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一定的并发效果。

❓问题2:为什么要弄这么多的内存?

        实际上并没有这么多的“内存”,这只是Java规范中的一个术语,属于“抽象”的叫法。我们所说的主内存其实才是真正硬件角度的“内存”,而工作内存,指的是CPU的寄存器和高速缓存。寄存器的作用是保存代码的运行结果,如果下次还有这个变量,就直接从寄存器中读取,不需要从内存中加载。这主要是由于CPU访问自身寄存器和高速缓存的速度远远大于访问内存的速度(快几千倍,上万倍)。而内存访问速度又远快于硬盘。相应的,CPU价格高于内存高于硬盘。

🌈5、代码的顺序性问题

        指在编译过程,JVM调用本地接口以及CPU执行指令的过程中,指令的有序性。因为指令并不是按照程序员的预期去执行的,在特殊情况下会打乱顺序。这种经过优化的方式,在程序中就叫做指令重排序,目的是为了提高程序的效率。指令重排序的前提是:保证代码重排序后的结果一定要是正确的。其中,在单线程的情况下,重排序的结果一定是正确的,但是在多线程环境中,由于环境的复杂性,结果不一定是正确的。


🌟解决线程不安全的现象

🌈分析

        针对以上5点分别考虑:

1、在真实业务场景中大多都是要修改同一个变量的,因此无法规避;

2、CPU调度是硬件层面的,我们无法规避;

3、有可能通过java层面处理;

4、有可能通过java层面处理;

5、有可能通过java层面处理。强行通知编译器不要做指令重排序。

JMM的主要特性就是保证原子性,内存可见性以及有序性。

🌈1、synchronized 关键字—监视器锁monitor lock。       

1  使用

A- 修饰普通方法     

 B- 修饰要执行的代码块 

       两种方法的区别:使用synchronized关键字修饰,就是将并行操作变成串行操作。使用代码块的方式就是将要串行的代码块包裹起来,将锁的颗粒度减少,从而提升了代码的运行效率。      

C- 修饰静态方法:锁对象是当前的类对象=类名.class

2、 解释

        对当前执行的代码加上一把锁,某个线程要执行这个代码或者方法时就先获取锁,获取到之后再去执行线程,另外的方法在执行这个线程时也要获取锁,但是当有线程持有锁时就要进行等待,等到上一个线程释放之后才能获取该锁。

 过程总结

        (1)线程在获取到锁之后开始执行锁中代码;

        (2)其他线程在执行代码之前要先检查锁的状态;

        (3)如果该锁被其他线程占用,那么就要阻塞等待,对应的线层状态我们就称为BLOCK;

        (4)当锁被释放之后,其他线程才可以继续竞争锁资源。

 3、案例:理解下面两种情况


       

4、存在的问题

        加锁之后的方法变成了单线程。分场景分析这个现象:在获取数据的时候用多线程提高效率,在修改变量的时候用该关键字修饰,单线程,但是保证安全性。

5、原因解释

        synchronized可以解决原子性问题;可以保证内存可见性问题;但是不能保证有序性。

  • 保证原子性的原因:通过对代码加锁实现;
  • 保证内存可见性的原因:通过对代码加锁,保证一个线程执行完所有的操作之后并释放锁,第二个线程才可以获取到锁,此时读到的一定是第一个线程修改的最新结果,从而保证了可见性。

6、锁对象(重点)

        我们在前文使用synchronized关键字的时候,用到了this,表示的是当前对象。也就是锁对象:锁对象的作用是用来记录当前是哪一个线程获取到了锁。必须是同一个对象就可以产生所竞争,产生了锁竞争计算结果才是正确的。

        锁对象可以是Java中的任何对象,只要能够记录到当前获取到锁的线程就可以了。在Java虚拟机中,对象在内存中的结构可以划分为4部分。最关键的是markword。


 举例:区分以下情况

(1)可以使用任意对象充当锁对象

使用类对象来充当锁对象

(2)两个线程一个加锁一个不加锁,计算结果错误❎

 (3)两个线程获取的是不同的锁,计算结果错误❎,不会产生锁竞争。

 (4)注意区分观察以下两种情况是不是同一个对象?

7、synchronized特性

        (1)互斥性:同一时间只能有一个线程获取到锁,其他的线程就要阻塞等待;

        (2)刷新内存:线程修改完变量的值后,会把新的值写回主内存,是以并行->串行的方式来做的;

        (3)可重入性:Java中的synochnized是可重入锁。一个线程可以对同一个锁对象进行多次加锁。synchronized是可以嵌套使用的。如果持有锁的线程是当前线程,那么线程可以再次获取锁对象,锁对象会维护当前线程获取锁的次数,比如我们上述代码中的count=count+1;而每退出一层就会count=count-1,直到count=0的时候,就意味着当前的线程释放了锁。

8、关于synchronized使用时的注意事项:

(1)从并行到串行(指令重排序):要先保证正确,再是效率;

(2)加锁与CPU调度:加锁后:对一个方法加锁并不是说这个线程一直把这个刚发执行完才会调度走,在调度走的时候,只要没有释放锁,其他线程就一直处于BLOCK阻塞等待的状态,直到锁释放再竞争锁。

(3)加锁的范围:加锁的范围越大称为锁的粒度大,也就是串行化执行的更多;

(4)只给一个线程加锁:不会产生锁竞争,计算结果是错误的;

(5)给代码块加锁:synchronized可以加在方法上,也可以加在代码块中。在修饰代码块的时候,要传入参数:锁对象。锁对象可以是自身this,也可以是类对象,其他产生的对象。(看上述例子)

(6)锁对象:要计算结果正确,必须是同一个对象!同一个对象才可以产生锁竞争!

        synchronized可以解决原子性,内存可见性问题,不能解决有序性问题。而且synchronized并没有真正的通过线程之间的通信解决内存可见性。我们接下来看下一个关键字:volatile关键字。

🌈2、volatile关键字

1、通过代码观察内存不可见的现象

        🌰我们先来通过一段代码重现一下内存不可见的问题。创建两个线程,一个线程不断的循环判断flag标志位是否能够退出,第二个线程则用来修改flag标志位。理想情况下,我们认为,第二个线程修改标志位后,第一个线程就应该正常退出了,我们看一下实验结果是否符合我们的预期呢?        

  //定义标志位
    private static int flag = 0;
    public static void main(String[] args) {
        //创建第一个线程,当flag为0 的时候一直循环
        Thread t1 = new Thread(()->{
            System.out.println("t1线程已经启动");
            //根据flag的标志不断的循环
            while (flag==0){
                //TODO 要执行的代码
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1线程已经退出");
        });        

        //创建第二个线程,同时修改标记位flag
        Thread t2 = new Thread(()->{
            System.out.println("t2线程已经启动");
            //修改标记位flag
            System.out.println("请输入整数:");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
            System.out.println("t2线程已经退出");
        });
        //启动线程
        t1.start();
        t2.start();
    }

控制台输出:

         这是怎么回事呢?我们观察到的现象是:当用户输入一个非零的值之后,线程t1并没有正确的退出,出现了线程不安全的现象。主要原因就在于了内存不可见。

        画图理解🌰

 

       解释:在执行的过程中,线程1先将flag变量从主内存中加载到自己的工作内存中,也就是寄存器和缓存中,后来CPU发现,我当前这个t1线程并没有要修改flag这个变量的值的操作,而且从工作内存中读取变量的速度是主内存的几千甚至上万倍,那么CPU就对执行过程做了一定的优化:我每次就直接从工作内存中读取这个变量,不从主内存中加载,因此就获取不到t2修改flag后的最新值。因此出现了这种内存不可见的现象。

2、volatile关键字的使用:用来修饰变量

        解决办法:在上述代码中用volatile来修饰变量flag。

        当然也可以使用sleep()来实现,由于设备不同等因素,你不能确保在一定时间范围内它的休眠时间就足以让线程退出。因这种方式强烈不推荐。代码演示:

3、怎么实现内存可见性的?->MESI缓存一致性协议

       缓存一致性协议也可以理解为是一种通知机制。 当某个线程修改了一个共享变量之后,通知其他CPU其缓存中的变量值已经失效了,要从主内存中加载最新的值。

简单了解下Java层面的内存屏障  

        Load指令(读屏障):它将内存存储的数据拷贝到处理器的缓存中。

        Store指令(写屏障):它主要实现让当前线程写入高速缓存中的最新数据 更新写入到内存,让其他线程也可见。

 

        当发生写操作之后就会通过缓存一致性协议来通知其他CPU中的缓存值失效。加上volatile强制读写内存,虽然速度变慢,但是准确率提高了。因此volatile可以解决内存可见性的问题。

4、volatile可以解决有序性问题

        有序性指的是在程序执行结果正确的前提下,编译器,CPU对指令进行的优化过程。用volatile修饰的变量,就是要告诉编译器,我不需要你对这个变量涉及到的操作进行优化,从而实现有序性。       

5、总结synochnized与volatile

原子性内存可见性有序性
synchronized
volatile

        建议:如果涉及到多线程,对于共享变量最好加上一个volatile关键字修饰。


 

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

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

相关文章

当无触控板和鼠标的情况下,如何开启触控板

背景&#xff1a;一次出行匆忙&#xff0c;忘记带鼠标&#xff0c;周围也无可用工具&#xff0c;主要是触控板当时也被我关闭了&#xff0c;下面讲述一下我是如何解决在没有鼠标的情况下开启触控板的。 首先我们开启电脑后&#xff0c; 存在两种思路去开启触控板 第一种方案…

加拿大访问学者签证材料清单

加拿大在教育、政府透明度、社会自由度以及生活品质等方面在国际上排名名列前茅&#xff0c;出于环境、社会氛围等因素&#xff0c;不少学者将目光聚焦于这个北美的发达国家。加拿大的访问学者签证属于工作签证&#xff0c;过去只要有邀请函就可以办理&#xff0c;但是自去年2月…

Python:Python底层原理:Python的整数是如何实现的

Python整数在底层存储方式 1. Python整数在底层对应的结构体 PyLongObject2.整数是怎么存储的2.1 整数0存储2.2 整数12.3 整数-12.4. 2**30 -12.5 . 2**302.6 . ob_digit[a, b, c] 对应整数计算 计算整数所占内存大小总结 Python的底层是C/C &#xff0c;但是 C/C 能表示的整数…

Linux挂载新磁盘到根目录

添加磁盘到需要挂载的机器上 lsblk查看硬盘挂载情况&#xff0c;sdb,sdc为我新挂载的磁盘 fdisk -l查看挂载之前的分区情况 为新硬盘创建分区 fdisk /dev/sdb 终端会提示&#xff1a; Command &#xff08;m for help&#xff09;&#xff1a;输入&#xff1a;n 依次输入p…

【HTTPS】

HTTP明文传输问题 窃听风险&#xff0c;比如通信链路上可以获取通信内容&#xff0c;用户号容易没。篡改风险&#xff0c;比如强制植入垃圾广告&#xff0c;视觉污染&#xff0c;用户眼容易瞎。冒充风险&#xff0c;比如冒充淘宝网站&#xff0c;用户钱容易没。 TLS协议解决H…

【雅可比左乘右乘】

常见雅可比左乘&#xff08;以自变量R为例子&#xff0c;围绕旋转点p的旋转点的左扰动雅可比&#xff09;&#xff1a; 旋转点的右扰动雅可比&#xff08;右乘&#xff09;&#xff1a; 左雅可比和右雅可比之间的区别在于它们各自描述了不同的变换方向。左雅可比将输入空…

硬件-6-基站和移动通信系统的演进

1G、2G、3G、4G、5G 移动通信技术发展简史 1 移动通信系统简介 移动通信系统从第一代移动通信系统(1G)开始逐渐发展&#xff0c;目前已经发展到第四代移动通信系统(4G)&#xff0c;第五代移动通信系统(5G)也已经开始标准化&#xff0c;预计2020年商用&#xff0c;6G预计2030年…

Linux网络架构: XDP, iptables/netfilter和iproute2/tc/ip/Qdiscs

本文目录 1、架构框图2、网络架构分成三大块3、网络架构-----对应的配置工具-----对应的原理与概念 说到Linux的网络架构&#xff0c;就离不开谈。。。这些东西。这几个概念很容易混淆起来&#xff0c;但如果仔细去看&#xff0c;就会发现这个Linux的网络架构的设计其实是非常简…

10:00面试,10:04就出来了 ,问的实在是太...

从外包出来&#xff0c;没想到竟然死在了另一家厂子 自从加入这家公司&#xff0c;每天都在加班&#xff0c;钱倒是给的不少&#xff0c;所以我也就忍了。没想到12月一纸通知&#xff0c;所有人都不许加班&#xff0c;薪资直降30%&#xff0c;顿时有吃不起饭的赶脚。 好在有个…

建筑专业可以转行学云计算吗?

当然可行。 在过去的几年中&#xff0c;我们已经帮助很多建筑土木工程专业的同学转行学习云计算技术&#xff0c;尤其是在建筑信息化编程方向。近年来&#xff0c;云计算行业持续发展&#xff0c;涉及到众多领域&#xff0c;如云数据中心、云安全、云存储、云计算机服务等。云…

管好【SD-WEBUI】中大量的模型:名称+预览图+备注

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;模型&#xff08;1.1&#xff09;模型名称&#xff08;文件名&#xff09;&#xff08;1.2&#xff09;模型缩略图&#xff08;1.3&#xff09;模型备注文字&#xff08;1.4&#xff09;模型详细信息 &#…

国民技术N32G430开发笔记(18)- I2C1 从机收发数据

I2C1 从机收发数据 1、将PB6 PB7设置为i2c从机&#xff0c;跟android板卡通讯。 2、Android发送 写命令&#xff1a; 0x05 0x02 0x00 0x00 checksum n32将收到的命令打印出来 读版本命令&#xff1a; 0x01 0x02 0x00 0x00 checksum n32将app_version返回电视端 3、i2c从机配置…

MTK6765安卓智能模组5G核心板联发科MTK方案主板开发板

联发科MTK6765这是一款12纳米八核A53处理器&#xff0c;最高运行速度可达2.3GHz。它使用Android 9.0操作系统&#xff0c;配备2G16G内存&#xff0c;也支持其他选项1G/3G/4G8G/32G/64G。 此外&#xff0c;它支持全球主流频段&#xff0c;包括默认的国内频段以及2G GSM、2G/3G E…

【Android取证篇】ADB端口驱动更新详细步骤

【Android取证篇】ADB端口驱动更新详细步骤 更新ADB端口驱动&#xff0c;解决无法连接设备问题—【蘇小沐】 1、【记录VID】路径&#xff1a;设备管理器->便携设备->属性->&#xff08;记录&#xff09;VID 查看设备的VID 2、添加设备ID&#xff08;VID&#xff09…

Sass 总结

文章目录 Sass 总结概述编译.scss 和 .sass变量数据类型属性值变量属性名变量 嵌套规则基本嵌套& 父选择器标识符嵌套属性 import 导入导入Sass文件私有化导入原生CSS文件 mixin 混合器无参数带参数命名参数关键字参数可变参数content 混入内容 extend 继承media 媒体查询控…

hbase查询报错unable to find region for

某天由于集群资源不足。hbase大面积 region server 负载爆炸卡死&#xff0c;隔天发现部分查询报错 集群中一张表某些查询找不到region 尝试了重启集群和滚动重启集群&#xff0c;考虑滚动重启的过程会把regionserver上的region迁出和迁入。试试能不能修复&#xff0c;果断的…

自定义项目Jar上传到maven中央仓库(一步到位)

中央仓库 Open Source Software Repository Hosting 简称 OSSRH 实现目标&#xff1a;将自定义项目上传至maven中央仓库&#xff0c;其他人只需引入maven坐标即可直接使用 上传步骤&#xff1a;亲测有效 注册账号&#xff08;去它平台&#xff09;提交工单&#xff08;叫它做事…

USB HS-PHY眼图调试

1 USB2 PHY AFE 1.1 USB 2.0 FS PHY github ultraembedded / core_usb_fs_phy NOP USB transceiver for all USB transceiver which are either built-in into USB IP or which are mostly autonomous. 1.2 电阻参数 USB host端&#xff1a;D和D-各接一个15kΩ的下拉电阻&#…

数据结构学习记录——堆的建立(最大堆的建立、思路图解、代码实现、代码解释)

目录 最大堆的建立 方法1 方法2 思路图解 代码实现 代码解释 PercDown BuildHeap 最大堆的建立 建立最大堆&#xff1a;将已经存在的N个元素按最大堆的要求存放在一个一维数组中。 方法1 通过插入操作&#xff0c;将N个元素一个一个地插入到一个初始为空的堆中去。…

CSA发布|《洞察2022 云上数据安全与重要事项 》

云安全联盟大中华区就云上数据安全和重要事项的洞察和建议等相关问题展开调查并发布《洞察2022 云上数据安全与重要事项 》&#xff08;以下简称《报告》&#xff09;。报告的主要内容是关于云上数据安全和重要事项的洞察和建议。它包括了对云安全现状的分析、云安全风险的评估…