Volatile和高速缓存的关系

news2024/9/29 15:30:06

“volatile关键字有什么用?”

1 常见理解错误

  • 把volatile当成一种锁机制,认为给变量加上了volatile,就好像是给函数加sychronized,不同的线程对于特定变量的访问会去加锁
  • 把volatile当成一种原子化的操作机制,认为加了volatile之后,对于一个变量的自增的操作就会变成原子性
// 一种错误的理解,是把volatile关键词,当成是一个锁,可以把long/double这样的数的操作自动加锁
private volatile long synchronizedValue = 0;

// 另一种错误的理解,是把volatile关键词,当成可以让整数自增的操作也变成原子性的
private volatile int atomicInt = 0;
amoticInt++;

很多工程师容易把volatile关键字,当成和锁或者数据数据原子性相关的知识点。volatile最核心要关系JMM。

JMM是JVM这个进程级虚拟机里的一个内存模型,但该内存模型和计算机组成里的CPU、高速缓存和主内存组合在一起的硬件体系类似。理解JMM,可更容易理解计算机组成里CPU、高速缓存和主内存之间的关系。

2 “隐身”的变量

dzone.com代码段,后续修改这段代码来进行各种小实验。

2.1 demo1

public class VolatileTest {
    private static volatile int COUNTER = 0;

    public static void main(String[] args) {
        new ChangeListener().start();
        new ChangeMaker().start();
    }

    static class ChangeListener extends Thread {
        @Override
        public void run() {
            int threadValue = COUNTER;
            while ( threadValue < 5){
                if( threadValue!= COUNTER){
                    System.out.println("Got Change for COUNTER : " + COUNTER + "");
                    threadValue= COUNTER;
                }
            }
        }
    }

    static class ChangeMaker extends Thread{
        @Override
        public void run() {
            int threadValue = COUNTER;
            while (COUNTER <5){
                System.out.println("Incrementing COUNTER to : " + (threadValue+1) + "");
                COUNTER = ++threadValue;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
    }
}

先定义了一个volatile的int类型的变量,COUNTER。

然后,分别启动两个独立线程:

  • ChangeListener
    先取到COUNTER当前值,然后一直监听该COUNTER值。一旦COUNTER值变化,就把新值打印。直到COUNTER的值达到5。这监听过程,通过while死循环的忙等待实现
  • ChangeMaker
    取到COUNTER的值,在COUNTER小于5的时候,每隔500毫秒,就让COUNTER自增1。在自增前,通过println方法把自增后的值打印

输出结果并不让人意外。ChangeMaker函数会一次一次将COUNTER从0增加到5。因为这个自增是每500毫秒一次,而ChangeListener去监听COUNTER是忙等待的,所以每一次自增都会被ChangeListener监听到,然后对应的结果就会被打印出来。

Incrementing COUNTER to : 1
Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Got Change for COUNTER : 5

2.2 demo2

把上面的程序小小地修改一行代码,把定义COUNTER变量时的volatile去掉,会咋样?

private static int COUNTER = 0;

ChangeMaker还是能正常工作,每隔500ms仍然能够对COUNTER自增1。但ChangeListener不再工作。在ChangeListener眼里,它似乎一直觉得COUNTER的值还是一开始的0。似乎COUNTER的变化对ChangeListener彻底“隐身”。

Incrementing COUNTER to : 1
Incrementing COUNTER to : 2
Incrementing COUNTER to : 3
Incrementing COUNTER to : 4
Incrementing COUNTER to : 5

2.3 demo3

不再让ChangeListener进行完全的忙等待,而是在while循环里小等5ms

static class ChangeListener extends Thread {
    @Override
    public void run() {
        int threadValue = COUNTER;
        while ( threadValue < 5){
            if( threadValue!= COUNTER){
                System.out.println("Sleep 5ms, Got Change for COUNTER : " + COUNTER + "");
                threadValue= COUNTER;
            }
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

虽然COUNTER变量仍没设置volatile这个关键字,但ChangeListener似乎“睡醒了”。在通过Thread.sleep(5)在每个循环里“睡“5ms后,ChangeListener又能够正常取到COUNTER的值了。

Incrementing COUNTER to : 1
Sleep 5ms, Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Sleep 5ms, Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Sleep 5ms, Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Sleep 5ms, Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Sleep 5ms, Got Change for COUNTER : 5

这些现象就来自于 JMM 及关键字volatile的含义。volatile究竟代表什么?

它确保我们对该变量的读取和写入,一定同步到主内存,而非从Cache读取。

3 如何理解这句话?

3.1 有volatile

因所有数据的读、写都来自主内存。自然ChangeMaker和ChangeListener之间,看到的COUNTER值一样。

3.2 无volatile

这时,ChangeListener又是一个忙等待的循环,它尝试不停获取COUNTER值,这样就会从当前线程的“Cache”获取。于是,这线程就没有时间从主内存同步更新后的COUNTER值。这样,它就一直卡死在COUNTER=0的死循环。

3.3 虽无volatile,但短短5ms的Thead.Sleep给了这线程喘息之机

既然这个线程没有这么忙了,它就有机会把最新数据从主内存同步到自己的高速缓存。于是,ChangeListener在下一次查看COUNTER值的时候,就能看到ChangeMaker变化。

虽然JMM是个隔离了硬件实现的虚拟机内的抽象模型,但它给出“缓存同步”问题示例。若数据在不同线程或CPU核里更新,因不同线程或CPU核有各自缓存,很可能在A线程的更新,B线程看不见。

4 CPU高速缓存的写入

可将Java内存模型和计算机组成里的CPU结构对照。

Intel CPU多核。每个CPU核里都有独属的L1、L2 Cache,再有多个CPU核共用的L3 Cache、主内存。

因为CPU Cache访问速度>>主内存,而CPU Cache里,L1/L2 Cache也比L3 Cache快。所以,CPU始终尽可能从CPU Cache获取数据,而非每次都从主内存读数据:

这层级结构就像在JMM里,每个线程都有属于自己的线程栈。线程读取COUNTER时,其实是从本地的线程栈的Cache副本读,而非从主内存读。若对数据仅只是读,问题还好。Cache Line组成及如何从内存里把对应数据加载到Cache。

但不光要读,还要去写入修改数据。问题就来了:写入Cache的性能也比写主内存快,那写数据,到底写到Cache还是主内存?若直接写主内存,Cache里的数据是否会失效?

先看两种

5 写入策略

5.1 写直达(Write-Through)


最简单的写策略,每次数据都写主内存。
写入前,先判断数据是否已在Cache:

  • 已在Cache
    先把数据写入更新到Cache,再写主内存
  • 数据不在Cache
    只更新主内存

实现简单,但性能很慢。无论数据是否在Cache,都要把数据写主内存。这有点像volatile关键字,始终都要把数据同步到主内存。

5.2 写回(Write-Back)


既然读数据也默认从Cache加载,能否不用把所有写入都同步到主内存?只写入CPU Cache是不是就够?可以!这就是写回(Write-Back)策略,不再是每次都把数据写主内存,而只写到CPU Cache。只有当CPU Cache里的数据要被“替换”,才把数据写主内存。

过程

若发现要写入的数据,就在CPU Cache,就只更新CPU Cache的数据。同时标记CPU Cache里的这个Block是脏(Dirty)的:指此时CPU Cache里的这个Block的数据,和主内存不一致。

如发现要写入的数据所对应的Cache Block里,放的是别的内存地址的数据,就要看那个Cache Block里的数据是否被标记成脏:

  • 如果是脏,先把这个Cache Block里面的数据,写入主内存。再把当前要写入的数据,写入Cache,同时把Cache Block标记成脏
  • 如果Block里面的数据没有被标记成脏的,直接把数据写入Cache,然后再把Cache Block标记成脏

用写回策略后,在加载内存数据到Cache时,也要多出一步同步脏Cache的动作。若加载内存数据到Cache时,发现Cache Block里有脏标记,也要先把Cache Block里的数据写回主内存,才能加载数据覆盖Cache。

该策略里,若大量操作都能命中缓存,则大部分时间里,无需读写主内存,性能比写直达效果好太多!

但无论是写回or写直达,都没解决volatile程序问题:多个线程或多个CPU核的缓存一致性问题。
这也就是在写入修改缓存后,需要解决的第二个问题

要解决这个问题,需引入MESI协议,维护缓存一致性的协议。不仅可用在CPU Cache之间,也可广泛用于各种需要使用缓存,同时缓存之间需要同步的场景下。

总结

volatile程序可以看到,在有缓存的情况下会遇到一致性问题。volatile这个关键字可以保障我们对于数据的读写都会到达主内存。

Java内存模型和CPU、CPU Cache以及主内存的组织结构非常相似。在CPU Cache里,对于数据的写入,我们也有写直达和写回这两种解决方案。写直达把所有的数据都直接写入到主内存里面,简单直观,但是性能就会受限于内存的访问速度。而写回则通常只更新缓存,只有在需要把缓存里面的脏数据交换出去的时候,才把数据同步到主内存里。在缓存经常会命中的情况下,性能更好。

但是,除了采用读写都直接访问主内存的办法之外,如何解决缓存一致性问题?
下文分解。

参考

  • Fixing Java Memory Model
  • 《计算机组成与设计:硬件/软件接口》5.3.3

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

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

相关文章

Dubbo 3 Dubbo 快速入门 3.1 Zookeeper 安装

Dubbo 【黑马程序员Dubbo快速入门&#xff0c;Java分布式框架dubbo教程】 3 Dubbo 快速入门 文章目录Dubbo3 Dubbo 快速入门3.1 Zookeeper 安装3.1.1 Zookeeper 安装3.1 Zookeeper 安装 3.1.1 Zookeeper 安装 在Dubbo 架构图中 Dubbo官方推荐使用Zookeeper作为注册中心【Re…

【学习总结】注解和元注解

目录 一、注解 1、注解与XML区别 2、注解的用途 3、注解的三种分类 二、什么是元注解&#xff1f; 1、元注解有几种&#xff1f; 1、Retention存活时间 2、Target使用范围 3、Document保存到javadoc 4、Inherited注解继承 三、如何实现的注解 四、问提&#xff1a; …

为解决BERT模型对语料中低频词的不敏感性

来源&#xff1a;投稿 作者&#xff1a;COLDR 编辑&#xff1a;学姐 &#xff08;内容如有错漏&#xff0c;可在评论区指出&#xff09; 摘要 Dict-BERT为了解决BERT模型对语料中低频词&#xff08;rare words&#xff09;的不敏感性&#xff0c;通过在预训练中加入低频词词典…

人工智能/计算机期刊会议测评(持续更新...更新速度取决于我水论文的速度...)

IEEE Transactions on Knowledge and Data Engineering 中科院2区&#xff0c;CCF A。你为什么是二区&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;做梦都想中的刊。 …

5天带你读完《Effective Java》(二)

《Effective Java》是Java开发领域无可争议的经典之作&#xff0c;连Java之父James Gosling都说&#xff1a; “如果说我需要一本Java编程的书&#xff0c;那就是它了”。它为Java程序员提供了90个富有价值的编程准则&#xff0c;适合对Java开发有一定经验想要继续深入的程序员…

Servlet 原来是这个玩意、看完恍然大悟

1. 什么是 Servlet&#xff1f; 先让时间回到 25 年前&#xff0c;我国刚刚接入互联网不到两年时间。那时候的电脑长这个样子&#xff1a; 当时的网页技术还不是很发达&#xff0c;大家打开浏览器只能浏览一些静态的页面&#xff0c;例如图片、文本信息等。 随着时间的发展&a…

[附源码]Python计算机毕业设计Django社区生活废品回收APP

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

idea配置tomcat日志中文乱码,且修改后idea正常,但cmd窗口任然中文乱码解决方法

idea日志乱码问题的原因是tomcat的日志配置文件有两行有问题需要删掉&#xff0c;cmd乱码是Windows系统cmd窗口默认不是utf-8 首先解决idea中tomcat的日志乱码问题&#xff0c;在idea中进行如下的配置 Trans...........可以不勾选&#xff0c;它的作用是用选定的字符集把项目的…

[附源码]Python计算机毕业设计Django室内设计类网站

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

2023面试专题:JAVA基础

ArrayList和LinkedList有哪些区别 ArrayList扩容机制: ArrayList() 会使用长度为零的数组ArrayList(int initialCapacity) 会使用指定容量的数组public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量add(Object o) 首次扩容为 10&#xff0c;再次…

【OpenCV】透视变换应用——实现鸟瞰图与贴图

透视变换是3D转换&#xff0c;透视变换的本质是将图像投影到一个新的视平面&#xff1b; 据此&#xff0c;我们可以使用透视变化来实现鸟瞰图和图形贴图的效果&#xff1b; 一、鸟瞰图 实现前&#xff1a; 实现效果&#xff1a; 1.准备一个空的mat对象 用于保存转换后的图 M…

asp.net mvc+elementUI 实现增删改查

最开始心想着一直都是前端玩这些玩意&#xff0c;个人虽然不是纯前端。好歹做为一个.net全栈开发多年&#xff0c;我就不太想用node去搭建&#xff0c;那么试试吧&#xff0c;总归不是那么几个css和js的文件引用&#xff0c;如果对vue.js不太熟悉&#xff0c;最好先去看看。 那…

智能家居创意DIY之智能触摸面板开关

触摸开关&#xff0c;即通过触摸方式控制的墙壁开关&#xff0c;其感官场景如同我们的触屏手机&#xff0c;只需手指轻轻一点即可达到控制电器的目的&#xff0c;随着人们生活品质的提高&#xff0c;触摸开关将逐渐将换代传统机械按键开关。 触摸开关控制原理 触摸开关我们把…

springboot入门案例

今天写一个springboot入门案例&#xff0c;接下来我将带大家走进springboot第一课的案例。如果有问题&#xff0c;望大家指正。 目录 1. 简介 2. 开发示例 2.1 创建springboot工程 3. 启动类 4. 常用注解 5. springboot配置文件 6. 开发一个controller 1. 简介 Spring …

大一学生WEB前端静态网页——旅游网页设计与实现(15页面)

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…

Git GitHub入门

目录Git1. 安装Git1. 下载Git2. 安装Git2. Git常用命令1. 设置用户签名2. 初始化本体库3. 查看本地库状态4. 添加暂存区5. 提交本地库6. 查看历史记录7. 修改文件8. 版本穿梭3. Git分支操作1. 查看分支2. 创建分支3. 切换分支4. 合并分支GitHub操作1. 创建远程仓库2. 远程仓库操…

【sciter】历经一个月封装的Web组件库使用说明文档

Web 组件库 1、组件库总体架构 2、目录结构及组件库引入 使用时,只需要引入 CSS 样式文件及组件入口文件即可。 3、组件注意事项 1、当组件支持绑定事件时,事件名称命名规则为 Sc + DOM事件名称且首字母大写,即点击事件(click)对应(ScClick) 在 sciter 中,通过原生 D…

0127 排序 Day16

剑指 Offer 45. 把数组排成最小的数 输入一个非负整数数组&#xff0c;把数组里所有数字拼接起来排成一个数&#xff0c;打印能拼接出的所有数字中最小的一个。 示例 1: 输入: [10,2] 输出: "102" 示例 2: 输入: [3,30,34,5,9] 输出: "3033459" class…

浅谈 Flink 窗口

本次只记录最近对于窗口的新认知 关于窗口的详细知识可以参考如下链接: https://blog.csdn.net/mynameisgt/article/details/124223193 窗口的作用是为了在无限流上进行统计计算,数据到来时,则为此条数据开辟窗口。当 Flink 的时间大于等于窗口的结束时间时,触发这个窗口…

线程池的设计与原理解析(四)之---runWorker()方法

在调用start()方法&#xff0c;调用的就是worker的run方法&#xff0c;实际上调用的是runWorker()方法 public void run() {// 这个是核心方法&#xff0c;worker启动后的逻辑从这里进入runWorker(this);}简单的梳理runWorker的流程 如果构造worker的时候&#xff0c;指定了fi…