Java并发编程实战3-可见性与volatile关键字

Java并发编程——volatile关键字 Java并发编程——volatile关键字概述一、内存模型二、并发编程的三个概念三、Java内存模型四、剖析volatile关键字4.1 volatile关键字的两层含义4.2 volatile能否保证原子性五、使用volatile关键字场景六、总结常见问题 概述 在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性、可见性和有序性。只要有一条原则没有被保证,就... 阅读详情

1. 缓存一致性问题

在计算机中,每条指令都是在CPU执行的,而CPU又不具备存储数据的功能,因此数据都是存储在主存(即内存)和外存(硬盘)中。但是,主存中数据的存取速度高于外存中数据的存取速度(这也就是为什么内存条的价格会高),于是计算机就将需要的数据先读取到主存中,计算机运算完成后再将数据写入到外存中。

但是,CPU的计算能力太强了,CPU从主存中读取写入数据的速度还是太慢了,严重影响了计算机的性能。因此,CPU就有了高速缓存(cache)。也就是说,当程序再运行的时候,会将运算需要的数据从主存复制一份到CPU的高速缓存中,那么当CPU进行计算时就可以直接从它的高速缓存中读取和写入数据,当运算结束后,再将高速缓存中的数据刷新到主存中。

这样的逻辑在单线程中是没有问题,但是到了多线程中就有问题了。在多核CPU中,每个线程可能有运行在不同的CPU上,因此每个线程运行时都有自己的高速缓存;在单核CPU中,多线程是以线程调度的形式分别执行的,线程间的高速缓存也不同。在开始运算时,每个CPU都将主存中的数据复制到自己的高速缓存中,运算完成之后再将数据刷新到主存中,在这个过程中,问题就出现了。如果没有意识到问题的话,请看下面的例子。假设线程A和线程B都要执行下面的代码:

i = i + 1

假定i的初始值为0,线程A从主存中读取i的值到自己的高速缓存cache A中,此时cache A中i的值为0,CPU进行计算后,cache A中i的值变为了1。此时,线程B的执行进度是不确定:(1)如果线程B已经从主存中读取了i的初始值0到到了cache B中,CPU 计算完成之后,cache B中的i的值也变为了1,如果线程A和线程B分别将自己的缓存中的i值刷新到主存中,那么主存中的i的值最终为1;(2)如果线程B还没有从主存中读取i的值,线程A将cache A中的i刷新到主存中,那么主存中i的值为1,线程B再将主存中的i值读取到cache B中并计算并将i=2刷新到主存中,那么最终主存中i的值变为2。上述情况是假定线程A先执行的,如果线程B先执行时同样存在问题。

上面的问题上就是缓存一致性问题。

于是,就出现了缓存一致性协议,最著名的就是Intel的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。由于没有对缓存一致性协议进行详细的了解,本文不再介绍,想要了解的读者请参考文章《缓存一致性(Cache Coherency)入门》。此时,多线程计算模型就如下图所示:

多线程计算模型

2. volatile关键字

对于缓存一致性问题,我们可以通过对代码块加锁的方法来解决:

synchronized(lock) {
    i += 1;
}

通过synchronized来解决缓存一致性问题的原因,《并发编程实战》中是这么写的:

当线程B执行到与线程A相同的锁监视的同步块时,A在同步块之中或之前所做的每件事,对B都是可见的。没有同步,就没有这样的保证。

本文的理解是:相同的锁监视的同步块,每一时刻只能保证有一个线程获得锁并进入,其它的线程就等待或阻塞直到获得获得锁。相当于,同步块中的代码是串行执行的,当然就不会出现缓存一致性的问题了。

注意,无论是书中写的还是本文理解的,都在强调相同的锁监视器,也就是说,不同线程中的synchronized使用的锁要是同一个。

至此,正如《并发编程实战》中写的:

锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程都能够看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

至此,synchronized的作用:
1. 复合操作的原子化和互斥;
2. 内存可见性。

除了加锁之外,Java也提供另外一种保存内存可见性的方法:volatile变量。一旦一个共享变量被volatile修饰之后,那么久具备两层语义:
1. 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对于其它线程来说是立即可见的;
2. 禁止进行指令重排。

下面通过一段代码来描述volatile关键字的作用:

public class NoVisibility {
    private static boolean ready;

    private static class ReaderThread extends Thread {
        public void run() {
            while(!ready){
                Thread.yield();
            }
        }
    }

    public static void main(String[] args){
        new ReaderThread().start();
        ready = true;
        //when ready=true, do something
    }
}

上面的这段代码是常见的采用标记中断线程的方法,然而这段代码却不一定能正常的执行。如同上文所讲,每个线程都有自己的cache,主线程main和子线程ReaderThread都有自己的缓存,开始执行之后,子线程ReaderThread会使用自己缓存中的ready值进行判断,而在主线程main中,在设置ready=ture之后,还要做其它的事情,只是将ready=true写到了自己的cache中,并没有将ready的值刷新到主存中,子线程ReaderThread也就不会停止。

但是,当用volatile修饰ready之后就变得不同了:
1. 使用volatile关键字之会强制将修改的值立即刷新到主存中;
2. 使用volatile关键字之后,当main线程进行修改时,会导致子线程ReaderThread的cache中的ready的值无效;
3. 由于子线程ReaderThread缓存中的ready的值无效,所以再次读取ready值时回到主存中读取。

这样,就保证了main线程及其子线程的正常执行。

3. volatile与synchronized的区别:

加锁可以保证原子性和可见性; volatile变量只能保证可见性。

也就是说,volatile变量的操作不会加锁,不会使得操作对象为volatile变量的复合操作原子化,也不会引起执行线程的阻塞,相对于synchronized而言,只是轻量级的同步机制。

4. volatile的禁止指令重排

volatile关键字禁止指令重排有两层含义:

  1. 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定已经全部进行,且结果对后面的操作可见;在其后面的操作肯定还没有进行;
  2. 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

下面通过代码来理解:

//x、y is not volatile variable
//flag is volatile variable

x = 1; //line 1
y = 2; //line 2
flag = true; //line 3
x = 3; //line 4
y = 4; //line 5

由于flag是volatile变量,x、y不是volatile变量,在进行指令重排的时候,不会将第3行的语句放到第1行或第2行语句的前面,也不会放到第4行或第5行的后面,第1行和第2行的语句可以重排,但是一定始终在第3行的语句的前面,第4行和第5行的语句也可以重排,但是也一定在第3行语句的后面。

虽然,这个小的程序中,并没有大的作用,但是当第1、2、4、5行的语句是一些重要且复杂的语句时,效果就明显了:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

在这个程序中,如果inited变量不是volatile变量,那么语句1和语句2可以重排,就有可能导致语句2先执行,那么导致语句1中的真正执行初始化的操作并没有执行,线程2却已经认为已经初始化了,开始执行操作,就会导致程序错误。

5. volatile的原理和实现机制

volatile是如何保证可见性和禁止指令重排呢,《深入理解Java虚拟机》中有如下解释:

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,家兔volatile关键字时,会多出一个lock前缀指令

lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:
1. 它确保执行重排时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令重排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2. 它会强制将对缓存的修改操作立即写入主存;
3. 如果是写操作,它会导致其它CPU中对应的缓存行无效。

6. volatile使用的场景

synchronized关键字可以保证原子性和可见性,但是影响执行效率;volatile可以保证变量的可见性,但是不能保证原子性,在某些情况下性能优于synchronized。因此,volatile不能替代synchronized,volatile使用时,应该满足下面2个条件:
1. 写入变量时不依赖变量的当前值;
2. 变量不需要与其它的状态变量共同参与不变约束。

本文的理解是,第一个条件是说,使用这个volatile变量仅仅是提供数值给其它的语句用,本身的修改依赖于自己的当前值;第二个条件是说,volatile变量不能与其它的变量一起参与运算作为某种约束,也就是说此时这个约束仍然不是原子的。

volatile适用的场景:

1. 状态标记位
volatile boolean inited = false;
//线程1:
context = loadContext();  
inited = true;            

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2. double check
class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(null == instance){
            synchronized (Singleton.class) {
                if(null == instance){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
参考资料

Java并发编程:volatile关键字解析

缓存一致性(Cache Coherency)入门

致谢

本文主要是在参考文献Java并发编程:volatile关键字解析,以及《并发编程实战》的相关章节的基础上,加上作者的理解写的,非常感谢前辈们的无私奉献!

Java并发编程中的volatile关键字:原理与实战 在Java并发编程中,多线程协作时经常遇到数据不一致、逻辑混乱等问题。本文聚焦“volatile关键字”,系统讲解其核心原理(可见性、有序性)、底层实现(内存屏障)、实战用法(状态变量、单例模式),以及常见误区(无法保证原子性),覆盖从基础概念到工程实践的全流程。本文从生活故事引入,逐步拆解volatile的核心概念(可见性、有序性),结合JMM内存模型和内存屏障讲解底层原理,通过代码实战演示正确用法,最后总结使用场景与未来趋势。可见性:一个线程对共享变量的修改,其他线程能立即看到最新值的特性。有序性。 阅读详情

相关推荐

Java并发编程--volatile关键字

引言 很久之前就想写volatile关键字,但是迟迟没有动笔,主要是volatile涉及到的东西有点多,讲不清楚倒还好,就怕反而把读者绕晕了,哈哈。 volatile关键字是为了保证内存可见性的,首先介绍并发编程中涉及到的3个问题: 并发编程规则 原子性 原子具有不可分割性,所谓原子性,指的是一个或多个操作,要么全部执行且执行过程不会被任何因素打断,要么就全部执行,其与事务的语义是一致的。 比如经...

打酱油的葫芦娃 4931

pytroch中的Variable()介绍

个人理解 在pytorch中都是tensor进行计算的,神经网络在训练的过程中是不断更新参数的,也就需要反响传播,但是仅仅依靠tensor是不能达到反向传播的,这个时候,Variable变量,是一个可以变化的量,那么就可以反向传播了 requires_grad (指定该节点及依赖它的节点是否求导) variable默认是不需要被求导的,即requires_grad属性默认为False,如果某一个节点的requires_grad为True,那么所有依赖它的节点requires_grad都为True。 vol

xdg15294969271的博客 1564

HybridCLR热更新全流程实操:用Addressables实现资源+代码双热更(Unity 2021版)

本文详细介绍了在Unity 2021 LTS版本中,如何结合HybridCLR代码热更新与Addressables资源管理系统,实现资源与代码的双热更解决方案。通过环境配置、技术架构设计、实战案例和性能优化,帮助开发者解决中大型项目中的依赖管理和版本协同问题,提升游戏迭代效率。

cc789的博客 848

volatile变量的概念及用法

一、voltaile关键字 volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。 二、编译器优化的概念 int i = 5; int a = i; …… int b = i; 编译器发现两次从i读数据的代码之间,并没有对i...

踏着时光去旅行的博客 2568

避开这些坑!QMT多因子策略实盘中的5个常见错误及解决方案

本文深入剖析了在QMT平台上实施多因子策略时常见的5个错误,包括因子过拟合、数据延迟处理不当、因子失效、交易成本低估和风险控制缺失,并提供了实用的解决方案。通过优化因子筛选、数据对齐和动态管理,帮助量化交易者提升策略稳健性和实盘表现。

k5l6m的博客 238

在树莓派上部署训练好的YOLO模型

.pt.模型转换为onnx格式的模型,树莓派上部署模型

pai_da_xing8的博客 3887

volatile变量与普通变量的区别

我们通常会用volatile实现一些需要线程安全的代码(也有很多人不敢用,因为不了解),但事实上volatile本身并不是线程安全的,相对于synchoronized,它有更多的使用局限性,只能限制在某些特定的场景。本篇文章的目的就是让大家对 volatile 在本质上有个把握,为了达到这个目的,我们会从java 的内存模型及变量操作的内存管理来说明(不用怕,你会发现很简单)。 一、内存模型 可...

weixin_34138139的博客 630

SAP ABAP ALV表格实战:5分钟搞定单元格下拉框配置(附完整代码)

本文详细介绍了在SAP ABAP开发中如何快速配置ALV表格的单元格下拉框功能,通过5分钟实战教程帮助开发者掌握关键步骤。文章包含完整的ABAP代码示例,涵盖下拉值定义、字段目录配置及ALV控件绑定等核心操作,特别适合需要实现财务系统费用类型选择或采购系统物料分类等场景的开发者参考。

weixin_28331555的博客 112

【转】C/C++中的 mutable、volatile 关键字作用详解

mutable和volatile        很少遇到这两个关键字,学嵌入式估计知道后者,深入研究 C++ 的估计知道前者。      (1)mutable        在 C++ 中,mutable 是为了突破 const 的限制而设置的。被 mutable 修饰的变量,将永远处于可变的状态,即使在一个 const 函数中,甚至结构体变量或者类对象为 const

一程山水一程歌 4757

浅谈“无人机通感一体化(ISAC)”

总的来说,通信和感知一体化是一个循序渐进的过程,是未来一个比较有潜力的发展场景,当然,需要研究的内容有很多,面临的挑战也有很多,但是它值得我们去探索和学习!

m0_69182322的博客 4202

EMD (经验模态分解) 在 C++ 中的实现指南

EMD 是由 Norden E. Huang 等人在1998年提出的一种适用于非线性和非平稳信号的分析方法。对称性:IMF 的上下包络对称。零交叉点:IMF 的零交叉点数量与极值点数量相同或相差一个。本文详细介绍了如何在C++中实现EMD,包括理论背景、算法步骤、代码实现和优化等内容。通过丰富的代码示例和详细的解释,希望能够帮助读者掌握EMD的基本原理,并能够在实际项目中灵活应用。未来,随着信号处理技术的不断发展,EMD及其变种(如EEMD、CEEMDAN等)将继续在各个领域发挥重要作用。

qq_38334677的博客 1750

Java并发编程实战 Day 3:volatile关键字与内存可见性

Java内存模型(Java Memory Model, JMM)定义了Java程序中变量的访问规则,屏蔽了不同硬件平台和操作系统的差异,保证了Java程序在各种平台下对内存的访问效果一致。在JMM中,每个线程都有自己的本地内存(Local Memory),其中保存了主内存(Main Memory)中该线程使用的变量副本。线程读写变量时,默认情况下只能访问本地内存,这可能导致多个线程看到的变量值不一致。今天我们学习了Java中volatile关键字的使用方法及其背后的工作机制。

在未来等你的专栏 912

详细介绍用MATLAB实现基于A*算法的路径规划(附完整的代码,代码逐行进行解释)(一)--------A*算法简介和环境的创建

   本系列文章主要介绍基于A*算法的路径规划的实现,并使用MATLAB进行仿真演示。    一、 A*算法简介       A*(A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。算法中的距离估算值与实际值越接近,最终搜索速度越快。      公式表示为: f(n)=g(n)+h(n),其中, f(n) 是从初始状态经由状态n到目标状态的代价估计,g(n) 是在状态空间中从初始状态到状态n的实际代价,h(n) 是从状态n到目标状态的最佳路径的估计代价。

慕羽★的博客 7万+

Java并发编程第5讲——volatile关键字(万字详解)

本篇文章从volatile的定义谈起,由于volatile与Java内存模型有较多的关联,所以接着介绍Java内存模型的相关概念、线程、主内存和工作内存之间的关系以及内存间的交互规则;随后详细介绍了volatile和synchronized在Java并发编程的三大特性——原子性、可见性和有序性中的表现(重点介绍volatile)。最后介绍了一下“重排序”、“内存屏障”和“先行发生原则(happens-before)”等重要概念。

让~学习~成为一种习惯(橡 皮 人の技术博客) 5205

程序员应用AI大模型创业实现财务自由的100种赚钱方案原理与实战指南

成功的 AI 创业者都遵循着 “技术 + 需求 + 变现” 的铁三角法则。记住:不是要做 “全能大模型”,而是成为某个垂直领域的 “AI 解决方案专家”。现在就开始行动:用周末 2 小时完成第一个 MVP,下周启动种子用户测试,三个月内找到可复制的盈利模式。当你实现第一个 10 万用户时,会发现财务自由的密码藏在持续迭代的每个 0.1 版本更新中。延伸思考:你的技术背景适合切入哪个细分领域?

AI天才研究院 1381

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

【JUC并发编程 】 详解Java关键字之 volatile

Java的关键字 volatile 你了解多少呢,详解 volatile!

wanghuichen的博客 1536

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Java并发编程:volatile关键字解析

Java并发编程:volatile关键字解析    volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。   volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模

u011396148的博客 1753

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

java中volatile关键字保证可见性有序性

我们知道java并发编程主要会有可见性,原子性,有序性几个问题。 volatile可以保证可见性和有序性,但是无法保证原子性。 有序性:编译器会根据自认为的排序对代码进行指令重排,指令重排不会对单线程造成影响,但是可能会导致多线程时候运行错误。 可见性:可行性是指对线程的操作对其他线程可见。 volatile关键字可以禁止指令重排,所以可以保证了有序性。有序性也通常是指的happen-before原则。 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作 锁定规则

xby7437的博客 1万+

Java并发编程:JMM和volatile关键字

一、Java内存模型   随着计算机的CPU的飞速发展,CPU的运算能力已经远远超出了从主内存(运行内存)中读取的数据的能力,为了解决这个问题,CPU厂商设计出了CPU内置高速缓存区。高速缓存区的加入使得CPU在运算的过程中直接从高速缓存区读取数据,在一定程度上解决了性能的问题。但也引起了另外一个问题,在CPU多核的情况下,每个处理器都有自己的缓存区,数据如何保持一致性。为了保证多核处理器的数据一...

TuT 5434

volatile关键字——保证并发编程中的可见性、有序性

文章目录一.内存模型的相关概念二.并发编程中的三个概念1.原子性2.可见性3.有序性三.Java内存模型1.原子性2.可见性3.有序性四.深入剖析volatile关键字1.volatile关键字的两层语义2.volatile保证原子性吗?3.volatile能保证有序性吗?4.volatile的原理和实现机制五.使用volatile关键字的场景       &nbsp...

学习 1549

Java volatile关键字

作用及原理 注意事项

黑曼巴 7724

Java 并发编程——volatile 关键字解析

但是这样也带来了多线程中的读取“脏数据”的问题。举个简单例子:int a=1;int i=1;i += a;在上面的代码中如果两个线程执行i+=a操作,运行的结果可能不是我们期望的3。这是因为运行的过程中可能存在这样一种情况:初始时两个线程分别从主内存中读取到i=1缓存到自己的内存中,第一个线程执行完毕后,第二个线程中的缓存值i还是1,最后写入主内存的结果是2。

2501_90231022的博客 944
上一篇: Java并发编程实战2-线程安全
Peter_A
博客等级 码龄14年 5粉丝 12原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值