Java内存模型final的内存语义

目录
  • 1、final域的重排序规则final
  • 2、写final域的重排序规则
  • 3、读final与的重排序规则
  • 4、final域为引用类型
  • 5、为什么final引用不能从构造函数内“逸出”
  • 6、final语义在处理器中的实现
  • 7、JSR-133为什么要增强final的语义

上篇并发编程之Java内存模型volatile的内存语义介绍了volatile的内存语义,本文讲述的是final的内存语义,相比之下,final域的读和写更像是普通变量的访问。

1、final域的重排序规则final

对于final域编译器和处理器遵循两个重排序规则

  • 在构造函数内对一个final域的写入,与随后把这个对象的引用赋值给另一个引用变量,这两个操作之间不能重排序
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

用代码来说明上面两种重排序规则:

package com.lizba.p1;

/**
 * <p>
 *
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/11 20:37
 */
public class FinalExample {

    /** 普通变量 */
    int i;
    /** final变量 */
    final int j;
    /** 对象引用 */
    static FinalExample obj;

    /**
     * 构造函数
     */
    public FinalExample() {
        // 写普通域
        this.i = 1;
        // 写final域
        this.j = 2;
    }

    /**
     * 线程A执行writer写方法
     *
     */
    public static void writer() {
        obj = new FinalExample();
    }

    /**
     * 线程B执行reader读方法
     *
     */
    public static void reader() {
        // 读对象的引用
        FinalExample finalExample = obj;
        // 读普通域
        int a = finalExample.i;
        // 读final域
        int b = finalExample.j;
    }
}

假设线程A执行writer()方法,线程B执行reader()方法。下面来通过这两个线程的交互来说明这两个规则。

2、写final域的重排序规则

写final域的重排序禁止吧final域的写重排序到构造函数之外。通过如下方式来实现:

  • JMM禁止编译器把final域的写重排序到构造函数之外
  • 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

现在开始分析writer()方法:

 /**
   * 线程A执行writer写方法
   *
   */
public static void writer() {
    obj = new FinalExample();
}
  • 构造一个FinalExample类型的对象
  • 将对象的引用赋值给变量obj

首先假设线程B读对象引用与读对象的成员域之间没有重排序,则下图是其一种执行可能

 线程执行时序图:

3、读final与的重排序规则

读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意是处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

解释:初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。

  • 编译器遵守间接依赖关系,编译器不会重排序这两个操作
  • 大多数处理器也遵守间接依赖,不会重排序这两个操作。但是少部分处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门针对这种处理器的。

分析reader()方法:

  /**
    * 线程B执行reader读方法
    *
    */
public static void reader() {
    // 读对象的引用
    FinalExample finalExample = obj;
    // 读普通域
    int a = finalExample.i;
    // 读final域
    int b = finalExample.j;
}
  • 初次读引用变量obj
  • 初次读引用变量obj指向对象的普通域j
  • 初次读引用变量obj指向对象的final域i

假设B线程所处的处理器不遵守间接依赖关系,且A线程执行过程中没有发生任何重排序,此时存在如下的执行时序:

线程执行时序图:

上图B线程中读对象的普通域被重排序到处理器读取对象引用之前, 此时普通域i还没有被线程A写入,因此这是一个错误的读取操作。但是final域的读取会被重排序规则把读final域的操作“限定”在读该final域所属对象的引用读取之后,此时final域已经被正确的初始化了,这是一个正确的读取操作。

总结:

读final域的重排序规则可以确保,在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。

4、final域为引用类型

上面讲述了基础数据类型,如果final域修饰的引用类型又该如何?

package com.lizba.p1;

/**
 * <p>
 *      final 修饰引用类型变量
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/11 21:52
 */
public class FinalReferenceExample {

    /** final是引用类型 */
    final int[] intArray;
    static FinalReferenceExample obj;

    /**
     * 构造函数
     */
    public FinalReferenceExample() {
        this.intArray = new int[1];  // 1
        intArray[0] = 1;             // 2
    }

    /**
     * 写线程A执行
     */
    public static void writer1() {
        obj = new FinalReferenceExample();      // 3
    }

    /**
     * 写线程B执行
     */
    public static void writer2() {
        obj.intArray[0] = 2;                    // 4
    }

    /**
     * 读线程C执行
     */
    public static void reader() {
        if (obj != null) {                      // 5
            int temp = obj.intArray[0];         // 6
        }
    }
}

如上final域为一个int类型的数组的引用变量。对应引用类型,写final域的重排序对编译器和处理器增加了如下约束:

  • 在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给另一个引用变量,这两个操作不能重排序。

对于上述程序,假设A执行writer1()方法,执行完后线程B执行writer2()方法,执行完后线程C执行reader()方法。则存在如下线

程执行时序:引用型final的执行时序图

JMM对于上述代码,可以确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入。即写线程C至少能看到数组下标0的值为1。但是写线程B对数组元素的写入,读线程C可能看得到可能看不到。JMM不能保证线程B的写入对读线程C可见。因为写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。

此时如果想确保读线程C看到写线程B对数组元素的写入,可以结合同步原语(volatile或者lock)来实现。

5、为什么final引用不能从构造函数内“逸出”

本文一直在说写final域的重排序规则可以确保:在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化了。那究竟是如何实现的呢?

其实这需要另一个条件:在构造函数内部,不能让这个被构造对象的引用被其它线程所见。也就是对象引用不能在构造函数中“逸出”。

示例代码:

package com.lizba.p1;

/**
 * <p>
 *   final引用逸出demo
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/11 22:33
 */
public class FinalReferenceEscapeExample {

    final int i;
    static FinalReferenceEscapeExample obj;

    public FinalReferenceEscapeExample() {
        i = 1;                            // 1、写final域
        obj = this;              // 2、this引用在此处"逸出"
    }

    public static void writer() {
        new FinalReferenceEscapeExample();
    }

    public static void reader() {
        if (obj != null) {                 // 3
            int temp = obj.i;              // 4
        }
    }
}

假设线程A执行writer()方法,线程B执行reader()方法。这里操作2导致对象还未完成构造前就对线程B可见了。因为1和2允许重排序,所以线程B可能无法看到final域被正确初始化后的值。实际执行的时序图可能如下所示:

多线程执行时序图:

总结:

在构造函数返回之前,被构造对象的引用不能为其他线程可见,因为此时的final域可能还没被初始化。而在构造函数返回后,任意线程都将保证能看到final域正确初始化之后的值。

6、final语义在处理器中的实现

举例X86处理器中final语义的具体实现。

在编译器中会存在如下的处理:

  • 写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore屏障
  • 读final域的重排序规则要求编译器在读final域的操作前插入一个LoadLoad屏障

但是,由于X86处理器不会对写-写操作做重排序,所以在X86处理器中,写final域需要的StoreStore屏障会被省略。同样,由于X86处理器不会对存在间接依赖关系的操作做重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。因此,在X86处理器中,final域的读/写不会插入任何内存屏障。

7、JSR-133为什么要增强final的语义

在旧的Java内存模型中,一个最严重的缺陷就是现场可能看到final域的值会改变。比如一个线程读取一个被final域的值为0(未初始化之前的默认值),过一段时间再读取初始化后的final域的值,却发现变为了1。因此为了修复此漏洞,JSR-133增强了final语义。

总结:

通过为final增加写和读重排序规则,可以为Java程序员提供初始化安全保障:只要对象正确构造(被构造对象额引用在构造函数中没有“逸出”),那么不需要使用同步原语(volatile和lock的使用)就可以保障任意线程都能看到这个final域在构造函数中被初始化之后的值。

到此这篇关于Java内存模型final的内存语义的文章就介绍到这了,更多相关Java内存模型final的内存语义内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java实现LRU缓存淘汰算法的方法

    LRU算法:最近最少使用淘汰算法(Least Recently Used).LRU是淘汰最长时间没有被使用的缓存(即使该缓存被访问的次数最多). 如何实现LRU缓存淘汰算法 场景: 我们现在有这么个真实场景,我在爬取某个网站时,控制该网站的代理IP并发数,太多会搞垮对方网站的对吧,要蹲号子的呢.这里我需要维护一个代理IP代理池,而且这些IP肯定不是一直都很稳定的,但是又不能取一个就丢一个,这样太浪费资源.所以我会将这些IP缓存起来,进行按需提取,采用LRU最近最少使用的策略去管理代理IP. 代码

  • Java 实现完整功能的学生管理系统实例

    目录 一.前言 二.学生管理系统主要功能 在学生类下 在学生总部类下: main方法: 登录密码验证: 打印菜单: 增加学生信息: 查看学生信息: 删除学生信息: 修改学生信息: 查找学生信息: 代码效果演示图: 一.前言 我们前面写了通讯录管理系统,现在我们来写个学生管理系统, 其实主干代码都一 不过,在学生管理系统中我添加和优化了许多功能, 如[登录密码验证,异常处理,非空判断,,防止重复添加]等. 二.学生管理系统主要功能 增加学生信息 删除学生信息 修改学生信息 查找学生信息 查看所有学

  • 利用Java实现mTLS调用

    目录 本文将使用 Java作为客户端 与受 mTLS 保护的服务交互. 为了对我们的 Java 客户端进行 ssl 配置,我们需要先设置一个 SSLContext.这简化了事情,因为 SSLContext 可用于各种 http 客户端. 由于我们有客户端公钥和私钥,我们需要将私钥从 PEM 格式转换为 DER. openssl pkcs8 -topk8 -inform PEM -outform PEM -in /path/to/generated/client.key -out /path/to

  • Java十大经典排序算法图解

    目录 0.算法概述 0.1 算法分类 0.2 算法复杂度 0.3 相关概念 1.冒泡排序(Bubble Sort) 1.1 算法描述 1.2 动图演示 1.3 代码实现 2.选择排序(Selection Sort) 2.1 算法描述 2.2 动图演示 2.3 代码实现 2.4 算法分析 3.插入排序(Insertion Sort) 3.1 算法描述 3.2 动图演示 3.3代码实现 3.4 算法分析 4.希尔排序(Shell Sort) 4.1 算法描述 4.2 动图演示 4.3 代码实现 4.

  • Java ThreadLocal的详细解释

    目录 一.ThreadLocal简介 二.ThreadLocal简单使用 三.ThreadLocal的实现原理 1.set方法源码 2.get方法源码 3.remove方法的实现 4.如下图所示: 四.ThreadLocal不支持继承性 五.InheritableThreadLocal类 六.从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题 1.基础概念 2.分析ThreadLocalMap内部实现 总结: 一.ThreadLocal简介 多线程访问同一个共享变量的时

  • 并发编程之Java内存模型volatile的内存语义

    1.volatile的特性 理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对单个读/写操作做了同步. 代码示例: package com.lizba.p1; /** * <p> * volatile示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 21:34 */ public class VolatileFeatureExample { /** 使用volatile声明64位的long型

  • 并发编程之Java内存模型锁的内存语义

    目录 1.锁的释放-获取建立的happens-before关系 2.锁释放和获取的内存语义 3.锁内存的语义实现 4.concurrent包的实现 简介: 锁的作用是让临界区互斥执行.本文阐述所得另一个重要知识点--锁的内存语义. 1.锁的释放-获取建立的happens-before关系 锁是Java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 锁释放-获取的示例代码: package com.lizba.p1; /** * <p>

  • Java内存模型final的内存语义

    目录 1.final域的重排序规则final 2.写final域的重排序规则 3.读final与的重排序规则 4.final域为引用类型 5.为什么final引用不能从构造函数内"逸出" 6.final语义在处理器中的实现 7.JSR-133为什么要增强final的语义 上篇并发编程之Java内存模型volatile的内存语义介绍了volatile的内存语义,本文讲述的是final的内存语义,相比之下,final域的读和写更像是普通变量的访问. 1.final域的重排序规则final

  • Java内存模型知识汇总

    为什么要有内存模型 在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情.要说计算机的内存模型,就要说一下一段古老的历史,看一下为什么要有内存模型. 内存模型,英文名Memory Model,他是一个很老的老古董了.他是与计算机硬件有关的一个概念.那么我先给你介绍下他和硬件到底有啥关系. CPU和缓存一致性 我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道.而计算机

  • Java内存模型的深入讲解

    目录 内存模型 硬件架构 Java内存模型与硬件关联 对象的可见性 竞争条件 总结 Java内存模型展示了Java虚拟机是如何与计算机内存交互的,解决多线程读写共享内存时资源访问的问题. 内存模型 Java虚拟机中的内存模型将线程栈与堆划分开,下图描述了Java内存模型的逻辑图. 每个线程都要自己的线程栈,栈中存储着线程执行到当前位置所调用的方法信息,线程执行代码时,线程栈会不断执行入栈和出栈操作. 线程栈中会存储所有被调用的方法中定义的变量,并且自己访问自己栈中的变量,别的线程不可见.即使两个

  • 从内存模型中了解Java final的全部细节

    目录 从内存模型中了解final JMM 重排序 final域重排序规则 final对象是引用类型

  • Java内存模型JMM详解

    Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性.是否可以重排序等问题的无关具体平台的统一的保证.(可能在术语上与Java运行时内存分布有歧义,后者指堆.方法区.线程栈等内存区域). 并发编程有多种风格,除了CSP(通信顺序进程).Actor等模型外,大家最熟悉的应该是基于线程和锁的共享内存模型了.在多线程编程中,需要注意三类并发问题: ·原子性 ·可见性 ·重排序 原子性涉及到,一个线程执行一个复合操作的时候,其他线程是否能够看

  • 浅谈Java并发中的内存模型

    什么是JavaMemoryModel(JMM)? JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Java开发者无需关注不同平台之间的差异,达到一次编译,随处运行的目的,这也正是Java的设计目的之一. CPU和内存 在讲JMM之前,我想先和大家聊聊硬件层面的东西.大家应该都知道执行运算操作的CPU本身是不具备存储能力的,它只负责根据指令对传递进来的数据做相应的运算,而数据存储这一任务则交给内存去完成.虽然内存的运行速度虽然比起硬盘快非常多,但是和3GHZ,4GH

  • Java内存模型(JMM)及happens-before原理

    我们知道java程序是运行在JVM中的,而JVM就是构建在内存上的虚拟机,那么内存模型JMM是做什么用的呢? 我们考虑一个简单的赋值问题: int a=100; JMM考虑的就是什么情况下读取变量a的线程可以看到值为100.看起来这是一个很简单的问题,赋值之后不就可以读到值了吗? 但是上面的只是我们源码的编写顺序,当把源码编译之后,在编译器中生成的指令的顺序跟源码的顺序并不是完全一致的.处理器可能采用乱序或者并行的方式来执行指令(在JVM中只要程序的最终执行结果和在严格串行环境中执行结果一致,这

  • Java内存模型知识详解

    1. 概述 多任务和高并发是衡量一台计算机处理器的能力重要指标之一.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标比较能说明问题,它代表着一秒内服务器平均能响应的请求数,而TPS值与程序的并发能力有着非常密切的关系.在讨论Java内存模型和线程之前,先简单介绍一下硬件的效率与一致性. 2.硬件的效率与一致性 由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理

随机推荐