6-JMM

news2024/11/28 6:40:34

目录

1.主内存与工作内存

2.内存间交互操作

Java内存模型的三大特性:

happens-before原则(先行发生原则):

3.volatile型变量的特殊规则

①保证此变量对所有线程的可见性

②使用volatile变量的语义是禁止指令重排序


JVM定义了一种Java内存模型JMM(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台下的内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。

1.主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后两者是线程私有的,不会被线程共享。

Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要通过主内存来完成。

线程、主内存、工作内存三者的交互关系如下所示 :

2.内存间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了如下8种操作来完成。JVM实现时必须保证下面提及的每一种操作的原子的、不可再分的。

  1. lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  2. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
  7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的write操作使用。
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

Java内存模型的三大特性:

  1. 原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和 read。大致可以认为,基本数据类型的访问读写是具备原子性的。如若需要更大范围的原子性, 需要synchronized关键字约束。(即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行)
  2. 可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。 volatile、synchronized、final三个关键字可以实现可见性。
  3. 有序性:如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的操作都是无序的。前半句是指"线程内表现为串行",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象。

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

3.volatile型变量的特殊规则

关键字volatile可以说是JVM提供的最轻量级的同步机制,但是它并不容易完全被正确理解和使用。JVM 内存模型对volatile专门定义了一些特殊的访问规则。 当一个变量定义为volatile之后,它将具备两种特性。

①保证此变量对所有线程的可见性

这里的"可见性"是指:

当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量做不到这一点,普通变量的值在线程间传递均需要通过主内存来完成。

例如:线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程 A 回写完成之后再从主内存进行读取操作,新值才会对线程B可见。

关于volatile变量的可见性,经常会被开发人员误解。volatile变量在各个线程中是一致的,但是 volatile 变量的运算在并发下一样是不安全的。原因在于Java里面的运算并非原子操作。

范例:volatile变量自增操作

package com.company;

public class Main {
    public static volatile int num = 0;

    public static void increase() {
        num++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[10];

        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        increase();
                   }
                }
            });
            threads[i].start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
 
        System.out.println(num);
    }
}

问题就在于num++之中,实际上num++等同于num = num+1。volatile关键字保证了num的值在取值时是正确的,但是在执行num+1的时候,其他线程可能已经把num值增大了,这样在+1后会把较小的数值同步回主内存之中。

由于volatile关键字只保证可见性,在不符合以下两条规则的运算场景中,仍然需要通过加锁 (synchronized或者lock)来保证原子性。

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束。

如下代码这类场景就特别适合使用volatile来控制并发,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来。

volatile boolean shutdownRequested;

public void shutdown() {
    shutdownRequested = true;
}

public void work() {
    while(!shutdownRequested) {
        //do stuff
    }
}

②使用volatile变量的语义是禁止指令重排序

普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序和程序代码中执行的顺序一致。 volatile关键字禁止指令重排序有两层意思:

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

举个简单的例子:

//x、y为非volatile变量
//flag为volatile变量
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2 前面,也不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

范例:指令重排序

Map configOptions;
char[] configText;
volatile boolean initialized = false;

//假设以下代码在线程A执行
//模拟读取配置文件信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized = true;

//假设以下代码在线程B执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized) {
    sleep();
}
//使用线程A初始化好的配置信息
doSomethingWithConfig();

单例模式中的Double Check:

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。

为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public static Singleton getSingleton() {
    if(instance==null) { //Single Checked
        synchronized (Singleton.class) {
            if(instance==null) { //Double Checked
                instance=new Singleton();
            } 
        }
    }
    return instance;
}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存。
  2. 调用 Singleton 的构造函数来初始化成员变量。
  3. 将instance对象指向分配的内存空间。(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化,也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之 前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

只需要将 instance 变量声明成 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;
    }
}

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

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

相关文章

node.js+vue+express企业客户关系管理系统mysql

开发语言 node.js 框架&#xff1a;Express 前端:Vue.js 数据库&#xff1a;mysql 数据库工具&#xff1a;Navicat 开发软件&#xff1a;VScode 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身…

21.模糊神经网络预测水质评价(附matlab程序)

1.简述 学习目标&#xff1a;模糊神经网络预测水质评价——重风水厂、 高升水厂、秦玺水厂 采用模糊神经网络预测这三个水厂的水质&#xff0c;并进行对比 模糊神经网络(Fuzzy Neural Network, FNN) 背景 系统复杂度的增加&#xff0c;人工智能深度化发展 模糊数学创始人L. A…

io.netty学习(四)ChannelHandler

目录 前言 正文 ChannelHandler ChannelInboundHandler ChannelOutboundHandler ChannelDuplexHandler 总结 前言 先简略了解一下ChannelPipeline和ChannelHandler的概念。 想象一个流水线车间。当组件从流水线头部进入&#xff0c;穿越流水线&#xff0c;流水线上的工…

第四章 组合逻辑电路--数电(期末复习笔记)

第四章 组合逻辑电路 本章重点&#xff1a; 1. 组合逻辑电路的分析与设计方法 2. 常用组合逻辑模块的使用 4.1 概述 4.11 组合逻辑电路 任一时刻的输出仅取决于该时刻的输入&#xff0c;与电路原来的状态无关。4.12 时序逻辑电路 任一时刻的输出不仅取决于现时的输入&am…

电容的基本工作原理

目录 电容器的发展历程现象发现第一个存储电荷的元器件&#xff1a;莱顿瓶真正出名的时刻 为什么电容器的容量单位称为法拉&#xff1f;电容器和电容的区别电容的组成电介质与电解质对电容的影响电容是如何工作的通交流阻直流阻直流通交流 电容器的单位电容的容抗电容常见的种类…

Idea Mybatis插件:提高CRUD效率

mybatis-sql-viewer插件主要提供能力&#xff1a;将mybatis xml转成真实SQL语句、参数mock、SQL规范检查、SQL索引检查、SQL运行、SQL压测及Mybatis SQL语句扫描。 1. 简介 虽然写了很久的CRUD&#xff0c;但是依旧觉得写好CRUD是一件非常难且麻烦的事情&#xff0c;以下的情…

chatgpt赋能Python-python找出不同部分

介绍 在SEO&#xff08;Search Engine Optimization&#xff09;领域&#xff0c;比较常见的问题之一是如何快速有效地查找出两段文本的不同部分。这对于优化网站内容或对比竞争对手的网站内容都非常有用。Python作为一种强大的编程语言&#xff0c;其特性和库使得这种任务变得…

神经网络入门①多层感知器如何解决异或问题?

文章目录 1. 多层感知器2. BP算法参考文献 1. 多层感知器 感知机&#xff08;perceptron&#xff09;早在20世纪50年代就提出来了1&#xff0c;但直到近几年深度学习的崛起&#xff0c;神经网络才再次走入大众的视野&#xff0c;并且成为了当下最热门的研究方向之一。 一个单层…

MybatisPlus的save方法

当我前端传递的是一个整合的数据模型Dto,需要同时插入俩张表,并且其中一张表的属性需要得到另一张表的id,如何实现呢?这个时候MP封装好的插入方法:save就起了很好的作用 public R<String> SaveNewDish(DishDto dishdto) { // 1. 保存菜品基本信息log.info("前…

MySQL优化--覆盖索引,超大分页查询

目录 覆盖索引 MYSQL超大分页处理 面试回答 大纲 回答 覆盖索引 覆盖索引是指查询使用了索引&#xff0c;并且需要返回的列&#xff0c;在该索引中已经全部能够找到 。 MYSQL超大分页处理 在数据量比较大时&#xff0c;如果进行limit分页查询&#xff0c;在查询时&#…

一步一步教你如何使用MMSelfSup框架【1】

介绍 任务介绍 自监督学习(Self-supervised learning, SSL)是一种极具潜力的学习范式&#xff0c;它旨在使用海量的无标注数据来进行表征学习。在SSL中&#xff0c;我们通过构造合理的预训练任务&#xff08;可自动生成标注&#xff0c;即自监督&#xff09;来进行模型的训练…

C#探索之路(8):初探.Net中官方文档OpCode的格式

C#探索之路(8)&#xff1a;初探.Net中官方文档OpCode的格式 文章目录 C#探索之路(8)&#xff1a;初探.Net中官方文档OpCode的格式1 前提2 疑惑1&#xff1a;Opcode格式是什么&#xff1f;3 答疑1&#xff1a;4 验证方式&#xff1a;5 总结 1 前提 最近在尝试了解学习这个OpCod…

使用ChatGPT最新版实现批量写作,打造丰富多彩的聚合文章

随着人工智能的迅猛发展&#xff0c;ChatGPT最新版作为一种自然语言处理模型&#xff0c;可以为我们提供强大的文本生成能力。在这篇文章中&#xff0c;我们将探讨如何利用ChatGPT最新版来实现批量写作&#xff0c;从而打造丰富多彩的聚合文章。 一、ChatGPT最新版简介 Chat…

使用mpi并行技术实现快排Qsort()

快排基本原理&#xff1a; 快速排序可以说是最为常见的排序算法&#xff0c;冒泡排序时间复杂度达到了O&#xff08;N2&#xff09;&#xff0c;而桶排序容易造成浪费空间。快排&#xff08;Quicksort&#xff09;就成为了不错的选择。 1、原理&#xff1a;快排需要找一个数作…

基于 JavaScript 中的 Date 类型实现指定日期和天数的加减运算

文章目录 Intro基本方法构造方法--如何初始化一个Date对象&#xff1f;Date 对象的天数加减法 【工具方法封装】最后 Intro 刚才突然想到&#xff0c;我还有多久就要过27岁的生日了呢。 年近三十&#xff0c;一事无成。 以下基于 JavaScript 中的 Date 对象封装一些方法。 基…

CAS、AtomicInteger和LongAdder原理

目录 一、CAS 1、介绍 2、CAS与volatile 3、为什么无锁效率高 4、总结 二、原子整数 三、原子引用 1、介绍 2、ABA问题 3、AtomicStampedReference 4、AtomicStampedReference 四、原子累加器 1、介绍 2、LongAdder重要关键域 CAS锁 原理之伪共享 3、LongAdde…

Java性能权威指南-总结15

Java性能权威指南-总结15 堆内存最佳实践对象生命周期管理弱引用、软引用与其他引用 小结 堆内存最佳实践 对象生命周期管理 弱引用、软引用与其他引用 在Java中&#xff0c;弱引用和软引用也支持对象重用&#xff0c;不过作为开发者&#xff0c;并不会经常从重用的角度看待…

【C语言进阶】文件操作

目录 &#x1f945;什么是文件&#xff1a; &#x1f3d1;程序文件&#xff1a;&#x1f3d1;数据文件&#xff1a; &#x1f3d1;文件名&#xff1a; &#x1f945;文件的打开和关闭&#xff1a;&#x1f3d1;文件指针&#xff1a; &#x1f3d1;fopen和fclose&#xff1a; &a…

基于matlab使用先导校准来补偿阵列不确定性(附源码)

一、前言 此示例说明如何使用先导校准来提高天线阵列在存在未知扰动时的性能。 原则上&#xff0c;可以轻松设计理想的均匀线性阵列&#xff08;ULA&#xff09;来执行阵列处理任务&#xff0c;例如波束成形或到达方向估计。在实践中&#xff0c;没有理想的阵列。例如&#xff…

io.netty学习(二)Netty 架构设计

目录 前言 Selector 模型 SelectableChannel Channel 注册到 Selector SelectionKey 遍历 SelectionKey 事件驱动 Channel 回调 Future 事件及处理器 责任链模式 责任链模式的优缺点 ChannelPipeline 将事件传递给下一个处理器 总结 前言 上一篇文章&#xff…