Java 并发/多线程教程(十一)-JAVA内存模型

简介: 本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获。由于个人水平有限,不对之处还望矫正!        Java内存模型指定Java虚拟机如何与计算机的内存(RAM)一起工作。

本系列译自jakob jenkov的Java并发多线程教程,个人觉得很有收获。由于个人水平有限,不对之处还望矫正!

        Java内存模型指定Java虚拟机如何与计算机的内存(RAM)一起工作。Java虚拟机是整个计算机的一个模型,所以这个模型自然包含了一个内存模型——也就是Java内存模型。

        如果您想要设计正确的并发程序,那么理解Java内存模型是非常重要的。Java内存模型指定了不同线程如何以及何时可以看到由其他线程写入共享变量的值,以及在必要时如何同步访问共享变量。

        原来的Java内存模型不够用,所以Java内存模型在Java 1.5中被修改了。Java内存模型的这个版本仍然在Java 8中使用。

内部JAVA内存模型

      JVM内部使用的Java内存模型划分了线程栈和堆之间的内存。这个图表从逻辑的角度演示了Java内存模型

1240

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

        线程栈还包含每个正在执行的方法的所有本地变量(调用栈上的所有方法)。线程只能访问它自己的线程栈。线程创建的局部变量对于所有其他线程都是不可见的,而不是创建线程的线程。即使两个线程在执行完全相同的代码,两个线程仍然会在各自的线程栈中创建该代码的本地变量。因此,每个线程都有自己的每个局部变量的版本。所有基本数据类型本地变量如(boolean、byte、short、char、int、long、float、double)都被完全存储在线程栈中,因此对其他线程来说是不可见的。一个线程可能将pritimive变量的副本传递给另一个线程,但是它不能共享原始的局部变量本身。

        堆包含在Java应用程序中创建的所有对象,而不管创建对象的线程是什么。这包括原始类型的对象版本(如字节、整数、Long等等)。如果一个对象被创建并分配给一个局部变量,或者作为另一个对象的成员变量创建,对象仍然存储在堆中,这无关紧要。

          下面是一个图表,说明了在线程堆栈上存储的调用堆栈和本地变量,以及存储在堆上的对象:

1240

        局部变量可能是一种基本数据类型,在这种情况下,它完全被放在线程栈中。

        局部变量也可能是一个对象的引用。在这种情况下,引用(本地变量)存储在线程栈中,但是对象本身存储在堆上。

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

        对象的成员变量和对象本身一起存储在堆中.

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

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

        以下的图解说明上面的几点

1240

两个线程有一组本地变量,局部变量(局部变量2)指向堆上的一个共享对象(对象3),这两个线程对同一对象有不同的引用。
它们的引用是本地变量,因此存储在每个线程的线程堆栈中(在每个线程堆栈中),不过,这两个不同的引用指向堆上的同一个对象。注意,共享对象(对象3)是如何引用对象2和对象4作为成员变量的。

上图也给我们展示了本地变量同时指向堆中的两个不同对象(如variable1 指向堆上的两个不同对象Object1,Object2),理论上如果线程都引用这两个对象,他们都可以访问这两个对象的。但是在上图中,每个线程只对这两个对象的其中一个有引用。

因此,什么样的代码会出现上面的内存图呢?下面的代码非常简单的展示了这个问题:

public class MyRunnable implements Runnable(){

      public void run(){

            methodOne();

      }

      public void methodOne(){

          int localVariablel = 45;

          MySharedObject localVariable2 = MySharedObject.sharedInstance;

          //... do more with local variables.

          methodTwo();

      }

      public void methodTwo(){

          Integer localVariablel = 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();

        // meber variables pointing to two objects on the heap

        public Integer object2 = new Integer(22);

        public Integer 4 = new Integer(44);

        public long member1 = 12345;

        public long member2 = 67890;

}

如果两个线程执行run()方法,将会显示前面的结果,run()方法调用methodOne()方法,methodOne()方法又调用methodTwo()方法。methodOne()声明了一个私有的本地变量(int类型的localVariable1)和一个本地变量引用localVariable2。每个线程执行methodOne()将会在自己的线程栈中复制一份localVariable1和localVariable2,localVariable1将会与其他完全分离,只会生存在他们自己的线程栈上,一个线程不能看到另外的线程对localVariable1的更改。每个线程在执行methodOne()时也会复制一个localVariable2,但是最终这两个复制变量都最终指向堆上的同一个对象。

      代码将localVariable2设置为指向一个静态变量引用的对象。只有一个静态变量的副本,这个副本存储在堆中。因此,localVariable2的两个副本都指向了静态变量指向的MySharedObject的同一个实例。mysharedobtintinstance也被存储在堆中。它对应于上面的图中的对象3。

注意,MySharedObject类也包括两个成员变量,成员变量和类一样存储在堆上。这两个成员变量指向两个Integer对象,这些整数对象对应上图的Object2和Object2.注意method2()如何创建本地变量localVariable1,localVariable1是对一个Integer对象的引用。

methodTwo()方法创建了一个名为localVariable1的本地变量,这个变量引用一个Integer对象,这个方法把localVariable1的引用指向一个Integer实例,每个线程执行methodTwo()时都会存储一个localVariable1的引用副本,实例化的两个整数对象将被存储在堆中,但是由于该方法每次执行该方法时都会创建一个新的整数对象,因此执行该方法的两个线程将创建单独的整数实例。

在method2()中创建的整数对象对应于上面的图中的对象1和对象5。

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

硬件内存架构

      现代的硬件内存体系结构与内部Java内存模型有些不同。为了理解Java内存模型是如何工作的,理解硬件内存架构也是很重要的。本节描述通用的硬件内存架构,后面的部分将描述Java内存模型是如何工作的。

下面是现代计算机硬件架构的简化图

1240

      现代计算机通常有2个或更多的cpu。其中的一些cpu也可能有多个内核。需要指出的是,在一台拥有2个或更多cpu的现代计算机上,可以同时运行多个线程。每个CPU都可以在任何给定的时间运行一个线程。这意味着,如果您的Java应用程序是多线程的,在你的Java应用程序中每个CPU都可以同时运行一个线程(并发)。

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

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

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

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

连接Java内存模型和硬件内存架构之间的差距

正如前面提到的,Java内存模型和硬件内存架构是不同的。硬件内存体系结构不区分线程堆栈和堆。在硬件上,线程堆栈和堆都位于主内存中。线程栈和堆的某些部分有时可能出现在CPU缓存和内部CPU寄存器中。这张图中有这样的例子:


1240

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

      1、线程更新(写)到共享变量的可见性

      2、阅读、检查和写入共享变量时的竞态条件。

这两个问题都将在下面的部分中解释

共享对象的可见性

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

假设共享对象最初存储在主内存中。在CPU上运行的线程会将共享对象读取到它的CPU缓存中。在那里,它对共享对象进行了更改。只要CPU缓存没有被刷新到主存,那么共享对象的更改版本就不会被运行在其他CPU上的线程所看到。这样,每个线程都可以使用自己的共享对象副本,每个副本都位于不同的CPU缓存中

下图演示了所描绘的场景。在左侧CPU上运行的一个线程将共享对象复制到它的CPU缓存中,并将它的count变量更改为2。对于在正确的CPU上运行的其他线程来说,这个更改是不可见的,因为更新计数还没有被刷新到主内存中。


1240

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

竞态条件

如果两个或多个线程共享一个对象,并且多个线程在该共享对象中更新变量,那么可能会出现竞态条件。想象一下,如果线程A读取一个共享对象的变量计数到它的CPU缓存中。想象一下,线程B也一样,但是进入不同的CPU缓存。现在线程A添加了一个计数,而线程B也做了相同的工作。现在,var1已经在每个CPU缓存中增加了两次。

如果这些增量是按顺序执行的,那么变量计数将会增加两次,并将原来的值+2写回主存,然而,这两个增量在没有适当同步的情况下同时进行。不管线程A和B将其更新后的计数写回主存,更新后的值只会比原来的值高1,尽管有两个增量。

这张图说明了上面描述的竞态条件的问题:


1240

要解决这个问题,您可以使用Java同步块。同步块保证在任何给定的时间内只有一个线程可以进入给定的关键部分。同步块也保证在同步块中访问的所有变量都将从主内存中读取,当线程退出同步块时,所有更新的变量将再次被刷新回主存,不管变量是否被声明为volatile。

目录
相关文章
|
1天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
1天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
1天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
1月前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第22天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个主题,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。
15 0
|
17天前
|
安全 Java 容器
Java并发编程:实现高效、线程安全的多线程应用
综上所述,Java并发编程需要注意线程安全、可见性、性能等方面的问题。合理使用线程池、同步机制、并发容器等工具,可以实现高效且线程安全的多线程应用。
14 1
|
27天前
|
安全 Java 开发者
Java并发编程中的线程安全性探究
在Java编程中,线程安全性是一个至关重要的问题,涉及到多线程并发访问共享资源时可能出现的数据竞争和不一致性问题。本文将深入探讨Java并发编程中的线程安全性,介绍常见的线程安全性问题以及解决方法,帮助开发者更好地理解和应对在多线程环境下的挑战。
|
27天前
|
安全 Java 开发者
Java并发编程中的线程安全性探究
在Java开发中,多线程编程是一项常见且重要的技术。本文将深入探讨Java并发编程中的线程安全性问题,从数据竞争到同步机制的选择,帮助开发者更好地理解和应对多线程环境下的挑战。
13 2
|
28天前
|
安全 Java
Java并发编程中的线程安全问题与解决方法
在Java开发中,线程安全是一个至关重要的话题。本文将深入探讨Java并发编程中常见的线程安全问题,并结合实际案例介绍解决这些问题的方法,帮助读者更好地理解和应对多线程环境下的挑战。
18 1
|
1月前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第25天】 在Java开发中,高效地处理并发编程问题对于提升应用的性能和稳定性至关重要。本文将探讨Java并发编程的核心概念——线程安全,并介绍如何通过现代Java并发工具实现性能优化。我们将分析同步机制的工作原理,比较不同的线程安全策略,并通过实例展示如何在不牺牲性能的前提下确保数据的一致性和完整性。
75 2
|
1月前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【2月更文挑战第17天】本文旨在探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细分析线程安全问题的根源,以及如何通过各种技术和策略来解决这个问题。同时,我们还将讨论如何在保证线程安全的前提下,提高程序的性能。