当前位置: 首页 > 后端技术 > Java

Java内存模型

时间:2023-04-01 17:35:15 Java

前言在并发编程中,当多个线程同时访问同一个共享变量变量时,会产生不确定的结果,所以编写线程安全的代码本质上就是对共享变量的访问操作进行管理。造成这种不确定结果的原因是可见性、顺序和原子性的问题。Java引入Java内存模型来解决可见性和顺序性问题,并使用互斥方案(其核心实现技术是锁)来解决原子性问题。本文首先看一下解决可见性和顺序问题的Java内存模型(JMM)。什么是Java内存模型?Java内存模型在维基百科上是这样定义的:Java内存模型描述了Java编程语言中的线程是如何通过内存进行交互的。与单线程执行代码的描述一起,内存模型提供了Java编程语言的语义。内存模型限制共享变量,即存储在堆内存中的变量。在Java语言中,所有的实例变量、静态变量和数组元素都存储在堆内存中。方法参数、异常处理参数等局部变量保存在方法栈帧中,因此不会在线程间共享,不会受到内存模型的影响,不会有内存可见性问题。通常,线程之间有两种通信方式:共享内存和消息传递。很明显,Java采用了第一种共享内存模型。在共享内存模型中,程序的公共状态在多个线程之间共享。,通过读写记忆进行隐式交流。从抽象的角度来看,JMM实际上定义了线程和主存的关系。首先,多个线程之间的共享变量存储在主存中,每个线程都有自己私有的本地内存,线程的读或写共享变量的副本存储在本地内存中(注意:本地内存是JMM定义的一个抽象概念,实际上并不存在)。抽象模型如下图所示:在这个抽象内存模型中,当两个线程之间进行通信时(共享变量状态发生变化),会执行以下两个步骤:线程A复制本地内存中更新的共享变量的值是刷新到主内存。线程B在使用共享变量时,会去主存中读取线程A更新的共享变量的值,更新线程B的本地内存的值。JMM本质上是在硬件(处理器)内存模型之上创建了另一层抽象,这样应用程序开发人员只需要了解JMM就可以编写正确的并发代码,而不必过多了解硬件级别的内存模型。为什么需要Java内存模型?在日常的程序开发中,经常会遇到给一些共享变量赋值的场景。假设一个线程分配了一个整型共享变量count(count=9527;),这时候就会出现问题。,其他读取共享变量的线程在什么情况下得到变量值9527呢?在没有同步的情况下,有许多因素会阻止读取变量的其他线程立即或永远不会看到变量的最新值。例如,缓存可能会改变共享变量副本写入主存的顺序,本地缓存中存储的值对其他线程是不可见的;为了优化性能,编译器有时会改变程序中语句的执行顺序,这些因素可能导致其他线程无法看到共享变量的最新值。文章开头提到JMM主要是解决可见性和顺序问题,那么首先要弄清楚造成可见性和顺序问题的本质原因是什么?目前的大部分服务都运行在多核CPU的服务器上。每个CPU都有自己的缓存。这时候CPU缓存和内存中的数据就会出现一致性问题。当一个线程修改共享变量时,另一个线程不能立即看到。可见性问题的根本原因是缓存。顺序是指代码的实际执行顺序与代码定义的顺序一致。编译器为了优化性能,会遵守as-if-serial语义(无论怎么重新排序,单线程下的执行结果都不会改变),但是有时候编译器和解释器的优化也会带来一些问题。例如:仔细检查以创建单例对象。下面是使用双重检查实现单例对象惰性创建的代码:publicstaticDoubleCheckedInstancegetInstance(){if(instance==null){synchronized(DoubleCheckedInstance.class){if(instance==null){instance=newDoubleCheckedInstance();}}}返回实例;}}这里的instance=newDoubleCheckedInstance();,看起来Java代码只有一行,应该是无法重新排序的。实际上,编译后的实际指令是以下三个步骤:分配对象的内存空间,初始化对象,设置实例指向刚刚分配的内存地址,以及上面的步骤2和步骤3ifChanging执行顺序不会改变单线程的执行结果,也就是说可能会出现重新排序的情况。下图是一个多线程并发执行的场景:此时线程B获取到的实例还没有初始化。如果这样访问instance的成员变量可能会触发空指针异常。排序问题的本质原因是编译器优化。那么你可能会想,既然缓存和编译器优化是导致可见性和顺序问题的原因,那么直接禁用它们不就可以彻底解决这些问题了吗?但如果这样做,程序的性能可能会降低。会受到很大的影响。其实我们可以换个思路。我们能否将禁用缓存和编译器优化的权利交给编码工程师?他们必须最清楚什么时候禁用它,这样我们只需要提供禁用缓存和按需编译优化的方法即可。即使用起来更加灵活。因此,Java内存模型诞生了。它规定了JVM如何提供禁用缓存和按需编译优化的方法,并规定JVM必须遵守一组最低限度的保证。这个最低保证规定了线程何时写入共享变量。对其他线程可见。顺序一致性内存模型顺序一致性模型是一种理想化的理论参考模型,处理器和编程语言的内存模型设计是对顺序一致性模型理论的参考。它具有以下两个特点:一个线程中的所有操作都必须按照程序的顺序执行。无论程序是否同步,所有线程只能看到一个执行顺序。工程师视角的顺序一致性模型如下:顺序一致性模型有一个单一的全局内存,可以通过左右摆动连接到任何线程。每个线程必须按照程序的顺序进行内存读写操作。在这种理想模型下,任何时候只能有一个线程连接到内存。当多个线程并发执行时,可以通过switch将多个线程的读写操作序列化。在顺序一致性模型中,所有的操作都是按顺序串行执行的,但是在JMM中没有这样的保证。不仅JMM中非同步程序的执行顺序是乱序的,而且由于本地内存的存在,所有线程看到的操作顺序也可能不一致。例如,一个线程将写入的共享变量保存在本地内存中。在它刷新到主存之前,其他线程是不可见的。只有更新到主存后,其他线程才有可能看到。JMM为正确同步的程序做出顺序一致性保证,即程序的执行结果与程序在顺序一致的内存模型中的执行结果相同。Happens-Before规则Happens-Before规则是JMM中的核心概念。Happens-Before概念在本文中首次被提出,它使用Happens-Before来定义分布式系统之间的偏序关系。Happens-Before在JSR-133中用于指定两个操作之间的执行顺序。JMM正是通过这个规则来确保跨线程内存可见性。Happens-Before的含义是前一个操作共享变量的结果对于后面对该变量的操作是可见的,这约束了编译器的优化行为。虽然允许编译,但是优化后的代码必须满足Happens-Before规则,这就保证了工程师:同步多线程程序按照Happens-Before规定的顺序执行。目的是在不改变程序(单线程或正确同步的多线程程序)的执行结果的情况下,尽可能提高程序执行的效率。JSR-133规范中定义了以下6条Happens-Before规则:程序顺序规则:线程中的每一个操作,Happens-Before线程中的任何后续操作监控锁规则:解锁一个锁,Happens-Before之后是此锁的锁定操作。volatile规则写入一个volatile类型的变量。Happens-Before以及此volatile变量的任何后续读取操作。传递规则:如果操作AHappens-BeforeB,且操作BHappens-Before操作C,则操作AHappens-Before操作Cstart()规则:如果线程A执行操作threadB.start()以启动线程B,然后线程A的start()操作发生-在线程B的任何操作join()之前JMM从threadB.join()操作的一个基本原则是:只要单线程和正确同步的多线程的执行结果不变,编译器和处理器想怎么优化就怎么优化。事实上,应用程序开发人员并不关心这两个操作是否真的重新排序,真正关心的是执行结果无法修改。因此,Happens-Before和sa-if-serial的语义本质上是一样的,只是sa-if-serial只是保证单线程下的执行结果不会改变。小结本文主要介绍内存模型的基础知识和相关概念。JMM屏蔽了不同处理器内存模型之间的差异,为不同处理器平台上的应用开发者抽象出统一的Java内存模型(JMM)。.常见的处理器内存模型比JMM弱,因此JVM在生成字节码指令时会在适当的位置插入内存屏障(内存屏障的类型因处理器平台而异)以限制部分重排序。