死磕并发之Java内存模型(Java Memory Model)

Posted Alance

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了死磕并发之Java内存模型(Java Memory Model)相关的知识,希望对你有一定的参考价值。

Java内存模型JMM

java内存模型定义

上一遍文章我们讲到了CPU缓存一致性以及内存屏障问题。那么Java作为一个跨平台的语言,它的实现要面对不同的底层硬件系统,设计一个中间层模型来屏蔽底层的硬件差异,给上层的开发者一个一致的使用接口。Java内存模型就是这样一个中间层的模型,它为程序员屏蔽了底层的硬件实现细节,支持大部分的主流硬件平台。

java内存模型(Java Memory Mode):java内存模型是java虚拟机内存如何与计算机内存(RAM)一起工作。java虚拟机是是整个计算机的模型,所以这个模型自然包含一个内存模型。也可以说JMM是java虚拟机内存使用规范。

通俗的来讲,就是描述Java中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

Java内存模型规定了不同线程如何以及何时可以看到其他线程写入共享变量的值以及如何在必要时同步对共享变量的访问

注意:Java Memory Model并不是真实存在的,他只是物理内存模型的一个映射。

Java 内存模型介绍


JVM中内存分配的两个概念:

  1. stack(栈)
    特点: 存取速度快、对象生命周期确定、数据大小确定。
    存储数据:基本类型变量、对象引用(句柄)
    位置:缓存、寄存器、写缓冲区。

  2. heap(堆)
    特点: 存取速度慢、运行时动态分配大小、对象生命抽周期不确定、垃圾回收。
    存储数据:对象
    位置:主内存、缓存

理论上说所有的stack和heap都存储在物理主内存中,但随着CPU运算其数据的副本可能被缓存或者寄存器持有,持有的数据遵从一致性协议.

存储方式

上面说到,一个对象时存储在heap上的,对象中所属的方法与方法的成员变量存储在stack上。一个对象的成员变量随着对象本身存储在堆上,无论该对象类型是引用类型或者是基本类型。 静态变量和对象类定义存储于堆上。

并发原因

存储在堆上的对象可以被持有该对象引用的栈访问。能访问对象,也就能访问该对象中的成员变量。当了两个线程同时访问一个对象时,每个线程都拥有该对象成员变量的私有拷贝

这里只是粗略分配了java内存模型。具体细节的内存分配请查看
JVM内存管理概述

Java内存模型与系统内存模型

我们来看看一个关系图:

在系统内存架构中并没有栈(stack)、堆(heap)这种概念,只有寄存器(register)、缓存(cache)、主内存(RAM、Main Memory)。理论上说所有的栈和堆都存储在主内存中,但随着CPU运算其数据的副本可能被缓存或者寄存器持有。持有的数据遵从CPU-Cache一致性协议

CPU内存模型、一致性协议可以参考前一篇文章死磕并发之CPU缓存一致性协议(MESI)

Java 内存模型抽象结构图

主内存:保存了所有的变量。

共享变量:如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量。

比如成员变量、静态变量、数组元素等。

工作内存:每个线程都有自己的工作内存,线程独享,保存了线程用到了变量的副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互。为了更高的效率java虚拟机、硬件系统可能让工作内优先分配在寄存器、缓存中。

JMM对共享内存的操作做出了如下两条规定:

  • 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。
  • 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。

java并发问题的根源

假设线程A和线程B同事访问某个对象的成员变量x。当线程a需要操作变量a,时会将a副本复制到线程A的工作内存中。

当线程a未执行完毕,线程b也要访问变量a

但是线程a与线程b操作的是自己工作空间中的变量副本。 线程a中的副本和线程b中间的副本相符不可见。如果a线程率先完成了任务并写回主存。那么线程b的运算就是在使用后脏数据运算。如果b也写回主存那么线程a的任务就会丢失。

为了保证程序的准确性,我们就需要在并发时添加额外的同步操作。

java内存模型-内存间的八种同步操作

操作过程

我们接着再来关注下变量从主内存读取到工作内存,然后同步回工作内存的细节,这就是主内存与工作内存之间的交互协议。Java内存模型定义了以下8种操作来完成,它们都是原子操作(除了对long和double类型的变量)。

锁定(lock):作用于主内存中的变量,将他标记为一个线程独享变量。

通常意义上的上锁,就是一个线程正在使用时,其他线程必须等待该线程任务完成才能继续执行自己的任务。

解锁(unlock):作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。

执行完成后解开锁。

read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

从主内存 读取到工作内存中。

load(载入):把read操作从主内存中得到的变量值放入工作内存的变量的副本中。

给工作内存中的副本赋值。

use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。

程序执行过程中读取该值时调用。

assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

将运算完成后的新值赋回给工作内存中的变量,相当于修改工作内存中的变量。

store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

将该值从变量中取出,写入工作内存中。

write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

将工作内存中的值写回主内存。

读取执行步骤

写入执行步骤

操作规则

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。

  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

回顾

1.Java内存模型是一个规范,他规定了不同线程如何以及何时可以看到其他线程写入共享变量的值以及如何在必要时同步对共享变量的访问。

2.java内存模型要求,调用栈和本地变量存储在线程栈上,对象存放在堆上。线程之间的通信必须要经过主内存。

3.定义了同步的八个操作,以及使用这八个操作需要遵守的规则。

以上是关于死磕并发之Java内存模型(Java Memory Model)的主要内容,如果未能解决你的问题,请参考以下文章

并发编程之java内存模型(Java Memory Model ,JMM)

Java内存模型之分析volatile

『死磕Java并发编程系列』并发编程工具类之CountDownLatch

『死磕Java并发编程系列』并发编程工具类之CountDownLatch

『死磕Java并发编程系列』并发编程工具类之CountDownLatch

『死磕Java并发编程系列』并发编程工具类之CountDownLatch