【剧前爆米花--爪哇岛寻宝】java--线程不安全的原因及解决方法

作者:困了电视剧

专栏:《JavaEE初阶》

文章分布:这是关于线程安全相关的文章,在该文章中,我梳理了造成线程不安全的原因和使线程变安全的方法,希望对你有所帮助!

 

目录

线程的安全问题

什么是线程安全

线程不安全的原因

修改共享数据 

原子性

可见性

代码顺序性 

线程安全问题的解决

synchronized关键字

互斥

可重入

volatile关键字


线程的安全问题

我们在单线程的情况下,一般不会遇到线程的安全问题,但当我们进行多线程的编程时,多线程之间的并发并行机制,以及线程之间对CPU资源的抢占都会可能导致我们得到一些意料之外的结果。

什么是线程安全

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线 程安全的。

线程不安全的原因

修改共享数据 

这一点可以分为三个小类:

1.抢占式执行(根本原因)

2.多个线程修改同一个变量

   1)一个线程修改一个变量安全

   2)多个线程读取同一个变量安全

   3)多个线程修改不同的变量安全

3.修改操作不是原子的

举个栗子:

class Counter{
    public int count = 0;
    public void add(){
      
         count++;
        
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread( ()->{
            for ( int i=0;i<10000;i++ ){
                counter.add();
            }
        });

        Thread t2 = new Thread( ()->{
            for ( int i=0;i<10000;i++ ){
                counter.add();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

现在有这样的一段代码,我想要实现的功能就是设置两个线程,让每一个线程都做累加count一万次的功能,按照我们的逻辑来思考,最后主线程在两个线程执行完后输出的count值应该是两万,这是我们在单线程中的思维。但结果确实如此吗?

我们可以看到结果并不是两万,而是一个我们意料之外的数字,并且随着我们每次运行,这个结果都不同,这是为什么?

这就需要从计算机的底层来进行剖析了,我们在代码中执行“count++”这一句代码时,反应到计算机内部大致就是:

计算机先通过load指令将count的值从内存中取出来存到寄存器当中,然后再通过运算逻辑部件对寄存器中的count进行加一操作,完成后,再将寄存器中的值放回到内存中保存

在分析上大概是这种,我们的理想情况是t1线程执行完后,再执行t2线程,然后t2线程完整的执行完后在执行t1线程,但是在真实的计算机内部并不是这样的,由于线程的并发执行,这就导致一个count++代码可能并没有执行完就切换到另一个线程去了,这就导致了很多种不确定的情况,比如这种:

 

当出现这种情况的时候就会发现,虽然我们的count++执行了两次,但最后保存到内存中时,只会保存执行一次的结果,还有很多种其他的情况,这些情况有的会影响结果有的不会,在这种混乱的状况下,我们根本无法得到一个准确的值,更别说我们想要的值了。 

原子性

这个原子性和之前的事物的原子性类似,都是表示一种不能分的概念。

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

和上述举得count的例子一样,我们要想解决这类问题必须要让我们进行的操作具备一种原子性,即不执行完不能进行其他的操作。 

可见性

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

                                                                                                                                               

线程之间的共享变量存在 主内存 (Main Memory).
每一个线程都有自己的 " 工作内存 " (Working Memory) .
当线程要读取一个共享变量的时候 , 会先把变量从主内存拷贝到工作内存 , 再从工作内存读取数据 .
当线程要修改一个共享变量的时候 , 也会先修改工作内存中的副本 , 再同步回主内存 .

这里的主内存就是硬件角度的内存,而这里的工作内存就是寄存器,这里的代码不安全具体体现在,主内存对数据的修改无法及时的更新,举个栗子:

线程1需要对a进行修改,线程2也需要a这个数据,假设a的大小是10,然后进行修改后变成了20,由于工作内存的速度远远大于主内存的读写速度,所以此时修改后的20并不会及时地传入主内存中,于是在这期间线程2取得值还是10,这就造成了错误,也就是线程不安全。

代码顺序性 

代码的顺序性比较复杂,这里通过一个栗子来进行解释:

比如说我现在需要干三件事。1.去前台拿钥匙。2.完成一个试卷。3.去前台拿一盒粉笔。

如果 我们是单线程,那么计算机会自动的帮我们进行优化,即不按123的顺序执行而是按132的顺序执行,这样我们就会节约一次去前台的时间,而如果我们是多线程,当我们不按顺序执行,未执行2而执行了3这样就可能会造成一些问题,比如在完成两个任务的时间后其他线程需要进行批改试卷,而此时这个线程的试卷还没开始写...

这就造成了线程安全的问题。

线程安全问题的解决

synchronized关键字

互斥

synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到同一个对象 synchronized 就会 阻塞等待 .
进入 synchronized 修饰的代码块 , 相当于 加锁
退出 synchronized 修饰的代码块 , 相当于 解锁
理解 " 阻塞等待 ".
针对每一把锁 , 操作系统内部都维护了一个等待队列 . 当这个锁被某个线程占有的时候 , 其他线程尝试进行加锁, 就加不上了 , 就会阻塞等待 , 一直等到之前的线程解锁之后 , 由操作系统唤醒一个新的线程, 再来获取到这个锁。

换个角度思考,加上锁的代码块就是让这个代码块具有原子性,即对于这个代码块来说,必须要等当前线程执行完代码段内的操作其他线程才能执行,对于这个代码块中的内容cpu只能串行执行。 

这时候可能有人会问了,这和join有什么区别?

这是个好问题,首先最重要的一点是,初心不一样:synchronized通过给代码段上锁,赋予一段操作原子性,然后当这段代码执行结束时,其他被synchronized修饰的代码段再通过锁竞争进行执行,其本质是为了保证线程安全,而join则是完全等待另一个线程执行完,可能是另一个线程有当前线程需要的内容等等,总之不是为了线程安全考虑。

其次对于线程的并发而言,synchronized只是将那一段代码块进行上锁,即串行,其他需要执行的依然会并发执行,而join则是让整个线程进行等待,效率比上锁更慢。

可重入

volatile关键字

volatile 修饰的变量 , 能够保证 " 内存可见性 "。
代码在写入 volatile 修饰的变量的时候 ,
->改变线程工作内存中 volatile 变量副本的值
->将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候 ,
->从主内存中读取 volatile 变量的最新值到线程的工作内存中
->从工作内存中读取 volatile变量的副本

归根结底,volatile关键字修饰的变量就是,让其每次读写都强制访问主内存,而不仅仅是工作内存,这样虽然降低了运行的效率,但是却也避免了代码可见性相关的问题,是线程安全。

举个栗子:

public class ThreadDemo2 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread( ()->{
            while (flag == 0){
                //空循环
            }
            System.out.println("结束");
        });

        Thread t2 = new Thread( ()->{
            Scanner reader = new Scanner(System.in);
            System.out.println("输入flag");
            flag = reader.nextInt();
        });

        t1.start();
        t2.start();
    }
}

对于这段代码会发现,当我输入0时,t1线程并没有结束,这是为什么?

原因是,由于在t1的循环中我的是空循环,所以while()中的判断语句的执行时间远远大于循环体的执行时间,计算机为了提高效率就会进行优化,他不在每次都从主内存中读取flag而是直接读取工作内存中flag的副本以此来加快速度,所以只要我们加上volatile修饰就好。

以上就是本篇博客的全部内容,如有疏漏还请指正! 

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

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

相关文章

Mac 和 Win,到底用哪个系统学编程?

今天来聊一个老生常谈的问题&#xff0c;学编程时到底选择什么操作系统&#xff1f;Mac、Windows&#xff0c;还是别的什么。。 作为一个每种操作系统都用过很多年的程序员&#xff0c;我会结合我自己的经历来给大家一些参考和建议。 接下来先分别聊聊每种操作系统的优点和不…

【Linux】软件包管理器 yum

什么是软件包和软件包管理器 在 Linux 下需要安装软件时&#xff0c; 最原始的办法就是下载到程序的源代码&#xff0c; 进行编译得到可执行程序。但是这样太麻烦了&#xff0c;所以有些人就把一些常用的软件提前编译好, 做成软件包 ( 就相当于windows上的软件安装程序)放在服…

【Arduino 和 MPU6050 加速度计和陀螺仪教程】

【Arduino 和 MPU6050 加速度计和陀螺仪教程】 1. 概述2. 工作原理3. Arduino 和 MPU60504. MPU6050 Arduino 代码5. MPU6050 方向跟踪 – 3D 可视化1. 概述 在本教程中,我们将学习如何将MPU6050加速度计和陀螺仪传感器与Arduino一起使用。首先,我将解释MPU6050的工作原理以…

Springboot项目快速实现过滤器功能

前言很多时候&#xff0c;当你以为掌握了事实真相的时间&#xff0c;如果你能再深入一点&#xff0c;你可能会发现另外一些真相。比如面向切面编程的最佳编程实践是AOP&#xff0c;AOP的主要作用就是可以定义切入点&#xff0c;并在切入点纵向织入一些额外的统一操作&#xff0…

数据结构和算法(1):数组

目录概述动态数组二维数组局部性原理越界检查概述 定义 在计算机科学中&#xff0c;数组是由一组元素&#xff08;值或变量&#xff09;组成的数据结构&#xff0c;每个元素有至少一个索引或键来标识 In computer science, an array is a data structure consisting of a col…

基于深度学习的动物识别系统(YOLOv5清新界面版,Python代码)

摘要&#xff1a;动物识别系统用于识别和统计常见动物数量&#xff0c;通过深度学习技术检测日常几种动物图像识别&#xff0c;支持图片、视频和摄像头画面等形式。在介绍算法原理的同时&#xff0c;给出Python的实现代码、训练数据集以及PyQt的UI界面。动物识别系统主要用于常…

mac下搭建elasticsearch日志系统,filebeat + elasticsearch + logstash + kibana

一、下载安装 elasticsearch和kibana上一篇已经安装好&#xff0c;这篇主要讲filebeat和logstash安装和使用。 我是M1芯片的mac&#xff0c;需要安装 1.filebeat https://www.elastic.co/cn/downloads/past-releases/filebeat-8-6-0 2.logstash https://www.elastic.co/cn…

天狗实战(二)SpringBoot API开发详解 --SpringMVC注解+封装结果+支持跨域+打包(下)

本文目录前言专栏介绍一、创建SpringBoot项目1.1 添加springboot依赖1.2 创建启动类1.3 创建控制器类1.4 Run 或 Debug二、开发图书管理API2.1 web层BookAdminControllerBookVO2.2 service层BookServiceBookServiceImplBookBO2.3 dal层BookMapperBookMapperImplBook2.4 Postman…

什么是分布式任务调度?怎样实现任务调度

通常任务调度的程序是集成在应用中的&#xff0c;比如&#xff1a;优惠卷服务中包括了定时发放优惠卷的的调度程序&#xff0c;结算服务中包括了定期生成报表的任务调度程序&#xff0c;由于采用分布式架构&#xff0c;一个服务往往会部署多个冗余实例来运行我们的业务&#xf…

软件行业的最后十年【ChatGPT】

在这篇文章中&#xff0c;我将说明像 ChatGPT 这样的生成式人工智能 (GAI) 将如何在十年内取代软件工程师。 预测被离散化为 5 个阶段&#xff0c;总体轨迹趋向于完全接管。 但首先&#xff0c;一个简短的前言。 推荐&#xff1a;用 NSDT场景设计器 快速搭建3D场景。 1、关于AI…

VueX快速入门(适合后端,无脑入门!!!)

文章目录前言State和Mutations基础简化gettersMutationsActions&#xff08;异步&#xff09;Module总结前言 作为一个没啥前端基础&#xff08;就是那种跳过js直接学vue的那种。。。&#xff09;的后端选手。按照自己的思路总结了一下对VueX的理解。大佬勿喷qAq。 首先我们需要…

蓝桥杯刷题冲刺 | 倒计时22天

作者&#xff1a;指针不指南吗 专栏&#xff1a;蓝桥杯倒计时冲刺 &#x1f43e;马上就要蓝桥杯了&#xff0c;最后的这几天尤为重要&#xff0c;不可懈怠哦&#x1f43e; 文章目录1.选数异或2.特殊年份1.选数异或 题目 链接&#xff1a; 选数异或 - 蓝桥云课 (lanqiao.cn) 给定…

Java分布式事务(九)

文章目录&#x1f525;XA强一致性分布式事务实战_Atomikos介绍&#x1f525;XA强一致性分布式事务实战_业务说明&#x1f525;XA强一致性分布式事务实战_项目搭建&#x1f525;XA强一致性分布式事务实战_多数据源实现&#x1f525;XA强一致性分布式事务实战_业务层实现&#x1…

人工智能前沿知识

本来想着初试完学习一下李沐大神的《动手学深度学习》这本书的&#xff0c;但是时间仓促&#xff0c;完全来不及。只能先自行了解一些知识&#xff0c;之后再深入了解。 这里为面试应答&#xff0c;问了chatgpt一些关于AI前沿的知识&#xff1a; 还需要再了解一番&#xff1a;…

贪心算法的原理以及应用

文章目录0、概念0.1.定义0.2.特征0.3.步骤0.4.适用1、与动态规划的联系1.1.区别1.2.联系2、例子3、总结4、引用0、概念 0.1.定义 贪心算法&#xff08;greedy algorithm &#xff0c;又称贪婪算法&#xff09;是指&#xff0c;在对问题求解时&#xff0c;总是做出在当前看来是…

【Git使用学习】本地项目更改以及相对应的Github操作

接上一节&#xff0c;因为是vue项目&#xff0c;导致有很多的node_modules需要安装&#xff0c;如果将这个文件夹也一同上传到github中&#xff0c;太慢了。因此上一节将这个文件夹删除了。但是&#xff0c;在本地运行的时候&#xff0c;这个文件夹不能删&#xff0c;不然就跑不…

开箱即用的密码框组件

写了一个小玩具&#xff0c;分享一下 - 组件功能&#xff1a; 初次进入页面时&#xff0c;密码隐藏显示&#xff0c;且无法查看真实密码 当修改密码时&#xff0c;触发键盘&#xff0c;输入框则会直接清空 此时输入密码&#xff0c;可以设置密码的隐藏或显示&#xff1a; …

可换皮肤的Qt登录界面

⭐️我叫忆_恒心,一名喜欢书写博客的在读研究生👨‍🎓。 如果觉得本文能帮到您,麻烦点个赞👍呗! 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧,喜欢的小伙伴给个三连支持一下呗。👍⭐️❤️ 可换皮肤的Qt登录界面 QSS的学习笔记 快…

上手使用百度文心一言

3月16日&#xff0c;在距离新一代的GPT模型GPT-4发布还不足一天的时间内&#xff0c;百度便发布了对标ChatGPT的人工智能产品&#xff0c;名字叫&#xff1a;文心一言。成为国内首页发布该类型产品的公司。 那么&#xff0c;我们今天就来试一试百度的文心一言好不好用。 首先&a…

【SpringMVC】SpringMVC方式,向作用域对象共享数据(ModelAndView、Model、map、ModelMap)

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 向域对象共享数据一、使用 原生ServletAPI二、…
最新文章