【Java并发】浅谈Java内存区域与内存模型

目录

理解Java内存区域与Java内存模型

Java内存区域

栈、堆、方法区

举例说明:

Java内存模型概述

JMM存在的必要性

原子性

理解指令重排

可见性

JMM提供的解决方案

JMM中的happens-before 原则

有序性

 


理解Java内存区域与Java内存模型

Java内存区域

栈、堆、方法区

 

JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)

  • 堆区:

    1. 提供所有类实例和数组对象存储区域

    2. jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

  • 栈区:

    1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中

    2. 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。

  • 方法区:

    1. 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

    2. 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

    3. 运行时常量池都分配在 Java 虚拟机的方法区之中

举例说明:


public class A{

}

​public void test(){
  A a = new A();
  int b = 1;
}

这个例子中:

  • a是A对象的引用所以放在栈中,A是自定义对象所以放在堆中.

  • b是基础数据类型所以在栈中.

 

Java内存模型概述

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图

需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下

  • 主内存

    主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

  • 工作内存

    主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

弄清楚主内存和工作内存后,接了解一下主内存与工作内存的数据存储类型以及操作方式,根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存,简单示意图如下所示:

 

JMM存在的必要性

在明白了Java内存区域划分、Java多线程的实现原理与Java内存模型的具体关系后,接着来谈谈Java内存模型存在的必要性。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。如下图,主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x=1进行操作,A/B线程各自的工作内存中存在共享变量副本x。假设现在A线程想要修改x的值为2,而B线程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?答案是,不确定,即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值2,这是因为工作内存是每个线程私有的数据区域,而线程A变量x时,首先是将变量从主内存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内,而对于B线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,假如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到自己的工作内存中,这样B线程读取到的值就是x=1,但如果A线程已将x=2写回主内存后,B线程才开始读取的话,那么此时B线程读取到的就是x=2,但到底是哪种情况先发生呢?这是不确定的,这也就是所谓的线程安全问题。 

为了解决类似上述的问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则也称为Java内存模型(即JMM),JMM是围绕着程序执行的原子性、有序性、可见性展开的,下面我们看看这三个特性。

 

理解指令重排

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

  • 编译器优化的重排

    编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令并行的重排

    现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

  • 内存系统的重排

    由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

原子性、可见性、有序性具体介绍请看我的另一篇文章:

volatile详细解读


JMM提供的解决方案

在理解了原子性,可见性以及有序性问题后,看看JMM是如何保证的,在Java内存模型中都提供一套解决方案供Java工程师在开发过程使用,如原子性问题,除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性。而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化,关于volatile稍后会进一步分析。除了靠sychronized和volatile关键字来保证原子性、可见性以及有序性外,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。

JMM中的happens-before 原则

倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题。

  1. 程序顺序原则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁原则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量原则:对一个volatile域的写操作,happens-before于任意后续对这个volatile域的读操作。
  4. 传递性:如果A happens-before B,B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start(),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join(),那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

如果没理解,没关系,请看我的博文:

happens-before深入理解

 

文章摘自https://blog.csdn.net/javazejian/article/details/72772461

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值