java 内存模型

java内存模型(memory model)定义了java虚拟机如何与计算机内存交互。JVM将内存主要分为栈(stack)内存和堆(heap)内存。每当我们声明新的变量和对象、调用新的方法、声明String或执行类似的操作时,JVM都会从堆栈内存或堆空间为这些操作指定内存。

The Java Memory Model From a Logic Perspective

每个线程有自己的栈内存,是线程独有的。堆是线程共享数据区域。每个线程操作自己的线程栈,存储当前正在执行方法的局部变量(local variable)。一个线程创建的局部变量对创建它的线程以外的所有其他线程是不可见的。即使两个线程正在执行完全相同的代码,这两个线程仍然会在各自的线程堆栈中创建该代码的局部变量。因此,每个线程都有各自版本的局部变量。

所有基本类型的局部变量(boolean, byte, short, char, int, long, float, double)都完全存储在线程堆栈中,对其他线程不可见。局部变量可以是基元类型也可以是对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈上,但对象本身存储在堆上。

The Java Memory Model showing references from local variables to objects, and from object to other objects.

共享变量(shared variable)

所有线程可共同访问的内存称为共享内存或堆内存。

对象的成员变量与对象本身一起存储在堆中。无论成员变量是基元类型,还是引用对象。静态类变量也与类定义一起存储在堆中。堆上的对象可以由所有引用该对象的线程访问。当线程有权访问某个对象时,它也可以访问该对象的成员变量。如果两个线程同时在同一对象上调用一个方法,它们都可以访问对象的成员变量,但每个线程都有自己的局部变量副本

线程对共享变量操作

线程间操作是由一个线程执行的操作,该操作可以被另一个线程检测到或直接影响。程序可以执行几种线程间操作:

Read:读取共享变量

Write:对共享变量赋值

硬件内存架构

Modern hardware memory architecture.

CPU寄存器(CPU Register)是CPU内部一块存储空间,CPU可以直接访问,拥有非常高的访问速度。

Cache :即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,这样就减少了CPU的等待时间,提高了系统的效率。Cache又分为一级Cache(L1 Cache)和二级Cache(L2 Cache),L1 Cache集成在CPU内部,L2 Cache早期一般是焊在主板上,现在也都集成在CPU内部,常见的容量有256KB或512KB L2 Cache.

现在计算机一般会有多个CPU多核。可以同时运行多个线程。多线程在访问(读写)共享变量(主内存)会存在缓存一致性问题。

java内存模型和硬件内存架构间关系

The division of thread stack and heap among CPU internal registers, CPU cache and main memory.

Java内存模型和硬件内存体系结构是不同的。硬件内存体系结构不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主存中。线程堆栈和堆的一部分有时可能存在于CPU缓存和内部CPU寄存器中。由于对象和共享变量可能存储在不同的内存区域,多线程就会带来一些问题。

  • 可见性(Visibility)

如果两个线程同时访问一个共享变量,可能会引发一个线程修改了变量另一个线程无法感知到。可以使用volatile关键字定义共享变量解决可见性问题。volatile修饰的变量读操作会从主内存中读取变量,写操作会将结果立马刷新到主内存。

  • 原子性(Atomicity)

原子性意味着操作是不可中断的。即使多个线程一起执行,操作一旦启动也不会受到其他线程的干扰。也就是说,原子操作是最小操作单元。原子操作包括。。。

例:a =1 是原子操作,只有一个赋值操作。

i++不是原子操作。包含read,+1,write三个操作。在运算过程中i的值可能会变。

  • 有序性(Ordering)

有序性就是程序按照一定顺序执行。在并发编程中也是如此。顺序是指按照编写代码的顺序执行代码的能力。然而,为了提高程序的执行性能和编译性能,计算机和编译器有时会修改程序的执行顺序。但是,在多线程的情况下,编译器对执行顺序的修改可能会导致错误。

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

指令重排:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

计算机执行时内存数据区
image

Happen-before原则

顺序执行原则:在一个线程内保证语义串行化,按代码顺序执行

锁原则:对同一个monitor,unlock必须发生在lock之前。

volitale规则 对volatile的write操作早于接下来的read操作。保证可见性。

线程启动规则 线程的start()方法先于线程内的每一个动作

线程终止原则 线程的所有动作先于线程的终止操作

传递性 A先于B,B先于C,那么A先于C

所有对象的初始化操作先于对象的其它操作。

全是规范,实际代码用不到。

Java内存模型使用内存屏障保证有序性,volatile可以保证有序性,仅限于单线程内使用内存屏障防止重排序。多线程有序性还得靠锁。

内存屏障(Memory Barriers)

保证两个操作之间不进行重排序。内存屏障类型:

Load1; LoadLoad; Load2

Load1; LoadStore; Store2

Store1; StoreStore; Store2

Store1; StoreLoad; Load2

volatile内存屏障:

读:读后插入读屏障:volatile读,LoadLoad,LoadStore。

写:前后插入写屏障:StoreStore,volatile写,StoreLoad。

volatile使用内存屏障可以保证线程内有序性。

参考:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html

https://jenkov.com/tutorials/java-concurrency/java-memory-model.html

https://www.baeldung.com/java-stack-heap

https://gee.cs.oswego.edu/dl/jmm/cookbook.html

posted @ 2023-06-05 11:20  朋羽  阅读(35)  评论(0编辑  收藏  举报