Java多线程-带你认识Java内存模型,内存分区,从原理剖析Volatile关键字

作者:那个人

地址:https://juejin.im/post/59f8231a5188252946503294

声明:本文是那个人原创,已获其授权发布,未经原作者允许请勿转载

写在前面

读完本篇文章你将知道:

  • Java的内存模型。
  • Java的内存分区。
  • 全局变量、局部变量、对象、实例再内存中的位置。
  • JVM重排序机制。
  • JVM的原子性、可见性、有序性。
  • 彻底了解Volatile关键字。

Java的内存模型

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。想要掌握Java并非线程JMM一定要了解。Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。这里涉及到共享内存区域的知识,稍后会在Java的内存分区中介绍到。简单来说JMM解释了这么一个问题:当多个线程再访问同一个变量的时候,其中一个线程改变了该变量的值但是并未写入主存中,那么其他线程就会读取到旧值,无法获取到最新的值。好了接下来看看什么是内存模型:

Java内存模型定义了线程和主存(可以理解为java内存分区中的共享区域,稍后将介绍)之间的抽象关系:线程之间的共享变量存贮在主存中,每个线程都会拥有属于自己的私有工作内存(这个内存分配再栈里面),再工作内存中只会存储该线程使用到的共享变量的副本。这里的私有工作内存其实是一个抽象的概念,它包括了缓存、写缓冲区、寄存器等区域。Java内存模型控制线程间的通信,它决定一个线程对主存共享变量的写入何时对另一个线程可见。这是Java内存模型抽象图:

从图中我们能分析出:

1.每个线程再执行的时候都会有自己的工作内存,其中包括了方法里面所包含的所有变量等。

2.每个线程的私有工作内存是不能相互访问的,这也就解释了为什么我们不能再一个方法中访问另一个方法的局部变量。

3.当线程想要访问共享变量的时候,需要从主存中获取,再自己的方法区中只是保存的变量的副本。

4.当我们修改完共享变量的时候,需要把改过的变量写入主存中,这样才能让其他线程获取到正确的值。

简单一点就是:

(1)线程A把线程A本地内存中更新过的共享变量刷新到贮存中去。

(2)线程B到主存中去读取线程A之前已更新过的共享变量的的值。

也就是:

int i= 1;

这一天我不钓虾,东西也少吃。母亲很为难,没有法子想。到晚饭时候,外祖母也终于觉察了。

也就是说,这句代码被线程执行的时候是这样的情景:执行线程先把变量i的值的一个副本,存放到自己的工作内存中,然后再把值写入主存中,而不是直接写入到主存中。

这样是不是就可以说明用一个普通的变量作为标记去打断线程是不严谨的,大家可以移步到我的上一篇文章如何正确的打断线程。

Java的内存分区

一般来说,Java程序在运行时会涉及到以下内存区域:

寄存器:

JVM内部虚拟寄存器,存取速度非常快,程序不可控制。

Java虚拟机栈(通俗就是我们常说的“栈”):

它是线程私有的,它的生命周期与线程相同。每个方法被执行的时候都会同时创建一个栈帧(StackFrame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。它存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型),它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

堆:

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。 Java堆是垃圾收集器管理的主要区域。

方法区:

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来,但它还是属于堆里面的。

常量池(其实是方法区的一部分):

JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用(1)。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于堆中.

需要注意的一些:

  1. 对象所拥有的方法以及里面涉及到的变量都存储在栈里面,方法里面使用到的全局变量是随着对象实例一起存储在堆里面,在方法中使用的时候也是使用该全局变量的副本.
  2. 对于一个对象的成员变量,不管他是原始类型还是包装类型,都会被存贮在堆区.
  3. 方法区和堆是一样,是各个线程共享的区域,里面存放java虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据.
  4. 当调用一个对象的方法时会在java(虚拟机栈)栈里面创建属于自己的栈空间,方法走完即被释放
  5. 分清什么是实例什么是对象。Class a = new Class();此时a叫实例,而不能说a是对象。实例在栈中,对象在堆中,操作实例实际上是通过实例的指针间接操作对象。多个实例可以指向同一个对象。

那么我们通过代码来进一步的认识每个分区:

public class Persion{

privite String name = “Wang”;

privite static String love = “eat”;

public void init(int age){if(age < 0){

age = 0;

}

Log.e(TAG,"Name is "+ name+"Age is "+ age);

}

}

首先我们知道 当我用 Persion p = new Perison()的时候,Persion p 这个引用存贮再栈里面,new Perison()这个对象保存在堆里面,包括name成员变量都在堆里面;love这个静态变量存贮在常量池里面。当我们调用 p.init(10) 的时候,会在该线程所在的栈里面开创该线程私有的栈内存,用来保存age变量和name共享变量的副本。这里要说一下,堆、方法区被称为共享区域,这里面的数据才能被多线程所共享。

JVM重排序机制

在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。拿上面的例子来说:假如不是a=1的操作,而是a=new byte[1024*1024],那么它会运行地很慢,此时CPU是等待其执行结束呢,还是先执行下面那句flag=true呢?显然,先执行flag=true可以提前使用CPU,加快整体效率,当然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。我们看下简单的例子:

public void execute(){

int a=0;

int b=1;

int c=a+b;

}

这里a=0,b=1两句可以随便排序,不影响程序逻辑结果。所以程序再运行的时候会选择先运行int b = 1 ;然后再运行 int a=0;但是我们是无法观察到的,这确是可能发生的,这句c=a+b这句必须在前两句的后面执行,所以在他的前后不会出现重排序。这里我们就简单的了解下就可以啦.

JVM的原子性、可见性、有序性

  • 原子性

定义:对基本类型变量的读取和赋值操作是原子性操作,即这些操作是不可中断的,要么执行完毕,要么就不执行。

x =3;    //语句1

y =4    //语句2

z = x+y ;//语句3

x++;    //语句4

这里面的操作只有语句1和语句2是原子性的操作,语句3,4不是原子性的操作;因为在语句3中包括了三个操作,1是先读取x的值,2读取y的值,3将z的值写入内存中。语句4的解释是一样的。一般的一个语句含有多个操作该语句就不是原子性的操作,只有简单的读取和赋值才是原子性的操作。

  • 可见性

就是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改结果,另一个线程马上就能看到。

  • 有序性

Java内存模型允许编译器和处理器对指令进行重排序,虽然重排序不会影响到单线程的正确性,但是会影响到多线程的正确性。

Volatile关键字

这里呢Volatile的三个条件:

1.不保证原子性。

2.保证有序性。

3.保证可见性。

当用volatile修饰共享变量的时候,线程访问到该变量的时候都回去主存中去取该变量的值,它的工作内存中的缓存将失效,这样就保证了每个线程访问该变量的时候都是从主存中读写的。这就是为什么使用Volatile关键字来修饰线程间共享变量。

结束语

这些也是对JVM的一些小的探索,希望能给大家带来一点小的帮助,如果喜欢的话请点个赞再走吧,感兴趣的话就点 这里 这个关注吧,之后我会继续给大家带来一下新的见解,或者把通俗易懂的语言来描述苦涩难懂的原理~

原文发布于微信公众号 - Android先生(cyg_24kshign)

原文发表时间:2017-11-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏技术记录

Protobuf3语法详解

8875
来自专栏Java技术分享圈

杨老师课堂之JavaSe 部分面试题

​ JVM 是 JavaVirtual Machine 的缩写,全称是 Java 虚拟机。Java 语言的一个非常重要的 特性就是跨平台性,而 Java 虚...

873
来自专栏无题

堆外内存概要

DirectByteBuffer JDK中使用 DirectByteBuffer对象来表示堆外内存,每个 DirectByteBuffer对象在初始化时,都会创...

2414
来自专栏蓝天

RPC的实现

RPC全称为Remote Procedure Call,即远过程调用。如果没有RPC,那么跨机器间的进程通讯通常得采用消息,这会降低开发效率,也会增加网络...

1783
来自专栏WindCoder

JVM-Java内存区域

JVM在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,都有着各自的用途以及创建和销毁时间。包括以下几个如图所示的运行时数据区域:

1541
来自专栏有趣的django

7.python常用模块

time模块 常用表示时间方式: 时间戳,格式化的时间字符串,元组(struct_time) UTC(Coordinated Universal Time,世界...

45411
来自专栏Java 技术分享

Struts2 转换器

1192
来自专栏武培轩的专栏

Runtime源码解析(JDK1.8)

package java.lang; import sun.reflect.CallerSensitive; import sun.reflect.Refle...

3579
来自专栏皮皮之路

【JVM】浅谈双亲委派和破坏双亲委派

笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书,阅读完后自以为对jvm有了一定的了解,然而当真正碰到问题的时候,才发现自己读的有多粗糙,也体会到只有实践...

1922
来自专栏李航的专栏

Shell 主要逻辑源码级分析:SHELL 运行流程 (1)

分享一下在学校的时候分析shell源码的一些收获,帮助大家了解shell的一个工作流程,从软件设计的角度,看看shell这样一个历史悠久的软件的一些设计优点和缺...

2.1K0

扫码关注云+社区