JAVA多线程-线程安全问题

2023-10-26

一、CPU多核缓存架构

CPU分为三级缓存: 每个CPU都有L1,L2缓存,但是L3缓存是多核公用的。

CPU查找数据的顺序为: CPU -> L1 -> L2 -> L3 -> 内存 -> 硬盘

进一步优化,CPU每次读取一个数据,并不是仅仅读取这个数据本身,而是会读取与它相邻的64个字节的数据,称之为缓存行,因为CPU认为,我使用了这个变量,很快就会使用与它相邻的数据,这是计算机的局部性原理。这样,就不需要每次都从主存中读取数据了。一个缓存行现在是64个字节,这是很多科学家调优的结果,如果设计的太小则难以命中,如果设计的大了则读取比较慢,这是目前的最优解。

缓存行 (Cache Line) 便是 CPU Cache 中的最小单位,CPU Cache 由若干缓存行组成,一个缓存行的大小通常是 64 字节(这取决于 CPU),并且它有效地引用主内存中的一块地址。

多级缓存架构下最典型的问题就是可见性问题,可以简单的理解为,一个线程修改的值对其他线程可能不可见。

比如两个CPU读取了一个缓存行,缓存行里有两个变量,一个x一个y。第一颗CPU修改了x的数据,还没有刷回主存,此时第二颗CPU,从主存中读取了未修改的缓存行,而此时第一颗CPU修改的数据刷回主存,这时就出现,第二颗CPU读取的数据和主存不一致的情况。

除了存在可见性的问题,当多个线程同时修改相同资源的时候,还会存在资源争夺问题。

除了增加高速缓存之外,为了使处理器内部的运算单元尽量被充分利用。处理器可能会对输入的代码进行乱序执行,优化处理器会在计算之后将乱序执行的结果进行重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句的先后执行顺序与输入代码中的顺序一致。因此如果存在一个计算任务,依赖于另外一个依赖任务的中间,结果那么顺序性不能靠代码的先后顺序来保证。 Java虚拟机的即时编译器中也有指令重排的优化。

二、JMM模型中存在的问题

2.1、指令重排

我们写一个例子来证明指令重排的存在

public class OutOfOrderExecution {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    private static int count = 0;

    private static volatile int NUM = 0;

    public static void main(String[] args)
            throws InterruptedException {
        long start = System.currentTimeMillis();
        for (;;) {
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    x = b;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("一共执行了:" + (count++) + "次");
            if(x==0 && y==0){
                long end = System.currentTimeMillis();
                System.out.println("耗时:+"+ (end-start) +"毫秒,(" + x + "," + y + ")");
                break;
            }
            a=0;b=0;x=0;y=0;
        }
    }
}

我们的印象中,不论怎么执行,这个程序有可能是(x=0,y=1)即t1先与t2执行,也有可能是(x=1,y=0)即t1后与t2执行,但是现实是可能会出现(1,1)和(0,0),但是按道理绝对不会出现(0,0),因为出现零的情况一定是x = b; y = a; a = 1; b = 1;,如果出现了也就证明了我们的执行在执行的时候确实存在乱序。

怎么避免乱序执行呢,我们可以使用volatile关键字来保证一个变量在一次读写操作时的避免指令重排

2.2、可见性

我们来证明一下一个线程对数据的修改对于另一个线程不可见

public class Test {
    private static boolean isOver = false;

    private static int number = 0;

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isOver) {}
                System.out.println(number);
            }
        });
        thread.start();
        ThreadUtils.sleep(1000);
        number = 50;
        // 已经改了啊,应该可以退出上边循环的值了啊!
        isOver = true;
    }
}

我们的主线程已经修改了isOver,按道理新创建的线程要停下来来输出number,实际上永元也不会输出,isOver因为新的线程的频繁使用,已经被加载到一级缓存,CPU再次读取isOver是直接从一级缓存读取的,并没有从内存读取,所以主线程修改了内存中的值并没有效果。

怎样避免出现可见性问题呢?volatile能强制对改变量的读写直接在主存中操作,从而解决了不可见的问题。

JMM用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系 。

2.3、资源争夺

我们来证明一下一个线程之间存在资源争抢的问题

public class Test {
    private static int COUNT = 0;

    public static void adder(){
         COUNT++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                adder();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                adder();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最后的结果是:"+COUNT);
    }
}

最后我们发现每次的结果都不一样,都是10000以上的数字,这足以说明问题了,一个线程的结果对另一个线程不可见。

解决线程争抢问题的最好的方案就是加锁,这里我们解释另外一个问题,就是为什么volatile它不行,非得用synchronized,我们解释一个volatile做了什么,我们的CPU运行速度很快很快,主存是不够快的,所以我们会有cachecache就存在这样一个问题,cache中的数据命中了,CPU对数据进行了修改,cache中数据被修改了,要同步给主存,别的线程是看不到主存的改变的,JAVA内存模型中,每个线程都有自己的缓存,这完蛋了,内存不可见,所以volatile登场,它强制性的将我们修改的数据刷入每一个使用到了我们修改数据的线程的缓存中,可是这样就数据安全了嘛,其实并不安全,为什么?我们刚刚提到了CPU很快,内存很慢的,比如说我们的a++这个指令在线程1中执行的其实是 a=a+1,我们线程1将a=1刷回主存之后,会给线程2强制刷新,线程2的a都自加到10了,其实必然发生错误,也就是说volatile只保证了可见性,但是并没有保证操作的原子性,跟新不及时。volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存,这样任何时刻,不同的线程总能看到该变量的最新值。

synchronized就不一样了,它保证的是原子性,也就是说被synchronized加锁的操作,不论多少个线程只能有一个线程去操作它,它是线程安全的,volatile是线程不安全的。但是 synchronized是一个很重的操作

三、线程安全的实现方法

3.1、数据不可变

在Java当中,一切不可变的对象(immutable)一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障的措施,比如final关键字修饰的基础数据类型,再比如说咱们的Java字符串儿。只要一个不可变的对象被正确的构建出来,那外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态,带来的安全性是最直接最纯粹的。

3.2、互斥同步

互斥同步是常见的一种并发正确性的保障手段,同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用,互斥是实现同步的一种手段,互斥是因、同步是果,互斥是方法,同步是目的。

互斥同步面临的主要问题是,进行线程阻塞和唤醒带来的性能开销,因此这种同步也被称为阻塞同步,从解决问题的方式上来看互斥同步是一种悲观的并发策略,其总是认为,只要不去做正确的同步措施,那就肯定会出现问题,无论共享的数据是否真的出现,都会进行加锁。这将会导致用户态到内核态的转化、维护锁计数器和检查是否被阻塞的线程需要被唤醒等等开销。

3.3、非阻塞同步

随着硬件指令级的发展,我们已经有了另外的选择,基于冲突检测的乐观并发策略。通俗的说,就是不管有没有风险,先进行操作,如果没有其他线程征用共享数据,那就直接成功,如果共享数据确实被征用产生了冲突,那就再进行补偿策略,常见的补偿策略就是不断的重试,直到出现没有竞争的共享数据为止,这种乐观并发策略的实现,不再需要把线程阻塞挂起,因此同步操作也被称为非阻塞同步,这种措施的代码也常常被称之为无锁编程,也就是咱们说的自旋。我们用cas来实现这种非阻塞同步。

3.4、无同步方案

在我们这个工作当中,还经常遇到这样一种情况,多个线程需要共享数据,但是这些数据又可以在单独的线程当中计算,得出结果,而不被其他的线程所影响,如果能保证这一点,我们就可以把共享数据的可见范围限制在一个线程之内,这样就无需同步,也能够保证个个线程之间不出现数据征用的问题,说人话就是数据拿过来,我用我的,你用你的,从而保证线程安全,比如说ThreadLocal

四、并发编程三要素

4.1、原子性

原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。

4.2、可见性

可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。

4.3、有序性

有序性,即程序的执行顺序按照代码的先后顺序来执行。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JAVA多线程-线程安全问题 的相关文章

随机推荐

  • Linux指令中touch和mkdir的区别

    在Linux中 mkdir 用于创建空的文件夹 格式 mkdir 选项 目录 选项 功能 m 默认文件目录的权限 m755 p 连续创建多层 v 显示创建过程 touch touch 是用于创建新的文件 或者修改文件的时间
  • 福昕阅读器注册码

    以下文字复制到记事本存为frpkey txt 复制到福昕阅读器的安装目录即可 FoxitReaderPro SN FRPFZ12391Modules Users 1Licensee OlivierGuilloryLicenseDate 20
  • sql中and和or的混合使用

    1 and的优先级高于or 2 使用 调整优先级 下面sql没有添加过滤条件 下面sql查出的结果是错误的 下面sql查出的结果是正确的
  • 延时函数

    Dos sleep 1 停留1秒 delay 100 停留100毫秒 Windows Sleep 100 停留100毫秒 Linux sleep 1 停留1秒 usleep 1000 停留1毫秒 每一个平台不太一样 最好自己定义一套跨平台的
  • 继续探索Roop(单张图视频换脸)的各方面:比如喜闻乐见的“加速”

    文章目录 一 Roop项目的特点 二 Roop也能加速 三 Roop更新和依赖 3 1 飞速更新 3 2 依赖问题 3 3 需要CUDA么 前两天写了 简单介绍Roop 类似SimSwap 单张图视频换脸的项目 介绍了基本安装使用 之后这个
  • [1193]ClickHouse写入常见问题: Too many parts (300)

    文章目录 一 场景及错误信息 二 报错原因 三 解决办法 扩展 一 场景及错误信息 今天使用 Datax 往 ClickHouse 同步数据时 出现如下错误 ClickHouse exception code 1002 host 10 12
  • Ubuntu22.04安装mysql集群一主一从

    Ubuntu22 04安装mysql集群 以下是在Ubuntu 22 04上安装一主一从的MariaDB集群的步骤 首先 你需要有两个 Ubuntu 22 04 的服务器 分别命名为 Server1 和 Server2 这两个服务器都需要安
  • 可调用对象与lambda表达式

    可调用对象与lambda表达式 OVERVIEW 可调用对象与lambda表达式 1 using 1 函数指针别名 case1 定义基础类型 case2 定义函数指针别名 2 模板定义别名 2 可调用对象 1 包装器 case1 基本用法
  • uniapp幸运大转盘

  • JAVA实现微信授权登录(详解)

    第一步 前期设置 登录微信公众号接口测试平台设置信息 登录微信公众号接口测试平台 登录成功后可以看到测试用的appid和appsecret 稍后再后台我们要用到这两个ID 如下图 紧接着需要设置网页授权 体验接口权限表 网页服务 网页帐号
  • 残差连接 (及 梯度消失 网络退化)详解

    本文就说说用残差连接解决梯度消失和网络退化的问题 一 背景 1 梯度消失问题 我们发现很深的网络层 由于参数初始化一般更靠近0 这样在训练的过程中更新浅层网络的参数时 很容易随着网络的深入而导致梯度消失 浅层的参数无法更新 可以看到 假设现
  • R语言实战之描述性统计分析

    R语言实战之描述性统计分析 下面展示一些 描述性统计分析的R代码语言 vars lt c mpg hp wt head mtcars vars 创造一个统计的函数列表 通过sapply 计算描述性统计变量 包括偏度和峰度 mystats l
  • Sublime Text 2.0.1 (32位和64位)破解方法

    sublime 本身可以免费使用 不过看着那个 未注册 提示 总是不太爽 想支持正版嘛 可惜要50美元 不是RMB 只好找破解方法了 破解方法仅供交流使用 由此产生的一切问题与本人无关 喜欢的请支持正版 64位版本 1 复制Sublime安
  • Latex 中带左边大括号的方程组

    代码如下 documentclass article setlength textwidth 245 0pt usepackage CJK usepackage indentfirst usepackage amsmath begin CJ
  • 如何让ChatGPT你写一个短视频脚本

    很多网红博主以及各个领域的短视频博主都在使用的 AI编写视频脚本 效率直接提升20倍 很多自媒体平台对于ChatGPT的介绍很少 但是他们都在悄悄利用这个强大的AI来帮助处理工作 关于 如何利用ChatGPT编写视频脚本 这件事 我们今天就
  • 四行代码制作你的esp8266天气时钟——基于NodeMCU、OLED模块

    OLED 开学了 好闲呀 炸鸡 给你找个无休的工作 怎么样 ESP8266 物料 0 96OLED屏幕 esp8266 NodeMCU 开发板 杜邦线 可以自制PCB美化硬件组合 配置方法 四行代码 1 填上wifi或者热点的名称和密码 2
  • Apollo代码学习(三)—车辆动力学模型

    Apollo代码学习 车辆动力学模型 前言 车辆动力学模型 横向动力学 方向盘控制模型 总结 补充 2018 11 27 前言 接上一篇 Apollo代码学习 二 车辆运动学模型 主要参考资料仍是这三个 1 Rajamani R Vehic
  • Java学习心得

    Java学习心得 一 Java入门 Java是一门面向对象编程语言 不仅吸收了C 语言的各种优点 还摒弃了C 里难以理解的多继承 指针等概念 我初次接触java时 发现它和c语言有一些不同 不仅要定义类 还要搭建环境 我也是在同学的帮助下才
  • MySQL常见面试题(2023年最新)

    目录 前言 1 char和varchar的区别 2 数据库的三大范式 3 你了解sql的执行顺序吗 4 索引是什么 5 索引的优点和缺点 6 索引的类型 7 索引怎么设计 优化 8 怎么避免索引失效 也属于sql优化的一种 9 索引的数据类
  • JAVA多线程-线程安全问题

    一 CPU多核缓存架构 CPU分为三级缓存 每个CPU都有L1 L2缓存 但是L3缓存是多核公用的 CPU查找数据的顺序为 CPU gt L1 gt L2 gt L3 gt 内存 gt 硬盘 进一步优化 CPU每次读取一个数据 并不是仅仅读