JUC从实战到源码:JMM总得认识一下吧

JUC从实战到源码:JMM

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

前言

在现代计算机编程中,多线程并发编程是提高程序性能和资源利用率的关键技术之一。Java作为一种广泛使用的编程语言,其内存模型(JMM)对于理解和实现多线程编程至关重要。本文档旨在深入探讨Java内存模型(JMM),包括其定义、重要性、核心特性以及如何通过JMM确保多线程程序的正确性和性能。通过阅读本文,读者将能够理解JMM的工作原理,以及如何在实际开发中应用JMM的原则和机制。

简介

JMM,即Java内存模型(Java Memory Model),是Java虚拟机规范中关于多线程操作共享内存的一套规定。JMM定义了Java程序中线程间如何通过内存进行交互,并规定了不同线程对共享变量的可见性和有序性,以确保在并发编程中能够实现正确的同步和共享。

计算机硬件系统

首先我们先通过计算机硬件存储系统来认识一下,先看一下图片。

我们可以通过(windows,mac 不知道,没用过,没钱用) 快捷键 【Ctrl + Shift + Esc】打开任务管理系统。我们都知道,计算机的执行都是层层想依靠,越往寄存器上运行是最快的,寄存器的运行速率快于主存运行,就会导致寄存器等待主存运行,效率就会大大降低,那么,为了保证效率,就在两端之间增加了缓存。

那么,这个与 JMM 是有什么相关系呢?CPU 和物理内存的速度不一致,需要通过把内存的数据读到高速缓存中,在这里面是通过缓存一致性协议来维持速度的一致性问题。

在换来说 Java 程序,他是可跨平台运行的,但是由于平台不一样,不同的硬件和操作系统的内存访问会有差异,于是就有了 JMM 来屏蔽他们之间的差异,来实现能够让 Java 程序在不同平台能够达到一致的内存访问效果。

为什么需要 JMM?

由于上面的介绍,更能够解答这个问题。在多线程编程中,线程间通过共享内存来进行数据交互。每个线程都有自己的工作内存(线程栈),线程在其中存储变量的私有副本。Java中的主存(Main Memory)负责存储所有共享变量的实际值。线程之间相互通信时,必须通过主存来共享数据。不同的线程可能会将共享变量缓存到自己的工作内存中,这样一来,会出现多个线程对同一变量拥有不同的值。JMM规定了变量在主存和工作内存中的交互规则,保证了内存可见性和操作的有序性。

三大特性

可见性

可见性指的是一个线程对共享变量所做的修改能够被其他线程看到。例如,一个线程修改了共享变量 count,在JMM的规则下,其他线程可以正确地读取到 count 的最新值。

在Java中,可以通过 volatile 关键字、 synchronized 块或 final 变量来保证变量的可见性。

JMM 规定所有的变量都是存在主内存中。

在java中,变量都会被放在堆内存(所有线程共享的内存)中,多个线程对共享内存是不可见的,当每个线程去获取这个变量的值时,实际上是copy一份副本在线程自身的工作内存中(是不能够直接去操作主内存的数据,只能在各自本地内存中修改后再存入主内存)。

由于线程是各自对主内存的副本进行操作,所以线程互相是不知道谁改动了数据的。当提交到主内存时候,JMM 会有总线嗅探机制,会通知其他线程这个变量已经被改动了。

系统主内存的共享变量数据被修改写入的时机是不确定的,多线程并发下可能会出现"脏读 ",所以每个线程都有自己的工作内存,里面有该线程所需要的从主内存拷贝的变量副本,在这里进行读/改,而不能直接读写主内存的变量。

原子性

原子性指的是一个操作不可中断,要么完全执行完毕,要么不执行。常见的原子性操作包括读取和写入基本数据类型、赋值等,但自增、自减等复合操作不具备原子性。在多线程环境中,为了确保原子性,可以使用 synchronizedjava.util.concurrent.atomic 包中的原子类(例如 AtomicInteger)。

有序性

有序性指的是代码在执行时,符合程序员的预期顺序(指令重排序的有序)。JMM对线程之间的操作执行顺序提供了一些保证,但在某些情况下为了提高性能,JVM和CPU可能会对指令进行重排序(指令执行顺序与代码顺序不一致,但是执行结果一致)。JMM规定了一些"先行发生"(happens-before)原则来约束指令执行顺序,确保并发编程中的正确性。(指令重排可以保证串行语义一致,但没有义务保证多线程问的语义也一致(即可能产生"脏读"))

源码到最后的执行指令如下:

我们再来看一个例子

shell 复制代码
/**
 * @Author: lyd
 * @Date: 2024/11/12
 */
public class JmmDemo {
    public static void main(String[] args) {
        int x = 1;
        int y = 2;
        x = x ^ 2;
        y = y + x;
    }
}

上面的 demo 有 4 行代码,再执行指令之前,是可以根据一定的情况做重排序。很明显,如果 2 与 3 两行的指令进行互换,那么最终的到的结果是一致的。那么如果将 1 和 3 进行互换,那么就是错误的,因为处理器再进行重排序的时候会考虑到指令之间的数据依赖性。

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个级程中使用的变量能有保证一致性是无法确定的,结果无法预测。

happens-before 原则

在 Java 中,happens-before 关系是一个同步保证,规定了在一个线程的操作必须在另一个线程的某些操作之前发生的条件。简而言之,如果 A 操作 happens-before B 操作,A 的执行结果对 B 可见(包括内存可见性),并且 A 的所有影响都会在 B 执行时生效。

例如 ,假设线程 A 对变量 x 赋值 1,线程 B 读取变量 x。只有在 A 对 x=1 的操作 happens-before B 的读取操作时,线程 B 才能读到正确的值 1

① 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序再第二个操作之前。

② 两个操作之间存在happens-before 关系,并不意味着一定要按照happens-before 原则规定的顺序来执行,如果重排序之后的结果是一致的,那么可以按照这种重排序来执行。

实现机制

JMM通过 volatile 关键字、synchronized 关键字和显式锁等机制来控制线程间的内存可见性和操作顺序。例如:

  • volatilevolatile 变量的读写操作都会直接访问主存,因此具有可见性。
  • synchronized:锁机制使得同一时刻只有一个线程可以访问同步块,从而保证了可见性和原子性。
  • 显式锁 :Java提供的 Lock 接口及其实现(如 ReentrantLock)可以精细地控制锁的获取和释放时机,以实现线程间的同步。

八个原则

  1. 次序规则:一个线程内,按照代码顺序,写在前面的操作会先与写在后面的操作执行。也就是前一个操作的结果可以被后续操作获取。
  2. 锁定规则:说白了就是线程 A 获取了锁进入执行代码后要先 unlock(释放锁),线程 B 才能够获取同一把锁去执行,这就是 A 先行与 B。
  3. volatile 变量规则:对一个 volatile 变量的写操作一定先于后面对这个变量的读操作。
  4. 传递规则:如果 A 操作先于 B 操作,而 B 操作又先发生于 C 操作,则可得 A 操作先于 C 操作。
  5. 线程启动规则:
    1. 这个规则就是如上面的例子,这个 start()的动作要先于对 t1 的其他动作。如果没有 start 这个方法的执行,也就不会有里面这些内容的执行。
  6. 线程中断规则:对线程interrupt()的调用要先于被中断线程的代码检测到中断事件的发生。
    1. 通过isInterrupted()检测是否中断,但是再次之前一定要有interrupt()去标记中断。
  7. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isalive()等手段检测线程是否已经终止执行。
  8. 对象的终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

finalize() 是 Java 中的一个方法,用于在垃圾回收之前对对象进行清理。它属于 java.lang.Object 类,每个类都可以覆盖它。然而,随着 Java 的发展,finalize() 已被逐步废弃,不推荐在现代 Java 中使用。

finalize() 方法会在垃圾回收器准备回收对象时调用,它的设计初衷是允许对象在销毁前进行一些清理工作,比如释放外部资源或关闭文件流。我们可以通过覆盖这个方法来定义对象被回收之前的清理操作。

finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作。

JMM与指令重排序

为了优化性能,编译器、JVM和CPU可能会对指令进行重排序,JMM允许在单线程中这种重排序只要不改变程序的执行结果。但在多线程环境中,JMM通过happens-before等规则限制重排序,以避免对程序结果产生不可预期的影响。

总结

Java内存模型(JMM)是确保Java程序在多线程环境下正确性和一致性的关键。通过定义线程间如何通过内存进行交互,JMM确保了共享变量的可见性和有序性,从而使得Java程序能够在不同的硬件和操作系统平台上保持一致的内存访问行为。JMM的三大核心特性------可见性、原子性和有序性------是并发编程中不可或缺的概念。通过理解这些特性以及JMM提供的happens-before原则,开发者可以编写出既高效又安全的多线程程序。

在实际应用中,JMM通过volatile、synchronized和显式锁等机制来实现内存可见性和操作顺序的控制。这些机制不仅有助于防止指令重排序带来的问题,还能够确保在多线程环境中数据的一致性和线程间的同步。随着Java语言和虚拟机技术的不断发展,JMM也在不断进化,以适应新的编程模式和硬件特性。因此,对于Java开发者来说,深入理解JMM并掌握其应用是提高编程技能和编写高质量并发程序的重要一步。


转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人

持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

谢谢支持!

相关推荐
安之若素^8 分钟前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan9914 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc41 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野7 小时前
【Java|集合类】list遍历的6种方式
java·python·list