多线程基础——JMM

Java内存模型(JMM)规定了Java虚拟机如何处理多线程间的内存交互,确保并发编程的一致性。JMM将内存分为线程堆栈和堆,线程有自己的堆栈用于存储局部变量和方法调用信息,而堆中存储所有线程创建的对象。JMM通过volatile和synchronized关键字来解决对象可见性和竞争条件问题,确保线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java内存模型(JMM)

Java虚拟机是整个计算机的模型,因此这个模型自然包括一个内存模型——也就是Java内存模型。Java内存模型指定Java虚拟机如何使用计算机的内存(RAM)。它是一种虚拟机规范,屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。

The Java memory model describes how threads in the java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantic of the Java programming language.

Java内存模型描述了java编程语言中的多线程是怎样与内存交互的。Java内存模型与代码的单线程执行情况,一起提供了java编程语言的语义。

The original Java memory model, developed in 1995, was widely perceived as broken,[1] preventing many runtime optimizations and not providing strong enough guarantees for code safety. It was updated through the Java Community Process, as Java Specification Request 133 (JSR-133), which took effect in 2004, for Tiger (Java 5.0).[2][3]

提到Java内存模型,一般指的是JDK 5 开始使用的新的内存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。

内部结构

JVM内部使用的Java内存模型将内存划分为线程堆栈

The Java Memory Model From a Logic Perspective

Thread Stack

在Java虚拟机中运行的每个线程都有自己的线程堆栈。线程堆栈包含关于线程调用了哪些方法以到达当前执行点的信息。也称为“调用堆栈”。当线程执行其代码时,调用堆栈会发生变化。

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

所有基本类型的局部变量(boolean, byte, short, char, int, long, float, double)都完全存储在线程堆栈中,因此对其他线程是不可见的。一个线程可以将局部变量的副本传递给另一个线程,但它自己不能共享局部变量。

Heap

堆包含Java应用程序中创建的所有对象,而不管该对象是由哪个线程创建的。这包括局部类型的对象版本(例如Byte、Integer、Long等)。无论对象是创建并赋给局部变量还是作为另一个对象的成员变量创建的,该对象仍然存储在堆上。

存储关系

The Java Memory Model showing where local variables and objects are stored in memory.

局部变量可以是基本类型,在这种情况下,它完全保存在线程堆栈中。

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

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

对象的成员变量和对象本身一起存储在堆上。当成员变量是基本类型时,以及当它是对象的引用时,都是如此。

静态类变量也与类定义一起存储在堆上。

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

代码示例

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

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;
}

每个执行methodOne()的线程将在各自的线程堆栈上创建自己的localVariable1和localVariable2副本。localVariable1变量将彼此完全分开,只存在于每个线程的线程堆栈中。一个线程不能看到另一个线程对它的localVariable1副本所做的更改。

每个执行methodOne()的线程也将创建它们自己的localVariable2副本。然而,localVariable2的两个不同副本最终都指向堆上的同一个对象。代码将localVariable2设置为指向静态变量引用的对象。静态变量只有一个副本,这个副本存储在堆上。因此,localVariable2的两个副本最终都指向静态变量所指向的MySharedObject的同一个实例。MySharedObject实例也存储在堆上。它对应于上图中的对象3。

请注意MySharedObject类也包含两个成员变量。成员变量本身与对象一起存储在堆上。这两个成员变量指向另外两个Integer对象。这些Integer对象对应于上图中的对象2和对象4。

还要注意methodTwo()如何创建名为localVariable1的局部变量。该局部变量是对Integer对象的对象引用。该方法将localVariable1引用设置为指向一个新的Integer实例。localVariable1引用将存储在每个执行methodTwo()的线程的一个副本中。实例化的两个Integer对象将存储在堆上,但由于每次执行该方法时该方法都会创建一个新的Integer对象,因此执行该方法的两个线程将创建单独的Integer实例。在methodTwo()中创建的Integer对象对应于上图中的对象1和对象5。

还要注意类MySharedObject中的两个成员变量,它们的类型为long,这是一个基本类型。由于这些变量是成员变量,它们仍然与对象一起存储在堆上。只有局部变量存储在线程堆栈上。

现代硬件内存架构

Modern hardware memory architecture.

 现代计算机通常有两个或更多的cpu。有些cpu可能有多个核。关键是,在拥有2个或更多cpu的现代计算机上,可能有多个线程同时运行。每个CPU在任何给定的时间都能够运行一个线程。这意味着,如果您的Java应用程序是多线程的,那么每个CPU可以同时(并发地)运行Java应用程序中的一个线程。

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

每个CPU还可以有一个CPU缓存内存层。事实上,大多数现代cpu都有一定大小的缓存内存层。CPU可以比主存更快地访问它的高速缓存,但通常没有它访问内部寄存器的速度快。因此,CPU缓存内存的速度介于内部寄存器和主存之间。有些cpu可能有多个缓存层(级别1和级别2),但是要理解Java内存模型如何与内存交互,了解这一点并不重要。重要的是要知道cpu可以有某种类型的缓存内存层。

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

通常,当CPU需要访问主存时,它会将部分主存数据读入其CPU缓存。它甚至可以将部分缓存读入其内部寄存器,然后对其执行操作。当CPU需要将结果写回主存时,它会将其内部寄存器的值刷新到缓存中,并在某个时刻将该值刷新回主存。

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

Java内存模型和硬件内存架构之间的桥梁

Java内存模型和硬件内存体系结构是不同的。硬件内存体系结构并不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主内存中。部分线程堆栈和堆有时会出现在CPU缓存和内部CPU寄存器中。

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

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

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

对象可见性

如果两个或多个线程共享一个对象,而没有正确使用volatile声明或同步,那么一个线程对共享对象的更新可能对其他线程不可见。

假设共享对象最初存储在主内存中。然后,运行在CPU 1上的线程将共享对象读入其CPU缓存。在这里,它对共享对象进行了更改。只要CPU缓存没有被刷新回主存,更改后的共享对象版本对运行在其他CPU上的线程是不可见的。这样一来,每个线程都可能拥有共享对象的自己的副本,每个副本位于不同的CPU缓存中。

Visibility Issues in the Java Memory Model.

上图所示,在左侧CPU上运行的一个线程将共享对象复制到其CPU缓存中,并将其count变量更改为2。该更改对于在右边CPU上运行的其他线程是不可见的,因为对count的更新还没有刷新到主内存中。

要解决这个问题,可以使用Java的volatile关键字。volatile关键字可以确保给定的变量直接从主内存中读取,并且总是在更新时写回主内存。

竞争条件

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

如果线程A将一个共享对象的变量count读入它的CPU缓存中。再想象一下,线程B做同样的事情,但是放到了不同的CPU缓存中。现在线程A将1添加到计数中,线程B也做同样的事情。现在var1已经增加了两次,一次在每个CPU缓存中。如果这些增量顺序执行,变量count将增加两次,并将原始值+ 2写回主存。

但是,这两个增量是同时执行的,没有进行适当的同步。不管线程A和线程B将更新后的count写回主存,更新后的值将只比原始值高1,尽管有两次增量。

Race Condition Issues in the Java Memory Model.

要解决这个问题,可以使用Java  synchronized blocksynchronized保证在任何给定时间只有一个线程可以进入代码的给定临界区。synchronized还保证在同步块中访问的所有变量都将从主内存中读取,当线程退出同步块时,所有更新的变量将再次刷新到主内存中,无论该变量是否声明为volatile。

内存交互

一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节 JMM定义了以下8种操作来完成,都具备原子性

  • lock(锁定) 作用于主内存变量,把一个变量标识为一条线程独占的状态
  • unlock(解锁) 作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定 unlock之前必须将变量值同步回主内存
  • read(读取) 作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load
  • load(载入) 作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本
  • use(使用) 作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作
  • assign(赋值) 作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • store(存储) 作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用
  • write(写入) 作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中

这里的lock和unlock是实现synchronized的基础,Java并没有把lock和unlock操作直接开放给用户使用,但是却提供了两个更高层次的指令来隐式地使用这两个操作,即moniterenter和moniterexit

 

文献

Java memory model

Java Concurrency—— Java Memory Model

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值