Java内存模型—工作流程、volatile原理

简介: 最近在做项目的时候发现很多业务上用到了多线程,通过多线程去提升程序的一个运行效率,借此机会来复盘一下关于并发编程的相关内容。为什么要使用volatile?volatile底层原理是什么?JMM内存模型解决的是什么问题?带着这些问题来分享分享我的成果。

导入


最近在做项目的时候发现很多业务上用到了多线程,通过多线程去提升程序的一个运行效率,借此机会来复盘一下关于并发编程的相关内容。为什么要使用volatile?volatile底层原理是什么?JMM内存模型解决的是什么问题?带着这些问题来分享分享我的成果。


正文


JMM内存模型是什么?


根据百度百科介绍:


Java Memory Model,java内存模型,描述了程序中各个共享变量(成员变量、静态变量、数据元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节。


注意:局部变量不存在线程之间共享,它属于方法内定义的参数,不受内存模型影响

为什么要有JMM内存模型?要解决什么问题?


在多线程通信的情况下,如何保持读取一致是重中之重,解决存储在主内存的数据和CPU中工作内存的数据不一致问题,解决编译器对代码进行指令重排序导致执行数据不一致的问题,这些都是JMM去帮助我们去完成的。


那JMM具体是怎么工作的呢?且看我接下来的分享


JMM工作流程—抽象


下面是JMM的抽象结构示意图:


7b0768343c906b96bd548d7cf9c5d210.png


JMM去决定了一个线程对共享变量的写入何时对另一个线程可见。线程之间的共享变量都存在主内存中,而每一个线程包含了一个工作内存(本地),在本地内存中存储了共享变量的副本。线程之间工作内存中的变量是不能相互访问的,必须通过主内存获取


抽象工作流程如下:


1.若线程1修改了本地内存中的共享变量,将共享变量最新结果刷新到主内存中

2.线程2到主内存中读取线程1修改之后共享变量


实战演练—加锁+volatile前


我们结合程序来研究研究:


自定义线程类


class MyThread extends Thread {
    private boolean flag = false;
    public boolean isFlag() {
        return flag;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag=" + flag);
    }
}


main函数


public class SolveVolatile {
    public static void main(String[] args) {
        MyThread a = new MyThread();
        a.start();
        for (; ; ) {
            if (a.isFlag()) {
                System.out.println("进来了吗?");
            }
        }
    }
}


运行程序:


87b4ade090d26b452d35bd31e5fc24d5.png


发现,一直都不输出“进来了吗?”并且程序一直是处于运行状态的,结合上面讲到的JMM模型,其实是数据可见性问题。


JMM工作流程—具体


上面讲到了JMM工作流程,我们来结合这个程序具体来看看它是怎么工作的!


f4f6ab9ae4206b6a4b474ce2d8bc8677.png


  1. read(读取):从主内存读取数据
  2. load(载入):将主内存读取到的数据写入工作内存
  3. use(使用)从工作内存读取数据来计算
  4. assign(赋值):将计算好的值重新赋值到工作内存中
  5. store(存储):将工作内存数据写入主内存
  6. write(写入:将store过去的变量值赋值给主内存中的变量


我们会发现线程2对flag变量的值修改了之后线程1其实是并不知道的,导致程序一直都不会输出“进来了吗?”这句话,线程1 的工作内存中其实还一直保存着共享变量原来的值。


那如何解决这个问题呢?给变量添加volatile关键字修饰、同步代码块加锁


volatile修饰共享变量、加锁


线程类


class MyThread extends Thread {
    private volatile boolean flag = false;
    public boolean isFlag() {
        return flag;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag=" + flag);
    }
}


Main函数


public class SolveVolatile {
    public static void main(String[] args) {
        MyThread a = new MyThread();
        a.start();
        for (; ; ) {
            synchronized (a) {
                if (a.isFlag()) {
                    System.out.println("进来了吗?");
                }
            }
        }
    }
}


此时输出结果为:


3e277f03bb82364254214d757bf6fff3.png


为什么加锁和volatile可以解决数据可见性问题?


此时JMM内部工作结构就变成了这样:


lock(锁定):将主内存变量加锁,标识为线程独占状态


unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量


07801fa1751162c5a85386edf2462191.png


当某个线程进入到sysnchronized代码块,线程获得锁之后会清空本地的工作内存,重新从主内存中读取共享变量的副本到工作内存中,此时线程在执行代码判断的时候发现共享变量值被修改了。


volatile的底层原理是什么?


结合前面讲到的JMM工作流程,当线程对共享变量的副本数据进行了修改之后,会立马写回到主内存中,此时各个线程中工作内存的共享变量副本就失效了,需要重新去主内存中读取。


那其他线程怎么知道某个线程修改了共享变量呢?我们可不可以设置一个监听的人,只要有线程改变了值我就去主内存中读取?那就要讲讲MESI了!


MESI(缓存一致性协议)—硬件方式


8f63da357dba4745bcaad2f4248fbdb0.png


数据都是通过总线以流的形式传输,线程2将flag值改变之后,下一步应该是写入主存中,会经过总线,这时候线程1通过总线嗅探机制监听到flag值的改变,线程1去主存中读取flag的值,读到的值还是false,此时线程2还没有回写到主存中,此时就产生了偏差所以在数据要往主存中回写的时候store之前就加上锁,在主内存中write回写完了再释放锁。


CPU通过总线嗅探机制可以感知到数据变化从而自己缓存里的数据失效重新读取


总结


通过加锁和volatile我们可以解决多核cpu并发线程出现数据不一致、可见性问题,正式因为线程之间通信对我们完全的透明,所以在项目中会出现内存可见性的问题,追根溯源去了解原理,在开发过程中除了知道怎么用,还能知道为什么这么用!


如果有想要交流的内容欢迎在评论区进行留言,如果这篇文档受到了您的喜欢那就留下你点赞+收藏+评论脚印支持一下博主~

相关文章
|
9天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
9天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
11天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
9天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
30 6
|
13天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
35 2
|
14天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
36 1
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
370 0
|
22天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
46 1
|
27天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
1月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。