Basic Of Concurrency(五: Java内存模型)

548 阅读9分钟

JVM可以看作是一个完整的计算机系统,自然会有自己的内存模型,就像物理机有RAM一样.Java内存模型决定了Java是如何与物理机内存打交道的.如果你想编写出具有确定行为的多线程并发程序,就需要对Java内存模型有一定的了解.jdk1.5之前的内存模型存在一定的缺陷,因此在jdk1.5之后重新发布了内存模型,这个内存模型一直沿用到jdk1.8.

JVM中的内存模型

在Java内存模型中,将内存区域划分为线程栈和堆.如下图展示了Java内存模型的逻辑平面图:

jmm.png

线程栈

在java中,每创建一个线程,JVM就会在内存中创建一个线程栈,该线程栈中的信息仅会被当前线程访问,对其他线程是不可见的.且每当线程运行时,线程栈中的信息都会得到更新.

线程栈用于存储线程执行过程中全部方法的全部局部变量以及线程当前执行方法的现场信息,方便用于在线程切换后恢复运行.

线程栈中存放的数据类型有基础数据类型(short, int, long, float, double, boolean, byte, char等)和引用类型,而引用类型引用的具体对象则存放在堆内存中.

尽管多个线程执行的是同一段代码,他们各自在线程栈中都有一份局部变量的拷贝,互不影响,各自独立.

尽管线程可以将自己的局部变量传递给另一个线程,然而其他线程仅能得到这个线程局部变量的拷贝,并不能访问到这个线程局部变量本身.

在Java应用中创建的所有对象都会存储在堆中,无论是哪个线程创建的对象.这些对象包含基础数据类型的引用类型(Integer, Long等).

对象的成员变量会跟随对象存储在堆中,而在方法内创建和使用的对象也会存储在堆中,而存储在线程栈中的仅仅是该对象的引用.一个对象作为另一个对象的成员变量也一样会存储在堆中.

实例

  • 局部变量中的基础数据类型, 全部存储在线程栈中
  • 局部变量中的引用类型,引用一般存储在线程栈中,而引用指向的对象将存储在堆中
  • 一个对象可以包含若干方法,一个方法可以包含若干局部变量.局部变量将全部存储在线程栈中.尽管这些方法所属的对象是存储在堆中的.
  • 一个对象包含若干成员变量,这些变量将跟随对象一起存储在堆中.无论这些变量是基础数据类型还是指向对象的引用类型.
  • 静态对象和常量将跟随类声明一起被存储在堆中.
  • 只要线程中拥有堆内对象的引用就可以访问到堆中的全部对象.只要能访问到特定对象,就能访问到该对象中的成员变量.若两个线程同时调用一个对象的方法,那么两个线程能同时访问到该对象的成员变量,但两个线程都会持有各自的局部变量拷贝.

下图以逻辑平面图的方式展示上文提及的情形:

jmm2.png

图中展示了两个线程在java内存模型中的逻辑平面图.两个线程栈中各自有两个方法,方法A和方法B,方法A中有两个局部变量,方法B中有一个局部变量.其中局部变量1为基础类型,局部变量2为引用类型,两个线程栈的局部变量2都指向了堆中的对象3,其中对象3中有两个成员变量,成员变量都为引用类型,分别指向对象2和对象4.两个线程栈中的局部变量3都为引用类型,分别指向堆中的对象1和对象5.

通过编码实例来覆盖上文提及的情形,我们创建一个对象JMMExample类,用于模拟上图中存储实例的情形.


public class JMMExample {
    public static class Object1OrObject5 {
        private String str = "obj1 or obj5";

    }

    public static class Object2 {
        private String str = "obj2";

    }

    public static class Object3 {
        public static Object3 getInstance() {
            return new Object3();
        }

        private Object2 obj2;

        private Object4 obj4;

        public Object3() {
            this.obj2 = new Object2();
            this.obj4 = new Object4();
        }

        public Object2 getObj2() {
            return obj2;
        }

        public Object4 getObj4() {
            return obj4;
        }
    }

    public static class Object4 {
        private String str = "obj4";

    }

    public void methodA(Object3 localVariable2) {
        Thread thread = Thread.currentThread();

        System.out.println(thread.getName() + " using localVariable2( " + localVariable2 + " )");

        int localVariable1 = 10;
        System.out.println(thread.getName() + " using localVariable1( " + localVariable1 + " )");

        Object2 obj2 = localVariable2.getObj2();
        Object4 obj4 = localVariable2.getObj4();
        System.out.println(thread.getName() + " using localVariable2 point to ( " + obj2 + " )");
        System.out.println(thread.getName() + " using localVariable2 point to ( " + obj4 + " )");

        methodB();
    }

    public void methodB() {
        Thread thread = Thread.currentThread();

        Object1OrObject5 obj = new Object1OrObject5();
        System.out.println(thread.getName() + " using localVariable3 point to ( " + obj + " )");
    }

    public static void main(String[] args) {
        final Object3 obj3 = Object3.getInstance();
        final JMMExample jmmExample = new JMMExample();
        Runnable myRunnable = () -> jmmExample.methodA(obj3);

        IntStream.range(1, 3)
                .forEach(i -> new Thread(myRunnable, "Thread-" + i).start());
    }
}

执行结果:

Thread-1 using localVariable2( org.menfre.JMMExample$Object3@41d5399c )
Thread-1 using localVariable1( 10 )
Thread-2 using localVariable2( org.menfre.JMMExample$Object3@41d5399c )
Thread-2 using localVariable1( 10 )
Thread-2 using localVariable2 point to ( org.menfre.JMMExample$Object2@4f12cfd6 )
Thread-1 using localVariable2 point to ( org.menfre.JMMExample$Object2@4f12cfd6 )
Thread-1 using localVariable2 point to ( org.menfre.JMMExample$Object4@7c716752 )
Thread-2 using localVariable2 point to ( org.menfre.JMMExample$Object4@7c716752 )
Thread-1 using localVariable3 point to ( org.menfre.JMMExample$Object1OrObject5@4b2d52ec )
Thread-2 using localVariable3 point to ( org.menfre.JMMExample$Object1OrObject5@72b696ac )

从结果我们可以看出线程1和线程2各自将局部变量1和2加载到线程栈中,其中局部变量1为int类型,数值为10,局部变量2为引用类型,指向对象Object3,从运行结果看出他们指向的对象为同一个.随后线程1和线程2通过Object3的成员变量访问到Object2和Object4,从运行结果可以看出Object2和Object4也为同一个对象.再然后线程1和2分别创建了各自的局部变量3,从运行结果可以看出两个线程的局部变量3指向的对象是不同的,这符合上文提及的Object1和Object5.

物理机内存架构

物理机内存架构跟java内存模型不太一样,但了解物理机内存架构有助于理解java内存模型与物理机内存之间的交互。

物理机内存架构逻辑平面图如下:

jmm3.png

现代计算机通常拥有两个或两个以上的cpu数量,有些cpu甚至拥有多个核心。这使得多个线程同时执行成为可能。在同一时间点,每个线程可以交由一个cpu进行调度,多个线程可以同时执行。若你的应用支持多线程,那么你的程序将会在多个cpu中执行。

通常物理机内存会有三层架构,分别是主存,cpu缓存(cpu缓存可能会有多级,如1~3不等,但不影响理解),还有位于cpu中的多组寄存器。主存的容量一般比cpu缓存和寄存器的容量大得多,而cpu缓存容量会比寄存器大。

通常寄存器的读写速度会大于cpu缓存,而cpu缓存的读写速度会大于主存。cpu执行时会将主存的部分数据加载到cpu缓存,同样会将cpu缓存中的部分数据加载到寄存器中,然后再对寄存器中的数值进行操作。

cpu执行结束后会将结果回写到cpu缓存,但cpu缓存中的数据更新后不会立即写回到主存中,而是当cpu需要将其他数据加载到cpu缓存中时,才将cpu缓存中更新后的数据回写到主存。

cpu缓存不会一次性的写入和写出整个缓存区,而是分块的进行写入和写出。cpu缓存中分块单位称为“cache lines”。

Java内存模型与物理机内存架构交互

物理机内存并不会区分Java内存模型中的线程栈和堆。会将线程栈中的局部变量和堆中的对象无差别的载入主存中。且无论是线程栈中的局部变量还是堆中的对象都会出现在物理机内存三层架构中。如下逻辑平面图所示:

jmm4.png

一旦变量和对象被载入到多个存储区域后,就会暴露出特定的问题,最主要的问题是如下两种;

  • 共享对象更改后对其他线程的可见性
  • 多线程读取和更改共享变量时产生的竟态条件

共享对象可见性

若一个线程将主存中的共享变量加载到cpu缓存中操作,随后另一个线程将主存中的共享变量加载到cpu缓存中,此时第一个线程在cpu缓存中对共享变量进行的更新对另一个线程不可见。如下逻辑平面图所示:

jmm5.png

图中主存中存储有共享变量count,count值等于1。在没有volatile修饰符修饰的情况下,此时两个线程分别将共享变量加载到cpu缓存中,第一个线程对count进行+1操作,此时count数值更新为2,但cpu缓存并不会将更新后的数值立即写回主存,此时线程2加载到cpu缓存中的count并不是线程1更新后的数值。

java中,可以用volatile修饰共享变量,来让共享变量得到更新后立即写回主存,以解决上述问题。

竟态条件

若两个线程同时将主存中的共享变量加载到cpu缓存中进行操作,同时更新共享变量,写回主存后的预期变化是两次更新都能对主存中的共享变量生效。但事实是,在没有任何同步措施的情况下,两个更新被回写到主存后,仅会保留最后一次的更新。如下逻辑平面图所示:

jmm6.png

图中主存中存储有共享变量count,count值等于1。在没有任何同步措施的情况下,此时两个线程同时将共享变量加载到cpu缓存中,两个线程都对count进行+1操作,此时共享变量在cpu缓存中有两个版本。在两个线程将更新后的count写回主存时,主存中的共享变量应该被更新成3,但此时仅会有一个+1操作生效,即count会被更新为2.

java中,可以用synchronized来修饰临界区代码,生成同步块。同步块中的变量一次仅能被一个线程访问加载到cpu缓存中,且cpu缓存中对同步块中的共享变量的更新会被立即写回主存,无论该共享变量有无volatile修饰。

该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: 线程安全
下一篇: 同步代码块