首页 > 编程语言 > Java内存模型的深入讲解
2021
10-12

Java内存模型的深入讲解

Java内存模型展示了Java虚拟机是如何与计算机内存交互的,解决多线程读写共享内存时资源访问的问题。

内存模型

Java虚拟机中的内存模型将线程栈与堆划分开,下图描述了Java内存模型的逻辑图。

每个线程都要自己的线程栈,栈中存储着线程执行到当前位置所调用的方法信息,线程执行代码时,线程栈会不断执行入栈和出栈操作。

线程栈中会存储所有被调用的方法中定义的变量,并且自己访问自己栈中的变量,别的线程不可见。即使两个线程执行相同的代码,也会在线程自己的栈中重复创建变量。一个线程可能会传递变量副本给另一个线程,但不能共享变量本身。

在栈中变量存储形式也有所不同。属于基本变量类型(int,byte,long,boolean,char,double,float,short)的变量,会直接将变量值存储在栈中,而其余类型的变量的值被存储在堆中,线程栈中只保留指向堆中变量地址的指针。
堆中则存储Java程序中创建的所有对象,不管是什么线程创建的。创建对象并将其分配给局部变量,或者将其创建为另一个对象的成员变量都没有影响,该对象仍存储在堆中。

值得注意的是,Java中的静态类变量也会随着类初始化而存储在堆中。

有指向对象指针的所有线程都可以访问堆上的对象。当线程可以访问对象时,它也可以访问该对象的成员变量。如果两个线程同时在同一个对象上调用一个方法,则它们都将有权访问该对象的成员变量,但是每个线程将拥有自己的局部变量副本。

两个线程有一组局部变量,指向堆上的共享对象。这两个线程分别具有对同一对象的不同指针。它们的指针也是局部变量,因此存储在每个线程的线程栈中(在每个线程上)。但是,两个不同的指针指向堆上的同一对象。
下面的代码块就是上图的一个实际例子。

public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }

    public void methodOne() {
        int localVariable1 = 45;
        MySharedObject localVariable2 = MySharedObject.sharedInstance;
        //...
        methodTwo();
    }

    public void methodTwo() {
        Integer localVariable1 = new Integer(99);
        //...
    }
}
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;
}

硬件架构

现代硬件的内存架构与Java内存模型还是有些不同的,了解硬件架构对理解Java内存模型也有帮助。简单的硬件架构图如下:

现代计算机一般是多核CPU,一般不止一个CPU,因此多个线程是可能在物理意义上并发运行的。这意味着,如果Java应用程序是多线程的,则每个CPU可能在Java应用程序中同时(并发)运行一个线程。

每个CPU包含一组寄存器,这些寄存器本质上是CPU内存储器。CPU在这些寄存器上执行操作的速度比对主存储器中的变量执行操作的速度快得多,这是因为CPU可以比访问主存储器更快地访问这些寄存器。

每个CPU可能还具有一个CPU高速缓存。实际上,大多数现代CPU都有一定大小的高速缓存。CPU可以比其主存储器更快地访问其高速缓存,但是通常不如其访问其内部寄存器的速度快。因此,CPU高速缓存存储器位于内部寄存器和主存储器之间的速度之间。某些CPU可能具有多个高速缓存层(L1和L2 Cache)。了解Java内存模型如何与内存交互并不是很重要,重要的是要知道CPU可以具有某种高速缓存层。

计算机还包含一个主存储区(RAM)。所有CPU都可以访问主存储器。主存储区通常比CPU的高速缓存大得多。

通常,当CPU需要访问主内存时,它将部分主内存读入其CPU缓存中。它甚至可以将缓存的一部分读入其内部寄存器,然后对其执行操作。当CPU需要将结果写回主存储器时,它将把值从其内部寄存器刷新到高速缓存,然后在某个时候将值刷新回主存储器。

当CPU需要将其他内容存储在高速缓存中时,通常会将高速缓存中存储的值刷新回主存储器。CPU高速缓存可以一次将数据写入其部分内存,并一次刷新其部分内存。它不必每次更新都读取/写入完整的缓存。通常,缓存在称为“缓存行”的较小存储块中更新,可以将一个或多个高速缓存行读入高速缓存存储器,并且可以将一个或多个高速缓存行再次刷新回主存储器。

Java内存模型与硬件关联

如前所述,Java内存模型和硬件内存体系结构是不同的,硬件内存体系结构不能区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主内存中。线程堆栈和堆的某些部分有时可能会出现在CPU缓存和内部CPU寄存器中。下图对此进行了说明:

当对象和变量可以存储在计算机的各种不同存储区域中时,可能会出现某些问题。 两个主要问题是:

  • 线程更新(写入)到共享变量的可见性。
  • 读取,检查和写入共享变量时的竞争条件。

对象的可见性

如果两个或多个线程共享一个对象,而没有正确使用volatile关键字,则一个线程对共享对象进行的更新可能对其他线程不可见。

每个线程都可以拥有自己的共享库副本,每个副本位于不同的CPU缓存中。想象一下,共享对象最初存储在主存储器中。然后,在CPU上运行的一个线程将共享对象读入其CPU缓存并进行修改。只要未将CPU缓存刷新回主存储器,在其他CPU上运行的线程就看不到共享对象的更改版本。

下图说明了这种情况,在左CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其count变量更改为2。在右CPU上运行的其他线程看不到此更改,因为尚未将count更新写回主内存。

当然这个问题可以使用volatile关键字来解决。

竞争条件

如果两个或多个线程共享一个对象,并且一个以上的线程更新该共享对象中的变量,则可能会发生竞争条件。

假如线程A将共享对象的变量count读入其CPU缓存中,而线程B执行同样操作,但是它位于不同的CPU缓存中。现在,线程A加一个要计数,线程B也执行相同的操作。现在count已增加两次,在每个CPU高速缓存中增加一次。

如果这些增加是顺序执行的,则变量计数将增加两次,并将原始值+2写回到主存储器中。

但是,这两个增量是在没有同步的情况下并发执行的。不管线程A和B中哪个线程将其更新后的版本写回主内存,尽管有两个增量,但更新后的值仅比原始值高1。

该图说明了如上所述的竞争条件问题的发生:

这个问题可以使用synchronized关键字来解决。

总结

到此这篇关于Java内存模型的文章就介绍到这了,更多相关Java内存模型内容请搜索自学编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持自学编程网!

编程技巧