JUC专题-线程安全性之可见性有序性

1 可见性

1.1 可见性问题引出

来看一段代码:

arduino 复制代码
public class VolatileDemo {
    public static boolean stop=false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
            }
        });
        t1.start();
        System.out.println("begin start thread");
        Thread.sleep(1000);
        stop=true;
    }
}

是t1线程中用到了stop这个属性,接在在main线程中修改了 stop 这个属性的值来使得t1线程结束,但是t1线程并没有按照期望的结果执行,也就是这个代码永远不会结束,为什么出现了这个情况呢,其实就是main线程对stop的修改对t1线程不可见

1.2 了解可见性问题

我们先来看一张图:

计算机是利用CPU进行数据运算的,但是CPU只能对内存中的数据进行运算,对于磁盘中的数据,必须要先读取到内存,CPU才能进行运算,也就是CPU和内存之间无法避免的出现了IO操作。但是矛盾的是,Cpu、内存和磁盘三者的速度差异很大,自然而然就会有一个问题:cpu可能需要等待内存的iO,这样肯定是不合理的

为了平衡这三者之间的速度差异,最大化的利用CPU。所以在硬件层面、操作系统层面、编译器层面做出了很多的优化

  1. CPU增加了高速缓存
  2. 操作系统增加了进程、线程。通过CPU的时间片切换最大化的提升CPU的使用率
  3. 编译器的指令优化,更合理的去利用好CPU的高速缓存

每一种优化,都会带来相应的问题,而这些问题是导致线程安全性问题的根源,那接下来我们逐步去了解这些优化的本质和带来的问题。

1.2.1 CPU层面的缓存

第一个思路就是加缓存,例如我们平时开发的时候觉得数据库慢,也会将数据加载到redis进行读取

对于主流的x86平台,cpu的缓存行(cache)分为L1、L2、L3总共3级。

缓存一致性问题

在多线程环境中,当多个线程并行执行加载同一块内存数据时,由于每个CPU都有自己独立的L1、L2缓存,所以每个CPU的这部分缓存空间都会缓存到相同的数据,并且每个CPU执行相关指令时,彼此之间不可见,就会导致缓存的一致性问题,据图流程如下图所示:

而为了解决一致性问题,我们需要标记cpu缓存中数据的可用性,当线程1更新了a之后,那么线程2所读取的a就应该变为失效状态

简而言之: 如果添加了voliate,当数据被修改后,JVM会向CPU发送一条LOCK的前缀的指令,此时会将修改后的数据从缓存中同步到主内存中;当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效,由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取,进而保证了线程之间的可见性

那么肯定会有一个疑问,为什么不是所有字段都使用这样的方式从而保证可见性呢?

缓存的设计就是为了让cpu尽可能少的去读内存,频繁的缓存同步和主内存读写,会严重降低 CPU 缓存的效率

2 指令重排序代码

CPU在性能优化道路上导致的顺序一致性问题,在CPU层面无法被解决,原因是CPU只是一个运算工 具,它只接收指令并且执行指令,并不清楚当前执行的整个逻辑中是否存在不能优化的问题,也就是说 硬件层面也无法优化这种顺序一致性带来的可见性问题。

因此,在CPU层面提供了写屏障、读屏障、全屏障这样的指令,在x86架构中,这三种指令分别是 SFENCE、LFENCE、MFENCE指令, sfence:也就是save fence,写屏障指令。在sfence指令前的写操作必须在sfence指令后的写操作 前完成。 lfence:也就是load fence,读屏障指令。在lfence指令前的读操作必须在lfence指令后的读操作 前完成。 mfence:也就是modify/mix,混合屏障指令,在mfence前得读写操作必须在mfence指令后的读 写操作前完成。 在Linux系统中,将这三种指令分别封装成了, smp_wmb-写屏障 、 smp_rmb-读屏障 、 smp_mb-读写屏障 三 个方法

相关推荐
计算机毕设定制辅导-无忧学长4 小时前
基于Spring Boot的酒店管理系统
java·spring boot·后端
Victor3564 小时前
Redis(57)Redis的慢查询日志是什么?
后端
Victor3564 小时前
Redis(56)如何监控Redis的内存使用情况?
后端
程序员爱钓鱼5 小时前
Go语言实战案例——进阶与部署篇:使用Go编写系统服务(如守护进程)
后端·google·go
JaguarJack5 小时前
PHP 15 个高效开发的小技巧
后端·php
235165 小时前
【并发编程】详解volatile
java·开发语言·jvm·分布式·后端·并发编程·原理
IT_陈寒5 小时前
JavaScript性能优化:3个被低估的V8引擎技巧让你的代码提速50%
前端·人工智能·后端
洛小豆5 小时前
java 中 char 类型变量能不能储存一个中文的汉字,为什么?
java·后端·面试
爱吃烤鸡翅的酸菜鱼5 小时前
从数据库直连到缓存预热:城市列表查询的性能优化全流程
java·数据库·后端·spring·个人开发