并发编程(二) — 内存可见性问题

news2024/11/24 19:56:44

目录

前言

内存可见性问题

synchronized

volatile

CAS算法

CAS算法原理

CAS算法应用场景

CAS算法代码实现

参考目录


前言

        在谈共享变量的内存可见性问题之前,先谈谈线程安全问题 ,线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。

在图2-3 中, 线程A 和线程B 可以同时操作主内存中的共享变量,当同时操作同一个共享变量时,可能导致线程安全问题。

注:这里说的操作是指写操作,多个读操作并不会产生线程安全问题,因为并没有修改共享变量的数据。只有当至少一个线程修改共享资源时才会存在线程安全问题。

例:

        线程A、B在同一时间段内访问贡献变量count资源(count为一个计数器,每次做加一操作)

        假如当前count=0 ,在t1时刻线程A 读取count 值到本地变量countA 。然后在t2 时刻递增countA 的值为1,同时线程B 读取count 的值0 到本地变量countB ,此时countB的值为0 (因为countA 的值还没有被写入主内存)。在t3 时刻线程A 才把countA 的值1写入主内存, 至此线程A 一次计数完毕,同时线程B 递增CountB 的值为1 。在t4 时刻线程B 把countB 的值1写入内存,至此线程B 一次计数完毕。最后结果为1,与预期的2不符。

t1

t2

t3

t4

线程A

从内存读取count值到本线程

递增本线程count的值

写回主内存

线程B

从内存读取count值到本线程

递增本线程count的值

写回主内存

注:当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存线程读写变量时操作的是自己工作内存中的变量。

内存可见性问题

        上面提到了在多线程下,线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存线程读写变量时操作的是自己工作内存中的变量。其内存模型如下:

Java 内存模型是一个抽象的概念,那么在实际实现中线程的工作内存是什么呢?

        图中所示是一个双核CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU 都共享的二级缓存。那么Java 内存模型里面的工作内存,就对应这里的Ll 或者L2 缓存或者CPU 的寄存器。

        当一个线程操作共享变量时, 它首先从主内存复制共享变量到自己的工作内存, 然后对工作内存里的变量进行处理, 处理完后将变量值更新到主内存。

        那么假如线程A 和线程B 同时处理一个共享变量, 会出现什么情况?

        我们使用上图所示CPU 架构, 假设线程A 和线程B 使用不同CPU 执行,并且当前两级Cache 都为空,那么这时候由于Cache 的存在,将会导致内存不可见问题, 具体看下面的分析。

  • 线程A 首先获取共享变量X 的值,由于两级Cache 都没有命中,所以加载主内存中X 的值,假如为0。然后把X=0 的值缓存到两级缓存, 线程A 修改X 的值为1,然后将其写入两级Cache , 并且刷新到主内存。线程A 操作完毕后,线程A 所在的CPU 的两级Cache 内和主内存里面的X 的值都是1。
  • 线程B 获取X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X= 1 ; 到这里一切都是正常的, 因为这时候主内存中也是X= 1 。然后线程B 修改X 的值为2 , 并将其存放到线程B所在的一级Cache 和共享二级Cache 中,最后更新主内存中X 的值为2 ; 到这里一切都是好的。
  • 线程A 这次又需要修改X 的值, 获取时一级缓存命中, 并且X= 1 ,到这里问题就出现了,明明线程B 已经把X 的值修改为了2 ,为何线程A 获取的还是1呢? 这就是共享变量的内存不可见问题, 也就是线程B 写入的值对线程A 不可见。

所以,共享变量内存可见性问题主要是由于线程的工作内存导致的!

如何解决共享变量的内存不可见性呢?

synchronized

        没有什么问题是一把锁解决不了的,不行就再加一把🤣🤣

  synchronized 块是Java 提供的一种原子性内置锁, Java 中的每个对象都可以把它当作一个同步锁来使用, 这些Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait 系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后, 其他线程必须等待该线程释放锁后才能获取该锁。

        另外,由于Java 中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized 的使用就会导致上下文切换


        讲完了synchronized 关键字,介绍一下synchronized 的内存语义,即如何解决上面的内存可见性问题的。

        进入synchronized 块的内存语义是把在synchronized 块内使用到的变量从线程的工作内存中清除,这样在synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized 块的内存语义是把在synchronized 块内对共享变量的修改刷新到主内存。

        其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。

        虽然这玩意确实好用,but synchronized 关键字会引起线程上下文切换并带来线程调度开销!!!

volatile

        上面提到synchronized 锁太重了,会引起线程上下文切换并带来线程调度开销。所以又提供了另外一种解决内存可见性的方法。

        Java 提供了一种弱形式的同步,也就是使用volatile 关键字。该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时 ,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile 的内存语义和synchronized 有相似之处,具体来说就是,当线程写入了volatile 变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile 变量值时就相当于进入同步块先清空本地内存变量值,再从主内存获取最新值) 。

下面看一个使用volatile 关键字解决内存可见性问题的例子。如下代码中的共享变量value 是线程不安全的,因为这里没有使用适当的同步措施。

public class ThreadNotSafeinteger (
    private int value ;
    public int get() {
    	return value;
	}

    public void set(int value) {
    	this.value =value;
    }
}

首先来看使用synchronized 关键宇进行同步的方式。

public class ThreadSafeinteger {
    private int value ;
    public synchronized int get() {
    	return value;
    }
    public synchronized void set (int value) {
 	   this.value = value;
    }
}

然后是使用volatile 进行同步:

public class ThreadSafeinteger {
    private volatile int value ;
    
    public int get() (
    	return value;
    }
    publiC void set (int value) {
    	this.value = value ;
    }
}

在这里使用synchronized 和使用volatile 是等价的,都解决了共享变量value 的内存可见性问题,但是前者是独占锁同时只能有一个线程调用get()方法,其他调用线程会被阻塞,同时会存在线程上下文切换和线程重新调度的开销,这也是使用锁方式不好的地方。而后者是非阻塞算法, 不会造成线程上下文切换的开销。但并非在所有情况下使用它们都是等价的, volatile 虽然提供了可见性保证,但并不保证操作的原子性。

那么一般在什么时候才使用volatile 关键字呢?

  • 写入变量值不依赖、变量的当前值时。因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性的,而volatile 不保证原子性。
  • 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。

CAS算法

        在Java 中, 锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起, 这会导致线程上下文的切换和重新调度开销。Java 提供了非阻塞的volatile 关键字来解决共享变量的可见性问题, 这在一定程度上弥补了锁带来的开销问题,但是volatile 只能保证共享变量的可见性不能解决读—改一写等的原子性问题

CAS算法原理

  CAS(Compare And Swap)算法是一种无锁的同步机制,是非阻塞原子性操作,它的原理是先比较内存中的值是否与期望值相等,如果相等,则将新值写入内存;否则不做任何操作。CAS算法的核心是原子性操作,即在执行比较和写入操作时,不会被其他线程干扰。(通过硬件保证了比较更新操作的原子性)

CAS算法的基本流程如下:

  1. 读取内存中的值;
  2. 比较内存中的值与期望值是否相等;
  3. 如果相等,则将新值写入内存;
  4. 如果不相等,则不做任何操作。

CAS算法的优点是无锁,可以避免线程阻塞,提高程序的并发性能。但是CAS算法也有一些缺点,比如ABA问题、循环时间长等。

ABA 问题具体如下: 假如线程I 使用CAS 修改初始值为A 的变量X , 那么线程I 会首先去获取当前变量X 的值(为A ), 然后使用CAS 操作尝试修改X 的值为B , 如果使用CAS 操作成功了, 那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程I 获取变量X 的值A 后,在执行CAS 前,线程II 使用CAS 修改了变量X 的值为B ,然后又使用CAS 修改了变量X 的值为A 。所以虽然线程I 执行CAS时X 的值是A , 但是这个A 己经不是线程I 获取时的A 了。这就是ABA 问题。

ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A 到B,然后再从B 到A。如果变量的值只能朝着一个方向转换,比如A 到B , B 到C , 不构成环形,就不会存在问题。JDK 中的AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳, 从而避免了ABA 问题的产生。

CAS算法应用场景

CAS算法适用于多线程环境下的共享变量的更新操作。比如计数器、标志位等。CAS算法可以保证在多线程环境下,共享变量的更新操作是原子性的,避免了线程安全问题。

CAS算法代码实现

在Java中,CAS算法的实现是通过Unsafe类来实现的。Unsafe类提供了一些底层的操作,比如内存操作、线程操作等。下面是一个使用CAS算法实现计数器的示例代码:

import sun.misc.Unsafe;

public class Counter {
    private volatile int count;
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long offset;

    static {
        try {
            offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("count"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    public void increment() {
        int current;
        do {
            current = unsafe.getIntVolatile(this, offset);
        } while (!unsafe.compareAndSwapInt(this, offset, current, current + 1));
    }

    public int getCount() {
        return count;
    }
}

在上面的代码中,我们使用了volatile关键字来保证count变量的可见性。在increment方法中,我们使用了do-while循环来不断尝试更新count变量的值。如果更新成功,则退出循环;否则继续尝试更新。在更新操作中,我们使用了compareAndSwapInt方法来实现CAS算法。

参考目录

《Java并发编程之美》

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

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

相关文章

c高级day4作业

有m1.txt m2.txt m3.txt m4.txt,分别创建出对应的目录,m1 m2 m3 m4 并把文件移动到对应的目录下使用break关键字打印九九乘法表,提示:printf "%d * %d %d" $i $j $((i*j)) #!/bin/bash for i in m1 m2 m3 m4 do#文件夹…

只限今日免费,Midjourney 5.1震撼更新!逼真到给跪,中国情侣细节惊艳,3D视频大片马上来

来源 | 新智元 微信号:AI-era 【导读】全新升级的Midjourney让全网又疯狂了,创造力解禁,出图更逼真。重要的是,限时免费到今天,要玩的抓紧了。 一个月前,Midjourney V5画的一对中国完美情侣在网上爆火&am…

涅槃重生,BitKeep如何闯出千万用户新起点

在全球,BitKeep钱包现在已经有超过千万用户在使用。 当我得知这个数据的时候,有些惊讶,也有点意料之中。关注BitKeep这几年,真心看得出这家公司的发展之迅速。还记得2018年他们推出第一个版本时,小而美,简洁…

python和pycharm的安装(安装包免费下载共享)

说明: 本文内容包括Python和Pycharm安装。 一、python安装: python是一门编程语言,安装python是为了能在电脑上使用这门语言。 1、python下载 下载链接:https://pan.baidu.com/s/1mWsJjO8HngNQtINCzu0bBA 提取码:9…

又又又发现了一个 AI 插件神器 TeamSmart

简介 TeamSmart AI 是最近比较火的 Chrome 插件,它是基于 ChatGPT 集成的 AI 助手团队工具 对,没错,是一个团队。这个团队里面有许多不同角色的成员,每隔成员都有自己的专业领域,比如商业、市场营销、灵魂写手、程序…

批量查看域名历史软件-网站老域名批量查询注册

未注册备案域名批量扫描软件 未注册备案域名批量扫描软件是专门用于批量扫描未备案的域名的一种工具。它可以快速识别未备案的域名,并帮助用户抓住还未被注册的值得备案的域名,以便用户及时注册备案并使用。 该软件主要具有以下几个优点: 高…

【小程序】输入框检验姓名、身份证(正则表达式)并提交

目标 输入绑定姓名、身份证号并进行校验若未填或校验不通过则显示绑定失败的轻提示若通过校验并提交则显示绑定成功 使用Vant Weapp (gitee.io)库。 思路与代码 html: wx:model绑定输入框输入的值data-key是一个属性,在js中的e.currentTarget.datase…

【新星计划-2023】ARP“攻击”与“欺骗”的原理讲解

网络管理员在网络维护阶段需要处理各种各样的故障,出现最多的就是网络通信问题。除物理原因外,这种现象一般是ARP攻击或ARP欺骗导致的。无论是ARP攻击还是ARP欺骗,它们都是通过伪造ARP应答来实现的。 一、ARP攻击原理 一般情况下&#xff0…

TypeScript语言编译命令

1. 安装 npm install -g typescript2. 编译 tsc工具是TypeScript编译器的控制台接口,它可以将TypeScript文件编译成JavaScript文件; 编译文件: tsc [options] [file ...]查看编译命令的帮助信息: tsc --help或者 tsc -h或者 tsc…

微服架构基础设施环境平台搭建 -(一)基础环境准备

微服架构基础设施环境平台搭建 -(一)基础环境准备 通过采用微服相关架构构建一套以KubernetesDocker为自动化运维基础平台,以微服务为服务中心,在此基础之上构建业务中台,并通过Jekins自动构建、编译、测试、发布的自动…

【Java AWT 图形界面编程】IntelliJ IDEA 乱码问题最佳配置方案 ( 配置文件编码 | 配置编译器编码参数 | 配置运行时编码参数 )

文章目录 一、IntelliJ IDEA 乱码问题二、IntelliJ IDEA 乱码问题最佳配置方案1、文件编码设置成 UTF-82、编译器编码参数设置成 UTF-83、 配置运行时编码参数为 GBK 一、IntelliJ IDEA 乱码问题 在 IntelliJ IDEA 中开发 AWT / Swing 图形界面程序 , 经常遇到乱码问题 ; 文件…

博文的跑路笔记

HTML CSS Flex布局 使用flex布局 容器 .box {display: flex; }行内元素 .box {display: inline-flex; }flex布局后,float、vertical-align、clear失效。 容器属性 flex-direction:主轴方向 属性值 row:子元素起点在左,左到右。…

初级面试问到rabbitMQ,看这一篇文章就够了!

一、rabbitMQ的基础要点回顾 1.使用场景 1)解耦:使用消息队列避免模块间的直接调用。将所需共享的数据放在消息队列中,对于新增的业务模块,只要对该类消息感兴趣就可以订阅该消息,对原有系统无影响,降低了…

防范化解灾害风险,科技筑牢安全城墙

气象灾害:大气圈变异活动会对人类生命财产和国民经济及国防建设等造成的直接或间接损害。我国气象灾害种类繁多,不仅包括台风、暴雨、冰雹、大风、雷暴、暴风雪等天气灾害,还包括干旱、洪涝、持续高温、雪灾等气候灾害。此外,与气…

Docker中部署监控

Docker概念 一、部署Prometheus+grafana环境 1.1 、部署Prometheus+grafana环境 docker pull registry.cn-hangzhou.aliyuncs.com/lhrbest/lhrprometheus:1.0 docker tag registry.cn-hangzhou.aliyuncs.com/lhrbest/lhrprometheus:1.0 lhrbest/lhrprometheus:1.01.2 、创建镜…

【分布式事务】Seata 之 @GlobalTransactional 在TM侧的核心逻辑

文章目录 一、概述二、GlobalTransactional核心逻辑三、GlobalTransactional核心源码解读四、事务能力的启停运行期的开关变更 一、概述 Seata 依赖 Spring 的注解机制,实现声明式事务,即开发者给 Bean 使用GlobalTransactional注解 ,Seata …

深度学习03-卷积神经网络(CNN)

简介 CNN,即卷积神经网络(Convolutional Neural Network),是一种常用于图像和视频处理的深度学习模型。与传统神经网络相比,CNN 有着更好的处理图像和序列数据的能力,因为它能够自动学习图像中的特征&…

GIS 根据投影坐标点获取投影坐标所属的投影坐标系EPSG

什么是EPSG? EPSG(The European Petroleum Survey Group,官网 http://www.epsg.org/)维护着空间参照对象的数据集,OGC标准中空间参照系统的SRID(Spatial Reference System Identifier)与EPSG的空间参照系统ID相一致。…

冒泡数组实现和冒泡数组的改进以及插入法排序

概念 在数组排序的过程中&#xff0c;每次比较相邻的两个数&#xff0c;并且把大的数放在后面 例如{1&#xff0c;3&#xff0c;5&#xff0c;7&#xff0c;9&#xff0c;2&#xff0c;4&#xff0c;6&#xff0c;8&#xff0c;10} 实现 #include<iostream> using names…

修改ro.debuggable用于调试安卓应用

一.Root 网上有很多root教程,这里推荐: 玩机必看&#xff01;带你入坑安卓刷机&#xff0c;小白也能看懂的ROOT基础指南来啦&#xff01; 很详细的介绍了Root的原理和方法,强烈推荐使用Magisk工具 可以使用命令adb shell getprop ro.debuggable查看ro.debuggable的值 二.Magi…