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


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

相关文章
|
1月前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
22天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
51 12
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
1月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
1月前
|
小程序 前端开发 算法
|
1月前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
66 2
|
28天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
226 1
|
17天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
26天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80