Java并发编程--变量可见性、避免指令重排,还得是用它

news2024/11/25 14:27:51

那怎么保证程序里一个线程对共享变量的修改能立马被其他线程看到了?这时候有人会说了,加锁呀,前面不就是因为加锁成本太高才使用的 ThreadLocal的吗?怎么又说回去了?

其实CPU每个核心也都是有缓存的,今天要讲的volatile能保证变量在多线程间的可见性,本文我们会对变量可见性、指令重排、Happens Before 原则以及 Volatile 对这些特性提供的支持和在程序里的使用进行讲解,本文大纲如下:

变量的可见性

一个线程对共享变量的修改,另外一个线程能够立刻看到,称为变量的可见性。

在单核系统中,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。但是多核系统中,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。

比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言不具备可见性。

Java 里可以使用 volatile 关键字修饰成员变量,来保证成员在线程间的可见性。读取 volatile 修饰的变量时,线程将不会从所在CPU的缓存,而是直接从系统的主存中读取变量值。同理,向一个 volatile 修饰的变量写入值的时候,也是直接写入到主存。

下面我们再来看一下,当不使用 volatile 时,多线程使用共享变量时的可见性问题。

Java 变量的可见性问题

Java 的 volatile 关键字能够保证变量更改的跨线程可见,在一个多线程应用程序中,为了提高性能,线程会把变量从主存拷贝到线程所在CPU信息的缓存上再操作。如果程序运行在多核机器上,多个线程可能会运行在不同的CPU 上,也就意味着不同的线程可能会把变量拷贝到不同的 CPU 缓存上。

因为CPU缓存的读写速度远高于主存,所以线程会把数据从主存读到 CPU 缓存,数据的更新也是是先更新CPU 缓存中的副本,再刷回主存,除非有(汇编指令)强制要求否则不会每次更新都把数据刷回主存。

对于非 volatile 修饰的变量,Java 无法保证 JVM 何时会把数据从主存读取到 CPU 缓存,或将数据从 CPU 缓存写入主内存。

这在多线程环境下可能会导致问题,想象一下这样一种情况,有多个线程可以访问一个共享对象,该对象包含一个声明如下的计数器变量。

public class SharedObject {

    public volatile int counter = 0;

}
复制代码

假设在我们的例子中只有线程1 会更新计数器 counter 的值,线程1 和线程2 都会时不时的读取 counter 的值。 如果 counter 未被声明为 volatile 的,则无法保证变量 counter 的值何时会从 CPU 缓存写回主存。这意味着,CPU 缓存中的计数器变量值可能与主内存中的不同。比如像下图这样:

线程2 访问 counter 的值的结果是 0 ,没有看到变量 counter 最新的值。这是因为 counter 它最新的值还在CPU1 的缓存中,还没有被线程1 写回到主内。

上面这个例子描述的情况,就是所谓“可见性”问题:一个线程的更新对其他线程是不可见的。

Volatile 的可见性保证

Java 的 volatile 关键字旨在解决变量可见性问题。通过将上面例子中的 counter 变量声明为 volatile的,所有对counter 变量的写入都将立即写回主存,所以对 counter 变量的读取都会先将变量从主存读到CPU缓存 (相当于每次都从主存读取)。

把 counter 变量声明成 volatile 只需要在定义中加上 volatile 关键字即可

public class SharedObject {

    public volatile int counter = 0;

}
复制代码

完整的 volatile 可见性保证

实际上,volatile 的可见性保证超出了 volatile 修饰的变量本身。它的可见性保证规则如下:

  • 如果线程 A 写入一个 volatile 变量,而线程 B 随后读取了同一个 volatile 变量,那么线程 A 在写入 volatile 变量之前,对线程 A 可见(更新可见)的所有变量,在线程 B 读取 volatile 变量之后也将对线程 B 可见。
  • 如果线程 A 读取一个 volatile 变量,那么在读取 volatile 变量时,线程 A 可见的所有变量也将从主存中重新读取。

我们通过例程解释一下这两个规则。

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}
复制代码

udpate() 方法写入三个变量,其中只有变量 days 是 volatile 的。 完整的 volatile 可见性保证意味着,当一个新值被写入到变量 days 时,该线程可见的所有变量也会被写入主内。这意味着,当一个新值被写入变量 days 时,years 和 months 的值也会被写入主存。

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

复制代码

而对于 volatile 变量的读取来说,在上面例程的 totalDays 方法中,当读取 days 变量的值的时候,除了会从主存中重新读取变量 days 的值外,其他两个未被 volatile 修饰的变量 years 和 months 也会被从主存中重新读取到CPU缓存。通过上述读取顺序,可以确保看到 days、months 和 years 的最新值。

指令重排

在指定的语义保持不变的情况下,出于性能原因,JVM 和 CPU 可能会对程序中的指令进行重新排序。比如说,下面这几个指令:

int a = 1;
int b = 2;

a++;
b++;
复制代码

这些指令可以重新排序为以下序列

int a = 1;
a++;

int b = 2;
b++;
复制代码

然而,对于存在被声明为 volatile 的变量的程序而言,我们传统理解的指令重排会导致严重的问题,还以上面使用过的例程来描述一下这个问题。

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}
复制代码

一旦当 update() 方法将新值写入 days 变量时,新写入的 years 和 months 的值也将被写入主存。但是,如果 JVM 重排指令,把程序变成下面这样会怎样呢?

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}
复制代码

重排后变成了先对 days 进行赋值,根于完整可见性的第一条规则,当写入 days 变量时,months 和 years 变量的值也会被写入主存。但是指令重排后,变量 days 的赋值这一次是在新值写入 months 和 years 之前发生的。因此,它们的新值不会正确地对其他线程可见。

显然,重新排序的指令的语义已经改变,不过 Java 内部会有解决方案防止此类问题的发生。

volatile 的 Happens Before 保证

为了解决上面例子里指令重排导致的问题,除了可见性保证之外,Java 的 volatile 关键字还提供了“happens-before”保证。

  • 如果原来位于写 volatile 变量之前的非 volatile 变量的读写,在指令重排时,不允许这些指令出现在 volatile 变量的写入指令之后。但是原来在 volatile 变量写入之后的对其他变量的读写指令,在重排时,是允许出现在写 volatile 变量之前的--即从后变前允许,从前变后不行。
  • 如果原来位于读 volatile 变量之后的对非 volatile 变量的读写,在指令重排时,不允许出现在读 volatile 变量之前。

上面的 Happens-Before 保证确保了 volatile 在程序发生指令重排时也能提供正确的可见性保证。

volatile 不能保证原子性

虽然 volatile 关键字保证了对 volatile 修饰的变量的所有读取都直接从主存中读取,对 volatile 变量的所有写入都会写入到主存中,但 volatile 不能保证原子性。

在前面共享计数器的例子中,我们设置了一个前提--只有线程1 会更新计数器 counter 的值,线程1 和线程2 会时不时的读取 counter 的值。在这个前提下,把 counter 变量声明成 volatile 的足以确保线程 2 始终能看到线程1最新写入的值。

事实上,当写入变量的新值不依赖先前的值(比如累加)的时候,多个线程都向同一个 volatile 变量写入时,是能保证向主存中写入的是正确的值的。但是,如果需要首先读取 volatile 变量的值,并基于该值为 volatile 变量生成一个新值,那么 volatile 就不能保证变量正确的可见性了。读取 volatile 变量和写入新值之间的这个短短的时间间隔,在多线程并发写入的情况下也是会产生 Data Racing 的。

想象一下,如果线程 1 将值为 0 的 counter 变量读取到运行它的 CPU 的缓存中,将其递增到 1,在线程1把 counter 的值写回主存之前,线程 2 可能正好也从主内存中把 counter 变量读到了运行它的 CPU 缓存中,读取到的 counter 变量的值也是 0,然后线程 2 也对 counter 变量进行递增的操作。

线程 1 和线程 2 现在实际上已经不同步了。理论上 counter 变量从 0 经过两次递增应该变成 2,但实际上每个线程在其 CPU 缓存中的 counter 变量的值为 1,即使线程最终将 counter 变量的值写回主存,它的值也是不对的。

那么,如何做到线程安全呢?有两种方案:

  • volatile + synchronized
  • 使用原子类替代 volatile

原子类后面到 J.U.C 相关的章节的时候再去学习。

什么时候适合使用 volatile

如果 volatile 修饰符使用恰当的话,它比 synchronized 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。但是要注意 volatile 是无法替代 synchronized ,因为 volatile 无法保证操作的原子性

通常来说,使用 volatile 必须具备以下 2 个条件

  • 对变量的写操作不依赖于当前值
  • volatile 变量没有包含在具有其他变量的表达式中

示例:双重锁实现线程安全的单例模式

class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}
复制代码

volatile 的原理

使用 volatile 关键字时,程序对应的汇编代码在对应位置会多出一个 lock 前缀指令lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供 3 个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。

注意 volatile 的性能问题

读取和写入 volatile 变量都会直接访问主存,读写主存比访问 CPU 缓存更慢得多,不过使用 volatile 变量还可以防止指令重排,这是一种正常的性能增强技术。因此,我们只应该在确实需要变量的可见性和防止指令重排时,再使用 volatile 变量。

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

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

相关文章

基于el-form实现自动展开/收起的查询条件组件

说明 如果查询条件过多,影响页面的展示效果,网上看了一些实现自动展开/收起的,有根据最小高度控制的,有基于条件的如v-show来控制,下面借助js原生的hidden属性实现要素的显示、隐藏控制。 先一下效果: 优…

web扫码登录

文章目录需求流程交互流程服务交互流程关键思路代码生成二维码,返回给PC展示轮询查询二维码状态APP扫码请求登录总结需求 pc端实现app扫码登录 流程 交互流程 服务交互流程 关键思路 主要问题在于如何识别APP端用户,然后传递给PC端已经登录成功 通过…

小程序瀑布流实现

什么是瀑布流布局 瀑布流布局,一般等宽,不等高的列表排列 原理是找出高度之和最小的那一列,在高度最小列继续添加元素 可以通过 absolute 定位实现,动态计算每一项的 top 和 left 封装瀑布流方法 function getAllRect(context…

HTML期末作业课程设计期末大作业--小米网站开发者平台首页 1页

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材,DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 公司官网网站 | 企业官网 | 酒店官网 | 等网站的设计与制| HTML期末大学生网页设计作业,Web大学生网页 HTML:结构 CSS&#x…

MyBatis学习笔记(2022-11-30)

熬过无人问津的日子才会有诗和远方。 文章目录一、MyBatis简述二、快速入门三、MyBatis配置文件详解1. MyBatis核心配置文件1.1 configuration(配置)1.2 properties(属性)1.3 environments(环境配置)1.4 ty…

vue项目 element UI input框扫码枪扫描过快 出现数据丢失问题(已解决二)

项目需求: 输入框要掉两个接口,根据第一个验证接口返回的code,弹不同的框,点击弹框确认再掉第二个接口 根据客户现场反应,扫描枪快速扫描会出现 料号前几位字符丢失 不完整的问题。于是开始了测试之路。 解决方案探索 1.首先考…

数据可视化,销量第一的新能源汽车是什么?比亚迪新能源汽车销量接近60万辆

去年以来,新能源汽车火热度席卷全球,中国的新能源汽车无论制造或者销售,数量增长迅猛。下面小编用一款数据可视化软件,带你用可视化数据解读高端制造背后,中国新能源汽车的具体销售情况。同样如果你工作上有数据报表需…

[附源码]计算机毕业设计springboot酒店物联网平台系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

全国省市县 经纬度的 json数据(提供原文件),写Java代码,入库(提供代码)

目录 1 需求2 分析1 需求 有一个全国省市县 经纬度的 json数据,我想要使用代码入库 如何操作,代码咋写 2 分析 首先分析json结构, 一般拿到一个json数据,如果最外层不是 { } 包裹的,那么自己手动加一个 以上这个是自己加的,这个就是key 值就是list 集合 分析完json数…

【并发】深入理解Java线程的底层原理

线程基础知识 线程与进程 进程 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。 线…

LDcad零件新增与导入

LDcad大颗粒小颗粒套装导入方法,以后LDcad也可以用套装搭建模型了。 LDcad大颗粒小颗粒套装导入方法, 以后LDcad也可以用套装搭建模型了。 有个遗憾,就是零件不全。 具体导入方法看下文。 我们可以看到。这些套装都有对应的图标。方便…

环境温湿度在线监测如何实现?有何应用场景?

温度、湿度等环境数据与人们生活生产息息相关。温湿度传感器作为能将温度量和湿度量转换成容易被测量处理的电信号的设备或装置,广泛应用于工农业生产、气象、环保、国防、科研等经常需要对环境或设备的温度与湿度进行测量的领域,因此也产生了对温湿度远…

ASP.NET Core 3.1系列(15)——Entity Framework Core之DB First

1、前言 本文开始介绍一些关于Entity Framework Core的内容。在EFCore中,常用的为DB First模式和Code First模式,下面就来介绍一下如何在EFCore中使用DB First模式生成实体类和数据库上下文。 2、创建测试数据库 在SQL Server中新建一个数据库Dao&…

2016-04《信息资源管理 02378》真卷解析,逐题解析+背诵技巧

本系列博客合计 21 篇,每篇都将解析一张《信息资源管理》真卷,并附带答案解析与背诵技巧。 全国 2016 年 4 月自学考试信息资源管理试题(02378) 单选题 1、按信息表现形式划分,信息可分为(C) …

JavaScript的Web api接口

JavaScript的Web api 文章目录JavaScript的Web api选中元素事件操作元素获取/元素内容获取/修改元素属性获取/修改表单元素属性实现一个显示/隐藏 密码的功能实现一个加减计算器复选框全选/全不选获取/修改样式属性点击文字放大字体实现白天模式与夜间模式的切换操作节点新增节…

【附源码】计算机毕业设计JAVA住房公积金筹集子系统的网站系统

【附源码】计算机毕业设计JAVA住房公积金筹集子系统的网站系统 目运行 环境项配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术&#xf…

3D漫游:所见即所得的形式,构建线上数字展厅

企业在数字化转型的大环境下,较为常用的当属数字展厅了,数字展厅能够为企业、行业协会、展销基地以及体验中心助力,以所见即所得的形式构建线上数字空间,满足企业的数字化展示和数字化营销。3D漫游,更加沉浸式的三维空…

使用 Hibernate Envers 进行实体审计

业务应用程序中的常见要求是在特定数据更改时存储版本控制信息;当某事发生变化时,谁改变了它,改变了什么。在这篇博文中,我们将介绍Hibernate Envers,它是Hibernate JPA库的一个组件,它为实体类提供了一个简单的审计/版…

【Linux网络配置实战】服务器Network静态路由配置

【Linux网络配置实战】服务器Network静态路由配置一、环境介绍1.环境规划2.实验目的二、检查各节点IP地址1.检查server01服务器上2.检查server02服务器网卡3.检查route01上的网卡三、在route01上启动IP包转发四、查看当前两节点互通情况1.查看server01和server02连通状态2.查看…

新手小白可以做什么互联网项目,副业项目应该怎么选择

现在网上的信息这么冗杂,有没有可靠的副业项目呢?怎样才能找到适合自己的副业呢? 说实话,在网上找副业并不难,搜索一下就会出来很多,但新手小白不知道如何选择,导致焦虑,一个重要的…