求你了,再问你 Java 内存模型的时候别再给我讲堆栈方法区了

标签: dev | 发表时间:2019-07-05 00:00 | 作者:
出处:http://itindex.net/relian

最近,面试过很多Java中高级开发,问过很多次关于Java内存模型的知识,问完之后,很多人上来就开始回答:

Java内存模型由几部分组成,堆、本地方法栈、虚拟机栈、方法区…

每一次我不想打断他们的话,虽然我知道这又是一个误会了我的问题的朋友。

其实,我想问的Java内存模型,是和并发编程有关的。而候选人给我回答的那叫JVM内存结构,完全是两回事。

很多时候,在我没有打断他们的情况下,一部分人慢慢的讲到了GC相关的知识。这种情况下,我只能硬着头皮继续问一些和JVM有关的知识。

但是,我的本意其实是想看一下他对Java并发有多少了解啊。

经常,我都在继续追问了一些他们回答的"Java内存模型"相关的知识后,友善的提醒一句,其实我想问的Java内存模型并不是他回答的这个…

有的时候,我会进一步提醒一句:是和并发编程有关的,是和主内存以及线程工作内存有关的。。。

那么,本文就来简单说一说,关于Java内存模型,到底应该如何回答这个面试题。

1

为什么会误解

首先,我们先来分析一下问什么很多人,甚至是大多数人会答非所问呢?

我觉得主要有几个原因:

1、Java内存模型,这个词听着太像是关于内存分布的知识了。听上去和并发编程没有半毛钱关系。

2、网上很多资料都是错的。不信你去网上搜索一下"Java内存模型",你会发现,很多人打着内存模型的标题,介绍了JVM内存结构的知识。

这里提一句,我尝试着Google搜索了一下搜索"Java内存模型",首页展示结果如下:

首页排名靠前的5篇文章中,有1篇是错的,介绍了JVM内存结构。

值得庆幸的的是,首页前5篇文章中,有两篇是我写的,至少我的这两篇我敢确定是不具备任何误导性的!!

3、还存在一种情况,虽然不多见,但是也有。那就是很多面试官自己也以为内存模型就是要介绍堆、栈、方法区这些知识。就导致有时候面试者不知道自己到底应该如何回答。

那么,到底什么是Java内存模型?关于这道面试题应该如何回答呢?


2

什么是内存模型

我曾经在《 再有人问你Java内存模型是什么,就把这篇文章发给他》中详细的介绍过Java内存模型的来龙去脉,这里再重新回顾一下。

Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。

Java内存模型的相关知识在 JSR-133: Java Memory Model and Thread Specification 中描述的。JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。

那么,我们这里就先来说说什么是所谓的内存模型规范、这里提到的原子性、可见性以及有序性又是什么东西?

原子性

线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。这种情况下,读改写就不是一个原子操作。即存在原子性问题。

缓存一致性

在多核CPU,多线程的场景中,每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

有序性

除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是有序性问题。

多CPU多级缓存导致的一致性问题、CPU时间片机制导致的原子性问题、以及处理器优化和指令重排导致的有序性问题等,都硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢?

最简单直接的做法就是废除处理器和处理器的优化技术、废除CPU缓存,让CPU直接和主存交互。但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点因噎废食了。

所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是—— 内存模型

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

针对上面的这些问题,不同的操作系统都有不同的解决方案,而Java语言为了屏蔽掉底层的差异,定义了一套属于Java语言的内存模型规范,即Java内存模型。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。


3

Java内存模型的实现

了解Java多线程的朋友都知道,在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。

在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。

本文并不准备把所有的关键字逐一介绍其用法,因为关于各个关键字的用法,网上有很多资料。读者可以自行学习。本文还有一个重点要介绍的就是,我们前面提到,并发编程要解决原子性、有序性和一致性的问题,我们就再来看下,在Java中,分别使用什么方式来保证。

原子性

在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized。

因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

可见性

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

除了volatile,Java中的synchronized和final两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。

有序性

在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性。实现方式有所区别:

volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。

但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。


4

面试如何回答

前面我介绍完了一些和Java内存模型有关的基础知识,只是基础,并不是全部,因为随便一个知识点还是都可以展开的,如volatile是如何实现可见性的?synchronized是如何实现有序性的?

但是,当面试官问你:能简单介绍下你理解的内存模型吗?

首先,先和面试官确认一下:您说的内存模型指的是JMM,也就是和并发编程有关的那一个吧?

在得到肯定答复后,再开始介绍(如果不是,那可能就要回答堆、栈、方法区哪些了….囧…):

Java内存模型,其实是保证了Java程序在各种平台下对内存的访问都能够得到一致效果的机制及规范。目的是解决由于多线程通过共享内存进行通信时,存在的原子性、可见性(缓存一致性)以及有序性问题。

除此之外,Java内存模型还提供了一系列原语,封装了底层实现后,供开发者直接使用。如我们常用的一些关键字:synchronized、volatile以及并发包等。

回答到这里就可以了,然后面试官可能会继续追问,然后根据他的追问再继续往下回答即可。

所以,当有人再问你Java内存模型的时候,不要一张嘴就直接回答堆栈、方法区甚至GC了,那样显得很不专业!


有道无术,术可成;有术无道,止于术

欢迎大家关注 Java之道公众号



好文章,我在看❤️

相关 [java 内存 模型] 推荐:

深入Java内存模型

- - ImportNew
你可以在网上找到一大堆资料让你了解JMM是什么东西,但大多在你看完后仍然会有很多疑问. happen-before是怎么工作的呢. 用volatile会导致缓存的丢弃吗. 为什么我们从一开始就需要内存模型. 通过这篇文章,读者可以学习到足以回答以上所有问题的知识. 它包含两大部分:第一部分是硬件层次的大体架构,第二部分是深入OpenJdk源代码和实现.

Java 内存模型 JMM

- - 码蜂笔记
JMM,Java Memory Model,Java 内存模型. 什么是内存模型,要他何用. 假定一个线程为变量var赋值: var = 3;,内存模型要回答的问题是:在什么条件下,读取变量var的线程可以看到 3这个值. 如果缺少了同步,线程可能无法看到其他线程操作的结果. 导致这种情况的原因可以有:编译器生成指令的次序可以不同于源代码的“显然”版本,编译器还会把变量存储在寄存器而不是内存中;处理器可以乱序或并行执行指令;缓存会改变写入提交到主存得到变量的次序;存储在处理器本地缓存中的变量对其他处理器不可见 等等.

Java 多线程内存模型

- - ITeye博客
Java 多线程内存模型.       Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果. 在此之前,主流程序怨言(如C/C++等)直接使用物理硬件(或者说操作系统的内存模型),因此,会由于不同的平台上内存模型差异,导致程序在一套平台上并发完成正常,而在另一套平台上并发访问却经常出错,因此经常需要针对不同的平台来编写程序.

Java运行时的内存模型

- - CSDN博客编程语言推荐文章
每个线程单独的数据区(线程间不共享). 每个线程都有一片单独的内存区域,这里面包含:程序计数器(program counter register),JVM栈和本地方法栈(Native Method Stack). 当一个新的线程被创建的时候,这片内存就已经被分配出来了. 程序计数器:为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储.

Java内存模型修订了!

- - 互联网 - ITeye博客
传统的Java内存模型涵盖了很多Java语言的语义保证. 在这篇文章中,我们将重点介绍其中的几个语义,以更深入地了解他们. 对于本文中描述的语义,我们还将尝试体会对现有Java内存模型更新的动机. 本文中与JMM未来更新相关的讨论,将被称为JMM9. 现有的Java内存模型,如JSR133(以下称为JMM-JSR133)中所定义的,为共享内存指定了一致性模型,并且有助于为开发者提供与JMM-JSR133表述一致的定义.

文章: 深入理解Java内存模型(五)——锁

- - InfoQ cn
锁的释放-获取建立的happens before 关系. 锁是java并发编程中最重要的同步机制. 锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 深度剖析WebKit渲染机制:Chromium项目Committer确认QCon北京2013. QCon北京自动化运维专题:腾讯海量SNS社区网站高效运维探索.

JAVA内存释放

- - Java - 编程语言 - ITeye博客
(问题一:什么叫垃圾回收机制. ) 垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能. 当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露. (问题二:java的垃圾回收有什么特点. ) JAVA语言不允许程序员直接控制内存空间的使用.

Java 堆内存(Heap)

- - ITeye博客
        堆(Heap)又被称为:优先队列(Priority Queue),是计算机科学中一类特殊的数据结构的统称. 堆通常是一个可以被看做一棵树的数组对象. 在队列中,调度程序反复提取队列中第一个作业并运行,因而实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权.

java内存泄漏

- - 编程语言 - ITeye博客
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址. Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的. GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题.

求你了,再问你 Java 内存模型的时候别再给我讲堆栈方法区了

- - IT瘾-dev
最近,面试过很多Java中高级开发,问过很多次关于Java内存模型的知识,问完之后,很多人上来就开始回答:. Java内存模型由几部分组成,堆、本地方法栈、虚拟机栈、方法区…. 每一次我不想打断他们的话,虽然我知道这又是一个误会了我的问题的朋友. 其实,我想问的Java内存模型,是和并发编程有关的.