https://www.jianshu.com/p/a3f9f2c3ecf8
- 内部java内存模型
- 硬件层面的内存模型
- Java内存模型和硬件内存模型的联系
- 共享对象的可见性
- 资源竞速
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,不是同一个对象。理论上,所有的线程都可以访问堆中的对象,只要这个线程持有堆中对象的引用。但是这个图中,每个线程只有这两个对象中的一个引用。
下面,我们将写一段实际的代码,这段代码的内存模型就跟上图一样:
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 member1 = 67890;
}
如果两个线程执行run方法那么,上图的内存模型就会是这段程序的执行结果。
methodOne()声明了一个原始数据类型的本地变量,int类型的localVariable1 ,和一个指向对象的引用的本地变量(localVariable2).
每个线程执行methodOne()的时候都会创建属于自己的一份本地变量的拷贝,也就是
localVariable1 and localVariable2在他们各自的线程栈的空间中。localVariable1将会被完全对其他线程是不可见的,只存在与每个线程自己的线程栈空间中。一个线程不能看到其他线程对localVariable1所做的改变与操作,是不可见的。
每个线程执行methodOne()方法的时候也会创建localVariable2的拷贝,但是不同的localVariable2的拷贝最终却指向同一个堆上的对象。这段代码让localVariable2指向之前通过一个静态变量引用的对象。静态变量只会存在一份,不会有多余的拷贝,而且静态变量是存在堆中的。所以,localVariable2的两份拷贝同时指向同一个MySharedObject对象的实例,与此同时,还有一个堆中的静态变量也指向这个对象实例。这个对象就是对应上图中的object3.
我们发现MySharedObject 包含这两个成员变量。这些成员变量跟对象一样存储在堆上。这两个成员变量指向两个integer对象。这两个对象分别对应上图中的object2和object4.
我们发现,methodTwo() 创建了一个本地变量叫做localVariable1。这个本地变量是一个对象的引用,他指向一个integer对象。这个方法将本地变量localVariable1指向一个新的值。在执行methodTwo()的时候,每个线程都会持有一份localVariable1的拷贝。这两个Integer对象将会被初始化在堆上,但是因为每次执行这个方法的时候,这个方法都会创建一个新的对象,所以两个线程会拥有独立的对象实例。这两个对象就对应上图中的object1和object5.
我们发现MySharedObject 中的成员变量是原始数据类型,但由于他们是成员变量,所以依旧存储在堆上。只有本地变量存储在线程栈中。
硬件层面的内存模型
硬件层面的内存内存结构与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关键字(参见笔者的另一篇volatile的博文),这个关键字可以保证所给定的变量都是直接从主内存中读取,而且每当更新时就立即写回到内存中,所以可以保证变化是及时可见的。
资源竞速
如果多个线程共享一个对象,而且多个线程需要更新共享对象中的变量,那么就可能造成资源竞速的发生。
假设线程A读取读取一个共享对象的变量count到cpu的cache中,同时,线程B也执行同样的步骤,但是是读取到一个不同的CPU的cache中,现在线程A给count加一,线程B也做同样的事情,现在这个变量被加了两次,分别在不同的cpu的cache中。
如果这两次递增操作是被按顺序先后执行的,这个变量count就会被加两次而且比最初的值加了2,写回到主内存中。
然而,如果这两个递增操作是并发执行的,且没有正确的进行同步操作,写回内存的时候,更新后的值只会被加一,虽然实际上是进行了两次递增操作。
下图就说明了程序并发执行的时候,产生的资源竞速的问题:
想要解决这个问题,我们可以使用java中的synchronize关键字。synchronize可以保证只有一个线程能进入那些被声明为synchronize的代码段中。同步的线程可以保证所有同步代码段中的变量都会从内存中读取,而且当线程离开代码块的时候,所有更新后的值都会被写回主内存中,不管这个变量有没有被声明volatile。
小结
本文详细的剖析了java内存模型和硬件层面的内存模型,并且分析了硬件和java是怎么在内存模型上合作联系的。这对于我们接下来理解java多线程的概念是及其重要的,打下了牢固的基础。
作者:六尺帐篷
链接:https://www.jianshu.com/p/a3f9f2c3ecf8
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
相关推荐
这里,我们将深度解析这个主题,探讨在Java环境下构建游戏服务器的关键知识点。 首先,我们需要理解Java语言的优势。Java以其跨平台的特性、丰富的类库以及强大的性能,成为了后端开发的首选语言之一,尤其适合大型...
### 基于Java的多线程网络爬虫设计与实现 #### 概述 本文档探讨了在Java环境下设计与实现多线程网络爬虫的技术细节与实践方法。网络爬虫(Web Crawler),是一种自动抓取互联网上网页信息的程序或自动化脚本,其...
Java内存模型(Java Memory Model,简称JMM)是Java并发编程中的核心概念,它定义了Java程序中多线程间共享变量的访问规则。理解JMM对于编写正确、高效的并发程序至关重要。本文将深入探讨JMM的原理、特性以及如何在...
### 三问JMM——有关JVM内存模型的深度解析 #### 前言 近期,在诚信通开源研究小组的专题学习分享会上,我们针对Java内存模型(JMM)进行了深入探讨,现将JMM相关的一些核心概念进行梳理,以便更好地理解和把握JMM的...
Java虚拟机(JVM)内存模型是Java程序运行的基础,它包括了多个关键的内存区域。首先,Java栈是每个线程独立拥有的,当线程创建时,JVM会为其分配栈空间,用于存储局部变量、方法返回值以及方法调用的上下文。栈空间...
3. **多线程编程**:Java以其强大的多线程支持而闻名,书中会介绍线程的创建、同步、并发控制以及死锁等问题的解决方案。 4. **集合框架**:Java集合框架是其强大的特性之一,书中会详细解释List、Set、Map等各种...
# 掌握并发的钥匙:Java Executor框架深度解析 Java作为一种广泛应用的编程语言,自1995年由Sun Microsystems公司(现属Oracle公司)首次发布以来,已经发展成为软件开发领域的重要工具。Java的设计目标包括跨平台...
Java内置了对多线程的支持,通过`Thread`类或实现`Runnable`接口创建线程。了解线程的生命周期、同步机制(synchronized、wait/notify、Lock接口等)以及死锁、活锁和饥饿现象,有助于编写高效、安全的并发代码。 ...
本文将深度解析`volatile`的内存语义实现及应用场景,并基于Java内存模型(JMM)进行分析。 首先,`volatile`关键字的主要作用是确保多线程环境下的数据一致性。当一个变量被声明为`volatile`时,它的修改对于所有...
Java虚拟机(JVM)内存模型深度剖析 Java虚拟机(JVM)是Java应用程序的运行环境,负责解析字节码并执行Java程序。在理解JVM内存模型时,我们需要了解其整体结构以及如何设置内存参数,这对于优化性能至关重要。 ...
3. **内存管理**:讲解Java内存模型,包括堆、栈、方法区等,以及对象的生命周期和垃圾回收机制。这部分内容对于理解性能优化和避免内存泄漏至关重要。 4. **集合框架**:详述ArrayList、LinkedList、HashMap、...
这本书旨在帮助开发者理解和掌握在Java环境中创建高效、可扩展且可靠的多线程应用程序的关键技术和实践。它涵盖了从基本概念到高级主题的广泛内容,是Java开发者的必备参考资料。 在Java并发编程中,多线程是核心...
总结来说,Java内存区域和内存模型是Java运行时内存管理的核心,它们定义了数据如何在内存中存储和访问,以及在多线程环境下如何保证数据一致性。理解和掌握这些概念对于编写高效、健壮的Java代码至关重要。
### 静态的魔力:Java中静态方法和变量的深度解析 #### 一、引言 Java作为一门强大的编程语言,在软件开发领域占据着举足轻重的地位。其面向对象的特性、跨平台的能力以及强大的标准库,使得Java成为开发企业级...
6. **多线程编程** - 详细阐述Java并发编程的基础,包括线程的创建、同步机制(synchronized、Lock)、并发工具类如ExecutorService和Future,以及并发设计模式。 7. **反射与动态代理** - 探讨Java反射机制,...
此外,书中可能还会涉及Java内存模型(JMM),这是Java多线程编程中的关键概念。JMM规定了线程如何共享和访问内存,以及如何处理数据一致性问题。理解内存模型有助于避免并发编程中的常见问题,如数据竞争和死锁。 ...
首先,书中可能涵盖了Java核心类库的深度解析,包括集合框架、多线程、网络编程等。集合框架是Java中处理数据结构的关键部分,如ArrayList、LinkedList、HashMap、HashSet等,它们的工作原理和高效使用技巧都是进阶...