【JavaEE】并发编程(多线程)线程安全问题内存可见性指令重排序

news2024/10/5 18:27:11

目录

第一个问题:什么是线程安全问题?

第二个问题:为什么会出现线程安全问题? 

第三个问题:如何解决多线程安全问题? 

第四个问题:产生线程不安全的原因有哪些? 

第五个问题:内存可见性问题及解决方案 

第六个问题:指令重排序问题?


第一个问题:什么是线程安全问题?

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。 

第二个问题:为什么会出现线程安全问题? 

出现线程安全的问题的根源其实是在于我们之前说过的多线程“抢占式执行,随机调度”的特性决定的。当我们在使用多线程进行编程的时候,是躲不过这一“万恶之源”的。我们只可以通过一些编程手段来解决这些线程安全的问题。

我们可以看一下下面这部分的代码。

(这是一个典型的多线程的线程安全问题,里面会出现脏数据,也就是多个线程对同一个变量进行更改的问题)

首先我们来看当我们写两个线程进行更改同一个变量的情况:

package Thread;

class Countsum{
    private static int count=0;
    public  void CountAdd(){
        count++;
    }
    public int getCount(){

        return count;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Countsum countsum=new Countsum();
        //第一个线程t1
        Thread t1 =new Thread(()->{

            for (int i = 0; i <50000; i++) {
                countsum.CountAdd();
            }

        });
        //第二个线程t2
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                countsum.CountAdd();
            }
        });
        //两个线程操作同一个变量
        t1.start();
        t2.start();
        //让t1,t2两个线程执行完,再执行main线程,这里让main线程阻塞
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //打印最后的结果,看和预期值10_0000是否一致。
        System.out.println(countsum.getCount());
    }


}

预期值:10_0000

第一次运行:64603 

第二次运行:73388

第三次运行:75233

每一次的结果都和预期值相差甚远。这就说明期间发生了脏读了,也揭示了线程的不安全性。

那么具体的过程是怎样变成这样的?

首先我们需要知道count++这个过程到底是怎么实现的。

我们从CPU的角度出发:count++主要是由三个指令实现的

1、(load)把内存中count的值加载到CPU的寄存器当中

2、(add)把寄存器中的数值加1

3、(save)把寄存器中的值放回到内存中,对原来的值进行覆盖。

我们画个示意图:

同样,也正是因为这个过程需要多个步骤来进行实现,就使得多线程的“抢占式执行,随机调度”得以充分发挥作用了。我们都知道排列组合。在这10万次循环中,会有无数种排列的情况出现,所以基本上每一次的结果不会相等,但是不排除相等的情况。

这里我们就列举一种情况来进行说明即可:

比如这种情况:

我们来分析一下这个过程:

假设初始值为0.

 如果是正常情况下结果应该是2,但是这里结果却是1。这就和上面的程序是一样的道理。

如果要得到正确结果应该是这种的步骤:

就是像这样的能够得到正确的数据。 

第三个问题:如何解决多线程安全问题? 

答案:加锁 

那么java中加锁的方式有很多种,最常使用的是 synchronized 关键字。我们可以给上述代码的自增函数内部自增操作上加synchronized 关键字或者直接给自增的方法加上synchronized关键字就是加锁成功了。

加锁成功后在看一下程序:

package Thread;

class Countsum{
    private static int count=0;
    public  void CountAdd(){
        synchronized (this){
            count++;
        }

    }
    public int getCount(){

        return count;
    }
}
public class ThreadDemo15 {
    public static void main(String[] args) {
        Countsum countsum=new Countsum();
        //第一个线程t1
        Thread t1 =new Thread(()->{

            for (int i = 0; i <50000; i++) {
                countsum.CountAdd();
            }

        });
        //第二个线程t2
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                countsum.CountAdd();
            }
        });
        //两个线程操作同一个变量
        t1.start();
        t2.start();
        //让t1,t2两个线程执行完,再执行main线程,这里让main线程阻塞
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //打印最后的结果,看和预期值10_0000是否一致。
        System.out.println(countsum.getCount());
    }


}

这个结果就和我们的预期值一样了。

第四个问题:产生线程不安全的原因有哪些? 

1、线程是抢占式执行的,线程间的调度充满随机性。(线程不安全的根本原因)

2、多个线程对同一个变量进行修改操作。

3、针对变量的操作不是原子的,通过加锁操作就是把几个指令打包成一个原子的。

4、内存可见性。

 这里需要简单理解一下几个名词:

1)原子性  我们可以简单的理解为打包为一个整体 

第五个问题:内存可见性问题及解决方案 

2)内存可见性

内存可见性问题其实是编译器优化的结果。

我们这里以一个线程读取数字,一个线程修改数字为例:

t线程负责读取istrue的值,main线程负责修改istrue的值。

package Thread;

import java.util.Scanner;

public class ThreadDemo16 {
    public static int istrue=0;
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            while(istrue==0){

            }
            System.out.println("t线程结束!");
        });
        t.start();
        Scanner scanner=new Scanner(System.in);
        System.out.println("请输入一个数字:");
        istrue=scanner.nextInt();
        System.out.println("main线程执行完毕");

    }
}

我们看一下执行结果:

当我们输入一个5的时候,我们原来是应该让t线程结束的,然而main线程结束后,t线程却进入了死循环当中,也就是说,此时的istrue还是0,并没有得到修改。这到底是什么原因导致的呢?

原因是这样的:由于从内存中读是要比从寄存器中读慢很多的(好几个数量级吧大概) 这里的t线程需要不断的循环读取istrue的值,如果我们的main线程不做出修改,那么t线程读取到的值就一直是一样的值。于是编译器就可能会进行优化,让t线程直接从寄存器中读取数据,也就是省去了load的操作,这一大胆的行为使得后续我们对istrue进行的修改都无法让t线程感知到,也就是说修改失去了作用。所以t线程并不会终止。

那么如何解决内存可见性的问题呢?

1、使用synchronized 关键字。synchronized 不光能够保证原子性,同时也能够保证内存可见性。被synchronized 包裹起来的代码,编译器就不敢轻易做出上述假设,就相当于手动禁止了编译器的优化。

2、使用volatile关键字。volatile和原子性无关,但是能够保证内存可见性。使得编译器每次都要重新从内存中读取istrue的值。

方案一:使用volatile关键字(最常用)

 public static volatile int istrue=0;

方案二: 使用synchronized关键字

有时候我们也可以使用一些别的操作,比如sleep啊等等的,不过这些不太可靠哈。

                while(istrue==0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                }

编译器优化总的来说还是比较玄学的!!! 

说到这,还有一个由编译器优化引发的问题!!! 

第六个问题:指令重排序问题?

指令重排序问题听着挺吓人,其实就是个排序问题罢了。 

指令重排序是编译器优化的结果,编译器会对我们写的代码进行重排序从而来提高编译的效率,但是有时候一旦发生指令重排序,就可能会使得程序与我们预期的结果不同了。(在单线程中指令的重排序不会产生太大的影响,但是在多线程中容易出现严重bug,需要多注意!)我们要保证逻辑不变,对顺序进行调整。(使用synchronized可以进行禁止指令的重排序)

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

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

相关文章

模板进阶(包含特化)

非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将该参数当成常量…

CSS(基础,面试,常见用例)

CSS规则【*】CSS选择器一、 CSS选择器二、选择器匹配原理三、优先级 / 权重四、可继承 / 不可继承样式属性【*】盒模型一、盒模型二、box-sizing属性三、offsetWidth、clientWidth、scrollWidth**【*】em/px/rem/vh/vw的区别一、CSS单位二、px三、em四、rem五、vh、vw六、%百分…

基于spring生态的基础后端开发及渗透测试流程

这是一篇记录如何从建仓开始到最后安全测试完整流程的笔记&#xff0c;使用的spring生态&#xff0c;目的是为spring的基础后端开发及后期渗透测试打一个模板。本篇采用springSecurity作为安全框架&#xff0c;搭载了redis-cache、spring-valid等功能&#xff0c;并开放了OAuth…

实测十款连锁店管理系统,专为纠结的连锁店老板打造!

普通的数据工具、人工管理难以满足连锁店老板们的需求&#xff0c;正所谓“有需求就有市场”&#xff0c;随着连锁店、加盟店如雨后春笋般在城市里出现&#xff0c;连锁店管理系统也越来越多。究竟哪一款连锁店管理系统&#xff0c;才能满足老板们的需求&#xff1f;不用纠结了…

park unpark 原理

1、基本使用 // 暂停当前线程 LockSupport.park();// 恢复某个线程的运行 LockSupport.unpark(暂停线程对象) 先 park 再 unpark public class Test1 {public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(() -> {log.debug(&q…

「自控元件及线路」1.2 电机中的磁性材料与磁场

本节介绍磁性材料的性能、分类 本节介绍电机中永磁材料的工作曲线 本节介绍电机中主磁极、电枢的磁场及电枢反应 文章目录磁性材料的基本概念磁性材料的磁性能高导磁性 饱和性 磁滞性 非线性温度特性 电阻率特性铁耗磁性材料的分类电机中的永磁材料永磁电机概述永磁材料的磁性能…

mysql:聊聊mysql学完之后心得,从哪里学,学哪些,怎么选课程,学到什么程度。

mysql&#xff1a;聊聊mysql学完之后心得&#xff0c;从哪里学&#xff0c;学哪些&#xff0c;怎么选课程&#xff0c;学到什么程度。 学习完一套课程之后习惯性总结一下。首先说一下&#xff0c;咕咕是跟着尚硅谷的康老师学习的mysql&#xff0c;大家想学习的话可以直接去b站…

Java文件IO操作及案例

文章目录一. 文件概述1. 狭义和广义上的文件2. 文件的分类3. 文件的路径二. 针对文件系统的操作1. File类的属性和构造2. File类的获取操作3. File类的判断操作4. 文件的创建和删除5. 其他的常用方法三. 对文件内容进行读写1. IO流对象2. 文件的读操作3. 文件的写操作4. Scanne…

vim使用入门

目录vim模式介绍1.1 模式介绍1.2 模式之间切换1.3 进入vim1.4 退出vimvim模式介绍 1.1 模式介绍 vim具有6种基本模式和5种派生模式。 6种基本模式如下&#xff1a; 普通模式 vim启动后的默认模式。使用编辑器命令&#xff0c;比如移动光标&#xff0c;删除文本等等 普通模式进…

智能指针(二)—— unique_ptr、shared_ptr 的简单模拟实现

智能指针其实就是资源管理权限的转移&#xff0c;自己不想手动释放&#xff0c;交给一个对象管理&#xff0c;对象什么时候被销毁&#xff0c;这块资源也就什么时候被释放。unique_ptr 、shared_ptr 和weak_ptr 之间的区别如下&#xff1a; unique_ptr&#xff1a;字面意思是…

非全研究生开题-室内定位最新研究论文总结-1

序言 时间飞逝,23年3月底之前要把开题报告和综述写好。开学后研究了2个方向:serverless冷启动和室内定位; 春节期间在这2个选题之间犹豫不决,不断在心里面分析 经过反复比较对比,决定研究室内定位,也可以为后续完善产品,甚至后面创业打下基础。 后面我会持续深入,在…

【数据结构】链表必做题

写在开头学完链表&#xff0c;我们就要适当做一些题目来巩固知识&#xff0c;下面的一些精选题难度适中&#xff0c;很适合初学者练手。做题之前我们来了解一点概念&#xff0c;就是链表调用传参时&#xff0c;我们什么时候使用一级指针&#xff0c;什么时候使用二级指针&#…

在Linux上安装Maven和配置Maven仓库(v3.8.7)

记录&#xff1a;369场景&#xff1a;在CentOS 7.9操作系统上&#xff0c;使用apache-maven-3.8.7安装Maven和配置Maven仓库。版本&#xff1a;JDK 1.8apache-maven-3.8.7名词&#xff1a;Apache Maven&#xff1a;Apache Maven is a software project management and comprehe…

31_内存马

内存马 一、php内存马(不死马) 原不死马文件是生成一个一句话木马文件之后,自动删除自身,并且抑制报错 即使删除生成的一句话木马文件 也会不断的继续生成一句话木马文件 除非重启服务,才能停止继续生成 <?php error_reporting(0);//抑制报错 unlink(__FILE__);//删除…

【工具】iOS代码混淆工具-iOS源码混淆

最新更新记录 V2.0.3&#xff08;2022年12月11日&#xff09;更新内容&#xff1a; 1、新增导入映射列表的逻辑&#xff1b; 2、优化修复其他混淆逻辑&#xff1b; 3、更新地址 - github 主要功能 ZFJObsLib是专业的iOS源码混淆工具&#xff0c;具体功能有方法混淆、属性…

AcWing 1082. 数字游戏(数位DP)

AcWing 1082. 数字游戏&#xff08;数位DP&#xff09;一、问题二、数位DP三、思路分析四、代码一、问题 二、数位DP 这道题是在一个区间内挑选满足某个条件的数&#xff0c;这是一个非常典型的数位DP的特点。 这道题是一道数位DP的题目&#xff0c;其实数位DP更像我们在高中…

5-TAMRA-TSA,5-TAMRA-Tyramide,5-四甲基罗丹明-酪酰胺

【中文名称】5-四甲基罗丹明-酪酰胺&#xff0c;5四甲基罗丹明酪酰胺【英文名称】 5-Tamra-Tyramide&#xff0c;5-TAMRA-Tyramide&#xff0c;5-Tamra-TSA&#xff0c;5-TAMRA-TSA【CAS】N/A【纯度标准】95%【包装规格】5mg&#xff0c;10mg&#xff0c;25mg【是否接受定制】可…

spring springboot关于异步线程实践案例

前言&#xff1a; 关于多线程的异步处理&#xff0c;由于项目的需求有个方法需要使用异步的方法来调用&#xff0c;方法是调用外部的接口&#xff0c;执行时间会比较长导致的没有办法同步拿去到结果&#xff0c;所以需要写一个异步线程的方法进行该接口的调用&#xff0c;下面是…

Unity - UI适配方案记录

1.普通屏不同分辨率适配及预览 1.背景图 界面背景图建议至少宽于21:9&#xff1b;其中16:9的范围为精细范围&#xff0c;12:9的范围为真机最小可见范围。 2.组件 各组件通过设置RectTransform属性来达到正确适配的效果。 3.预览 在unity中&#xff0c;设置Game窗口左上角…

【C++入门第一期】命名空间 缺省参数 函数重载 的使用方法及注意事项

目录简介命名空间为何会有命名空间命名空间的定义嵌套定义命名空间的使用作用域限定符using 将命名空间中某个成员引入using namespace 将该空间所有成员引入缺省参数全缺省参数函数半缺省参数函数如何给缺省值函数重载函数重载的概念函数重载的三种类型1&#xff1a;函数参数类…