多线程之Java内存模型(JMM)(一)

简介: 在未正确使用锁的时候,多线程的程序可能变的很容易出错,并且难以排查。而JMM则给我们一种规范,它描述了多线程程序如何与内存交互。与文无关JMM大致描述:JMM描述了线程如何与内存进行交互。

在未正确使用锁的时候,多线程的程序可能变的很容易出错,并且难以排查。而JMM则给我们一种规范,它描述了多线程程序如何与内存交互。


img_367699a26a51a796035de9e8d8eda911.png
与文无关

JMM大致描述:

  • JMM描述了线程如何与内存进行交互。Java虚拟机规范视图定义一种Java内存模型,来屏蔽掉各种操作系统内存访问的差异,以实现Java程序在各种平台下都能达到一致的访问效果。
  • JMM描述了JVM如何与计算机的内存进行交互
  • JMM都是围绕着原子性,有序性和可见性进行展开的

JMM的主要目标是定义程序中各个变量的访问规则,虚拟机将变量存储到内存和从内存取出变量这样的底层细节。此处的变量指在堆中存储的元素。

多线程的时候为什么容易出错?

Java内存模型规定所有的共享变量都存储在主内存中,而每条线程有自己的工作内存(本地内存),工作内存保存了共享变量的副本,而不同内存又无法访问对方的工作内存,所以如果线程在工作内存中修改了变量副本,其它线程是无从得知的。

线程的传值均需要通过主内存来完成

img_41723245b59c75025a14642c3623d6ef.png
JMM模型

img_fc3d250cb170fe3a5dfbf05ff78f5e55.png
JMM模型

主内存与工作内存如何交互?

Java内存模型定义了8种操作来完成主内存与工作内存的交互细节,虚拟机必须保证这8种操作的每一个操作都是原子的,不可再分的。

  • lock: 作用于主内存的变量,把变量标识为线程独占的状态
  • unlock: 与lock对应,把主内存中处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read: 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存,便于随后的load使用。
  • load:作用于工作内存的变量,把read读取到的变量放入工作内存副本
  • use: 作用于工作内存,把工作内存的变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign: 作用于工作内存,把执行引擎收到的值赋给工作内存的变量,虚拟机遇到赋值字节码时候执行这个操作
  • store:作用于工作内存,把变量的值传输到住内存中,以便随后的write使用
  • write:作用于主内存,把store操作从工作内存得到的值放入主内存的变量中。
img_fdb8de4d6468131e3d76baa59c1a8e96.png
JMM内存模型

执行上述8种基本操作的规则:

  • 不允许read和load,store和write操作之一单独出现。
  • 不允许一个线程丢弃它最近的assign操作。即变量在工作内存中改变了账号必须把变化同步回主内存
  • 一个新的变量只允许在主内存中诞生,不允许工作内存直接使用未初始化的变量。
  • 一个变量同一时刻只允许一条线程进行lock操作,但同一线程可以lock多次,lock多次之后必须执行同样次数的unlock操作
  • 如果对一个变量进行lock操作,那么将会清空工作内存中此变量的值。
  • 不允许对未lock的变量进行unlock操作,也不允许unlock一个被其它线程lock的变量
  • 如果一个变量执行unlock操作,必须先把次变了同步回主内存中。

这8种操作定义相当严禁,实践起来又比较麻烦,但是可以有助于我们理解多线程的工作原理。有一个与此8种操作相等的Happen-before原则。

Happen-before原则

这个是Java内存模型下无需任何同步器协助就已经存在,可以直接在编码中使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们的顺序就没有保障,虚拟机可以对他们进行任意的重排。

天然的happen-before

  • 程序顺序原则:一个线程内包装语义的串行性
  • volatile变量的写,先发生于读,这保证了volatile变量的可见性
  • 锁规则:unlock先与lock
  • 传递性:A 先于B,B先于C,那么A必然先于C
  • 线程的start先于线程的每一个动作
  • 线程的所有操作优先于线程的终结(Thread.join())
  • 线程的中断(interupt)先于被中断线程的代码
  • 对象的构造函数执行,先于finalize()方法

Java运行时数据区

JVM定义了一些程序运行时会使用到的运行时数据区,其中一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些是与现场一一对应的,这些线程对应的数据区会随着线程的开始和结束而创建和销毁。
这部分参考JVM规范

1. pc寄存器

可以支持多条线程同时允许,每一条Java虚拟机线程都有自己的pc寄存器。任意时刻,一条JVM线程之后执行一个方法的代码,这个方法被称为当前方法(current method)
如果这个方法不是native的,那么PC寄存器就保存JVM正在执行的字节码指令地址。
如果是native的,那么pc寄存器的值为undefined
pc寄存器的容量至少能保证一个returnAddress类型的数据或者一个平台无关的本地指针的值。

2. JVM Stack(虚拟机栈)
  • 每一个JVM线程都有自己的私有虚拟机栈,这个栈与线程同时创建,用于存储栈帧(Frame)。
  • 栈用来存储局部变量与一些过程结果的地方。在方法调用和返回中也扮演了很重要的角色。
  • 栈可以试固定分配的也可以动态调整
    • 如果请求线程分配的容量超过JVM栈允许的最大容量,抛出StackOverflowError异常
    • 如果JVM栈可以动态扩展,扩展的动作也已经尝试过,但是没有申请到足够的内存,则抛出OutofMemoryError异常
3. Heap(堆)

堆是可以可供各个线程共享的运行时存储区域,也是供所有类的实例和数组对象分配内存的区域。堆在JVM启动的时候创建。
堆所存储的就是被GC所管理的各种对象。
堆也是可以固定大小和动态调整的:
实际所需的堆超过的GC所提供的最大容量,那么JVM抛出OutofMemoryError异常。

4. Method Area(方法区)

也是各个线程共享的运行时内存区,它存储每一个类的实例信息,运行时常量池,字段和方法数据,构造函数和普通方法的字节码等内容。还有一些特殊方法。

方法区是堆的逻辑组成部分,也在JVM启动时创建,简单的JVM可以不实现这个区域的垃圾收集。

方法区也可固定大小和动态分配与堆一样,内存空间不够,那么JVM抛出OutofMemoryError异常。

5. Run-Time Constant Pool(运行时常量池)

在方法区中分配,在加载类和接口到虚拟机之后,就创建对应的运行时常量池。

它是class文件中每一个类或接口的常量池表的运行时表现形式。像字符串。Java的主要类型。

存储区域不够用时候抛出OutofMemoryError异常。

6. Native Method Stacks(原生方法栈或本地方法栈)

JDK中native的方法,System类和Thread类中有很多。使用C语言编写的方法,这个也通常叫做C stack。

可以不支持本地方法栈,但是如果支持的时候,这个栈一般会在线程创建的时候按线程分配。

与栈的错误一样,StackOverFlowError和OutOfMemeoryError.

一个案例
img_1c90e350e42732d4eafa86bb841004ac.png
案例
  • 一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。
  • 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
  • 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。
  • 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
  • 静态成员变量跟随着类定义一起也存放在堆上。
  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。
img_4015322359279c5568263aeb7f41c36d.png
Java内存模型和硬件内存架构之间的对应

最后

这次主要讲了一些规则相关的东西,及Java中运行时数据存储的位置,建议看一下《深入理解JVM》最后一章。最好下载JSR-133规范对照着看。

参考:

  • Java内存模型
  • 《深入理解Java虚拟机》
  • 《Java高并发程序设计》
  • 《JVM specification》
相关文章
|
8天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
10天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
10天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
11天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
33 3
|
11天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
92 2
|
19天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
46 6
|
27天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
342 1
|
28天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
2月前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80

热门文章

最新文章