Java 内存模型(Java Memory Model)

 2019-12-10 16:21  阅读(1497)
文章分类:Java Core

JVM内存模型定义的是Java虚拟机(JVM)如何在计算机内存(RAM)中工作机理。

深入理解JVM内存模型对于多线程下编写正确的并发程序至关重要。Java内存模型指定了不同线程在什么时候、以什么样的方式访问其他线程写入到共享变量的值,以及如何同步访问共享变量。

最初版本的Java内存模型相对不够完善,JDK 1.5对此进行了完善,并一直沿用至Java 8

The Internal Java Memory Model

简单来讲,JVM内存模型将内存分为线程栈和堆,下图从逻辑的角度说明了内存模型:
20191210001767\_1.png

JVM中的每个线程都有自己的线程堆栈。线程堆栈包含关于线程执行到当前节点所调用的方法信息,我们将其把称为“调用堆栈”。随着代码的执行,调用堆栈也随之发生变化。

线程堆栈还包含了堆栈中所有方法(包括线程正在执行)中定义的局部变量。每一个线程只能访问自己的线程堆栈。由线程创建的局部变量除了对创建父线程之外的其他所有线程均不可见,即使两个线程正在执行完全相同的代码,两个线程仍然会在各自的线程堆栈中创建该代码的局部变量。因此,每个线程都有自己的局部变量副本。

线程中定义的所有基础类型(boolean、byte、short、char、int、long、float、double)的局部变量都只存于当前线程私有堆栈上,因此,这些基础类型的局部变量对其他线程同样不可见。一个线程可以将一个基本类型变量的副本传递给另一个线程,但无法与别的线程共享基础类型局部变量本身。

应用程序所创建(无论是作为一个局部变量,抑或作为另一个对象的成员变量)的所有对象(包括基础类型的封装对象:如,Byte,Long,Int)均存放于堆上。

下图展示了存于线程堆栈上的调用堆栈和局部变量,以及存于堆上的对象:

20191210001763\_2.png

局部变量可能是基础类型,在这种情况下,它完全存于在线程堆栈上。

局部变量也可以是对象的引用,在这种情况下,对象引用存储在线程堆栈上,而对象本身存储则存于堆上。

对象可能包含方法,这些方法可能包含局部变量,这些局部变量也存储在线程堆栈上,即使方法所属的对象存储在堆上。

对象的成员变量(当成员变量为基础类型或者为对象引用时)与对象本身一起存储在堆中。

静态类变量与类定义一起存于堆中。

每一个线程都是通过对象引用访问存于堆中的对象,同时也通过对象引用访问到对象成员变量,即使两个线程同时访问,他们访问的也只是目标对象的局部变量拷贝。

20191210001763\_3.png

上图中,线程定义了一组局部变量,其中(Local Variable2)指向的是堆上的共享对象Object3,两个线程分别维护了一个该对象的引用。虽然这两个对象应用指向的是同一个对象,但对于线程来说,他们是属于线程的局部变量,因此存于线程的堆栈中。

注意,共享对象(Object 3)包含了两个指向Object2和Object4的对象引用作为成员变量,因为通过这种传递引用的方式,线程也可以访问对象Object2 和Object4。

上图用代码简单实现如下:

public class MyRunnable implements Runnable() {

        public void run() {
            methodOne();
        }

        public void methodOne() {
            int localVariable1 = 45;

            MySharedObject localVariable2 =
                MySharedObject.sharedInstance;

            //... do more with local variables.

            methodTwo();
        }

        public void methodTwo() {
            Integer localVariable1 = new Integer(99);

            //... do more with local variable.
        }
    }

public class MySharedObject {

        //static variable pointing to instance of MySharedObject

        public static final MySharedObject sharedInstance =
            new MySharedObject();

        //member variables pointing to two objects on the heap

        public Integer object2 = new Integer(22);
        public Integer object4 = new Integer(44);

        public long member1 = 12345;
        public long member2 = 67890;
    }

如果两个线程执行run()方法,那么执行结果即是上图所示关系图。run()方法调用methodOne方法,而methodOne调用方法methodTwo,methodOne方法声明一个基础类型int型的局部变量localVariable1和一个局部变量localVariable2,该变量是一个指向共享对象MySharedObject 的对象引用。

当线程执行methodOne方法时,它会在自己的线程堆栈上创建localVariable1和localVariable2的变量副本,对于局部变量localVariable1,其只存于当前线程的堆栈上,localVariable1变量的改变对其他线程不可见。

而对于变量localVariable2,虽然每个线程都有自己的localVariable2变量副本,但他们只是一个对象引用,最终指向的是存于堆之上的同一个对象,这个对象副本由静态变量所引用,存于内存堆之上,因此两个线程的localVariable2变量副本最终指向的是同一个存于堆上的一个静态变量,该静态变量指向MySharedObject实例,而MySharedObject实例本身也存于内存堆之上。

MySharedObject也包含了两个成员变量,这两个成员变量与对象一起存于内存堆之上,这两个成员变量指向另外两个Integer对象,这两个Integer对象对应上图中的Object2和Object4

methodTwo也创建了一个局部变量localVariable1,这是一个指向Integer对象实例的对象引用,每次线程执行methodTwo方法时,localVariable1对象引用都会保存一个副本,多个线程同时执行时都会创建单独的Integer实例,分别对应上图中的Object1和Object5

MySharedObject还包含了两个基本类型(long)变量member1和member2,因为他们是成员变量,所以他们也和对象一起保存在内存堆之上,只有局部变量才会存于线程的线程堆上。

Hardware Memory Architecture

现代硬件内存架构与内部Java内存模型略有不同。为了更好的了解Java内存模型是如何工作的,你需要同时理解计算机硬件内存体系结构也是很重要的。

下图是一个现代计算机硬件架构的简图:

20191210001763\_4.png

一般来讲,现代计算机通常都是多核结构,即包含有2个或更多的cpu。在同一时间,一个cpu可以处理运行一个java线程,多核机器为同时运行多个线程提供了可能。

每个CPU都包含一组寄存器,相比计算机内存,CPU在这些寄存器上的读写操作要更快

为了弥补这种读写速度上的差异,现在计算机通常都提供了高速缓存cache(一些计算机甚至提供了L1、L多级缓存)来解决这种速度上的不匹配,计算机还包含一个主存区,即我们通常所知的RAM。

通常情况下,当CPU需要访问主内存时,它会将部分主内存读取到它的CPU缓存中。它甚至可以将部分缓存读入内部寄存器,然后进行读写操作。当CPU需要将结果输出写回主存时,它会将其内部寄存器的值刷新到缓存内存中,并在某个时刻(高速缓冲存储器和主存储器之间信息的调度和传送是由硬件自动进行的。)将值刷新到主存。

当CPU需要在高速缓存内存中存储其他东西时,存储在高速缓存内存中的值通常会回写到主内存中去。高速缓存并不需要每次都全部更新。通常,CPU缓存可以一次部分写入更新,然后再重新写回内存。

Bridging The Gap Between The Java Memory Model And The Hardware Memory Architecture

正如前面提到的,Java内存模型和硬件内存架构是不同的。硬件内存架构并不区分线程堆栈和堆。在硬件上,线程堆栈和堆有可能都位于主内存中或者在CPU缓存中或者CPU内部寄存器中。

20191210001763\_5.png

当对象和变量可以存储在计算机的不同内存区域时,问题就有可能产生:

  • 线程对共享变量的更新操作对其他线程的可见性。
  • 线程在读取、检查和更新共享变量时的所产生的竞争条件。

共享对象可见性

如果两个或多个线程需要同时访问一个对象,但却没有正确使用volatile声明或同步机制,那么就有可能产生一个线程无法正确读取到另一个线程对共享变量的更新结果,即可能产生读取“脏数据。

假设共享对象最初存储在主内存中。在CPU上运行的线程将共享对象读入其CPU缓存。并对共享对象进行更新操作。只要CPU缓存没有被刷新回主存,那么,运行在其他CPU上的线程是无法读取到当前线程对共享变量的更新结果。这样,每个线程读取到的只是位于不同cpu缓存中的共享对象副本。

下面即是一个简单的示例:左边cpu中的线程将变量count=1读取到自己的缓存中,并将其修改为2,但修改结果对运行在右边cpu中的线程是不可见的,因为修改结果还没有被刷新回主存中,右边cpu中的线程读取到的仍然是count=1

20191210001763\_6.png

要解决这个问题,您可以使用Java的volatile关键字。volatile关键字可以确保给定的变量直接从主内存读取,并且在更新时立即写回主内存,确保当前修改对其他线程可见。

The Java volatile keyword is used to mark a Java variable as “being stored in main memory”. More precisely that means, that every read of a volatile variable will be read from the computer’s main memory, and not from the CPU cache, and that every write to a volatile variable will be written to main memory, and not just to the CPU cache.

竞争条件

如果两个或多个线程需同时访问一个共享对象,并且进行更新操作,那么就会产生竞争条件。

假设,如果线程A将一个共享对象的变量count读入其CPU缓存。另一个线程同时也进行了相同的操作,但是加载到不同的CPU缓存中。线程A、B同时对变量count做加1操作,那么变量count理论上被加了两次。

如果这些增量操作按顺序执行,那么变量将被增加两次,并将结果=原始值+2写回主存。

但是,两个增量操作同时进行,但没有进行正确的同步机制。不管哪个线程将其更新的count返回到主内存,最终结果也只能是原来的值加1,尽管有两次加1操作。

20191210001763\_7.png

要解决这个问题,就需要使用Java 同步模块synchronized。同步块保证在任何给定的时间内只有一个线程可以访问关键模块,同步块还可以保证在同步块中访问的所有变量都将从主内存中读取,当线程退出同步块时,无论该变量是否被声明为volatile,所有更新后的变量将立即刷新回主内存。

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

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> Java 内存模型(Java Memory Model)

相关推荐