Java内存模型很好的说明了JVM是如何在内存里工作的,JVM可以理解为java执行的一个操作系统,作为一个操作系统就有内存模型,这就是我们常说的JAVA内存模型。
如果我们想正确的写多线程的并行程序。理解好java内存模型在多线程下的工作方式是及其重要的,这可以帮我们更好的理解底层的工作方式。
java内存模型说明了不同的线程怎样以及何时可以看到其他线程写入共享变量的值,以及同步程序怎么共享变量。最初的java内存模型不够好,存在很多的不足,所以在java1.5z中,java内存模型的版本的进行了一次重大的更新与改进,并且在java8中仍然被使用。
内部java内存模型
JVM的内部的内存模型分为了两部分,thread stack和heap,也就是线程栈和堆,我们将复杂的内存模型抽象成下图:
每一个在JVM中运行的线程在内存里都会有属于自己的线程栈。线程栈一般包含这个线程的方法执行到哪一个点了这些信息,也被称作“call stack”,当线程执行代码,调用栈就会随着执行的状态改变。
线程栈也包括了每个方法执行时的local 变量,所有的方法也都存储在线程栈上,一个线程可以只能访问自己的线程栈。每个线程自己创建的本地本地变量对其他线程是不可见的,也就是私有的,即使两个线程调用的是同一个方法,每个线程会分别保存一份本地变量,各自属于各自的线程栈。
所有基本类型的local变量( boolean, byte, short, char, int, long, float, double)全都被存储在线程栈里,而且对其他线程是不可见的,一个线程可能会传递一份基本类型的变量值的一份拷贝给另一个线程,但是自己本身的变量是不能共享的,只能传递拷贝。
堆中存储着java程序中new出来的对象,不管是哪个线程new出来的对象,都存在一起,而且不区分是哪个线程的对象。这些对象里面也包括那些原始类型的对象版本(e.g. Byte, Integer, Long etc.). 不管这个对象是分配给本地变量还是成员变量,最终都是存在堆里。
下面这个图就说明了线程栈中存储了local变量,堆中存储着对象object。
一个原始数据类型的本地变量将完全被存储在线程栈中。
本地变量也可以是指向对象的引用,在这种情况下,本地变量存在线程栈上,但是对象本身是存在堆上。
一个对象可能包含方法这些方法同时也会包含本地变量,这些本地变量也是存储在线程栈上面,即使他们所属于的对象和方法是存在堆上的。
一个对象的成员变量是跟随着对象本身存储在堆上的,不管成员变量是原始数据类型还是指向对象的引用。
静态的类变量一般也存储在堆上,根据类的定义。
存储在堆上的对象可以被所有的线程通过引用来访问。当一个线程持有一个对象的引用时,他同时也就可以访问这个对象的成员变量了。如果两个线程同时调用同一个对象的一个方法,他们就会都拥有这个对象的成员变量,但是每一个线程会享有自己私有的本地变量。
下面这张图就说明以上的内容
两个线程有一系列的本地变量。其中一个本地变量(Local Variable 2)指向堆中的object3.这两个线程每个都有指向同一个对象object3的不同引用。他们的引用是本地变量,都存在各自的线程栈中,虽然这两个不同的引用是指向同一个对象的。
我们还可以发现,共有的对象object3有指向object2和object4的引用,这些引用是作为object3中的成员变量存在的。通过object3中的成员变量的引用,两个线程都可以访问到object2和object4.
这个图也说明了指向堆中不同对象的本地变量。例如图中的object1和object5,不是同一个对象。理论上,所有的线程都可以访问堆中的对象,只要这个线程持有堆中对象的引用。但是这个图中,每个线程只有这两个对象中的一个引用。
硬件层面的内存模型
硬件层面的内存内存结构与JVM中的内存结构是有不同的,对我们来说,正确理解掌握硬件层面的内存模型是很必要的,这可以帮助我们理解java多线程的底层机制,更要了解java内存模型如何在硬件内存结构上工作。这一章将讲述硬件层面内存模型,下一部分将讲述java如何结合硬件工作。
下图是一个简化的现代计算机硬件结构图:
现代计算机通常会有两个甚至更多的cpu,这些cpu可能还会有多个核心,这个意义是,拥有多个cpu的计算机可能会有多个线程在同时执行,每个cpu都可以在任何给定的时间运行一个线程。这就意味着如果我们的java程序是多线程的,在内部就每个线程就会有一个cpu在同时执行。
每个cpu都会有一系列的寄存器registers在cpu的内存中,而且这些寄存器是很重要的。cpu在寄存器上进行计算操作比在主内存中进行计算要快的多。这是因为cpu访问寄存器的速度比访问内存要快得多。
每个cpu也会有一个cpu的cache内存。这是因为cpu访问cache比访问内存的速度要快得多,但是却比访问的寄存器要慢一些,所以cache的速度是介于寄存器和内存的。一些cpu还有多级cache,比如(Level 1 and Level 2),但是这对于我们理解java内存模型关系不大,我们只需要cpu有三层内存结构,寄存器-cache-内存(RAM).
一台计算机一般都会有主内存也就是RAM,所有cpu都可以访问主内存,主内存的容量一般远比cache大得多。
一般的,当cpu需要访问内存的时候,他会先读取一部分主内存到cache中,甚至,会读取一部分cache到内部的寄存器中,然后再在寄存器进行计算操作。当cpu将计算结果写回内存中时,他会flush寄存器和cache中的数据,然后将值写回至内存中。
当cpu要求cache去存储其他内容时,也会将cache中的内容flush到内存中。cpu的cache可以边写入一部分数据到内存,边写入一部分到自己cache中,所以在更新数据,不必要全部清空cache,可以边读边写。一般的,cache真正更新数据是在更小的内存块上,叫做“cache lines”。多个“cache lines”可能正在读取数据到cache中,而另一部分可能正在将数据写回到内存中。
Java内存模型和硬件内存模型的联系
上文已经提到,java内存模型和硬件内存模型是不同的。硬件内存模型不区分堆和栈。在硬件层面,所有的线程栈和堆都被存储在主内存中,一部分线程栈和堆可能有时候会出现在cpu cache中和cpu寄存器中。下图可以说明这个问题:
当对象和变量被存储在不同的内存区域的时候,很多问题就可能发生,主要有以下两类问题:
- 当线程对一些共享数据进行更新或者写操作时,可见性的问题
- 当读写共享数据产生资源竞速的问题
共享对象的可见性
如果多个线程在共享一个对象,没有正确使用volatile或者synchronize声明,更新共享对象的时候就可能出现其他线程不可见的问题。
我们假设共享对象初始化主内存中。一个在cpu中运行的线程读取共享对象到cache中。这时候,随着程序的执行,可能导致共享对象发生一些变化。只要cpu的cache还没有被写回到主内存中,这个共享对象的变化就对其他在cpu上运行的线程不可见。这种情况下,每个线程都会有持有一份自己对于共享对象的拷贝,这份拷贝存储在各自的cpu的cache中,而且对于其他线程是不可见的。
下图说明了大致的情况,在左边cpu执行的线程将共享对象读取到cache中,并且将他的值改变为2.这个变化对右边的cpu的其他线程是不可见的,因为对于变量count的更新还没有被写回到主内存中。
想要解决这个共享对象可见性的问题,可以使用java的volatile关键字,这个关键字可以保证所给定的变量都是直接从主内存中读取,而且每当更新时就立即写回到内存中,所以可以保证变化是及时可见的。
资源竞速
如果多个线程共享一个对象,而且多个线程需要更新共享对象中的变量,那么就可能造成资源竞速的发生。
假设线程A读取读取一个共享对象的变量count到cpu的cache中,同时,线程B也执行同样的步骤,但是是读取到一个不同的CPU的cache中,现在线程A给count加一,线程B也做同样的事情,现在这个变量被加了两次,分别在不同的cpu的cache中。
如果这两次递增操作是被按顺序先后执行的,这个变量count就会被加两次而且比最初的值加了2,写回到主内存中。
然而,如果这两个递增操作是并发执行的,且没有正确的进行同步操作,写回内存的时候,更新后的值只会被加一,虽然实际上是进行了两次递增操作。 下图就说明了程序并发执行的时候,产生的资源竞速的问题:
想要解决这个问题,我们可以使用java中的synchronize关键字。synchronize可以保证只有一个线程能进入那些被声明为synchronize的代码段中。同步的线程可以保证所有同步代码段中的变量都会从内存中读取,而且当线程离开代码块的时候,所有更新后的值都会被写回主内存中,不管这个变量有没有被声明volatile。
rel: https://juejin.im/post/5a3b49506fb9a0452207ac4e
相关推荐
Java内存模型(JVM Memory Model,简称JMM)是Java平台中的一个重要概念,它定义了在多线程环境下,如何在共享内存中读写变量的行为。JMM的主要目标是确保多线程环境下的可见性、有序性和原子性,从而避免数据不一致...
### C++09内存模型与多线程编程 #### 一、引言 随着多核处理器的普及,多线程编程成为了现代软件开发中的一个重要组成部分。C++作为一门广泛使用的编程语言,在C++09标准中引入了一系列重要的新特性,其中最显著的...
本文将深入探讨Java多线程模型的相关知识点,包括线程与进程的区别、线程的实现原理、线程的创建方法以及线程的阻塞与唤醒机制等,旨在为初学者提供一个清晰的多线程概念理解和使用指南。 一、线程与进程的区别 在...
深入掌握Java内存模型对于编写高效且正确的Java多线程程序有着不可忽视的作用。程序员需要对内存模型有充分的理解,包括它的原理、规则以及如何在实际编程中应用这些规则,从而编写出能够正确处理并发的高效代码。
Java多线程、锁以及内存模型是Java编程中不可或缺的部分,尤其在面试中,这些问题的掌握程度往往被视为衡量开发者技术水平的重要标准。以下是一些关于Java并发编程的关键知识点: 1. **Synchronized原理**:...
### Java内存模型(有助理解多线程) #### JMM简介 Java内存模型(JMM,Java Memory Model)是Java虚拟机规范中一个重要的概念,它规定了程序中各种变量(包括实例字段、静态字段和数组元素)的访问规则,以及在...
《汪文君JAVA多线程编程实战》是一本专注于Java多线程编程的实战教程,由知名讲师汪文君倾力打造。这本书旨在帮助Java开发者深入理解和熟练掌握多线程编程技术,提升软件开发的效率和质量。在Java平台中,多线程是...
Java线程-Java内存模型是Java并发编程中的关键概念,它描述了多个线程如何共享和访问内存资源,以及如何保证数据的一致性和安全性。Java内存模型(JMM)是Java虚拟机规范的一部分,用于定义程序中各个线程对共享变量...
在Java编程中,多线程并发是提升程序执行效率、充分利用多核处理器资源的重要手段。本文将基于"java 多线程并发实例"这个主题,深入探讨Java中的多线程并发概念及其应用。 首先,我们要了解Java中的线程。线程是...
Java程序员了解CPU以及相关的内存模型,对于深入理解...通过分析具体的编程问题,比如Java锁的不同实现方式、CPU缓存的工作机制等,可以帮助程序员更好地理解Java内存模型,在多线程环境下写出更加健壮和高效的代码。
《Java多线程编程实战指南-核心篇》是一本深入探讨Java并发编程的书籍,旨在帮助读者掌握在Java环境中创建、管理和同步线程的核心技术。Java的多线程能力是其强大之处,使得开发者能够在同一时间执行多个任务,提高...
Java内存模型是并发编程中一个至关重要的概念,它定义了共享变量的访问规则,以及这些变量如何在多线程环境下进行读写操作。在深入理解Java内存模型之前,我们需要先了解并发编程模型的分类,然后掌握Java内存模型的...
### Java同步线程模型...这些改进不仅增强了Java多线程编程的灵活性,还提高了程序的性能、稳定性和可靠性。未来的研究可以进一步探索这些改进措施在实际应用场景中的效果,并考虑与其他并发编程技术相结合的可能性。
Java线程内存模型为开发者提供了高级抽象层,使得多线程编程变得更加容易。然而,这也带来了一些潜在的问题,特别是对于那些希望利用特定编程技巧(如DCL)来提高性能的应用而言。理解JMM的核心概念及其局限性对于...
Java多线程导出Excel是处理大数据量时的一种高效策略,尤其在面对千万级别的数据时。传统的Apache POI库在处理大规模数据时可能会遇到栈溢出(StackOverflowError)和内存溢出(OutOfMemoryError)等问题,因为这些...
Java多线程编程是提升程序性能和响应性的关键技术。理解多线程的概念,掌握线程的创建、同步、通信、死锁避免等核心知识点,以及合理使用线程池,对于编写高效、稳定的并发程序至关重要。通过实践,开发者可以更好地...
去睡觉吧 第11章 Thread-Specific Storage——每个线程的保管箱 第12章 Active Object——接受异步消息的主动对象 总结 多线程程序设计的模式语言 附录A 练习问题的解答 附录B Java的内存模型 附录C Java线程的...
总结来说,Java多线程并发实战和源码的学习涵盖了线程创建与管理、同步机制、并发容器、内存模型以及并发工具类等多个方面。虽然书中实例不足,但通过结合其他资源,如jcip-examples-src.rar中的代码,可以进一步...