深入浅出Java多线程(六):Java内存模型
引言
大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第六篇内容:Java内存模型。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!
在并发编程中,有两个关键问题至关重要,它们是线程间通信机制和线程间同步控制。
线程间通信机制
线程间通信是指在一个多线程程序中,不同线程之间如何有效地交换信息。在Java内存模型(JMM)采用的共享内存并发模型中,线程间的通信主要是通过共享变量来实现的。每个线程可以读取或修改这些存储在堆内存中的共享变量,从而传递状态或数据给其他线程。例如:
class SharedData {public volatile int sharedValue;
}public class ThreadCommunication {public static void main(String[] args) {SharedData data = new SharedData();Thread threadA = new Thread(() -> {data.sharedValue = 10; // 线程A更新共享变量});Thread threadB = new Thread(() -> {while (data.sharedValue == 0) {} // 线程B等待共享变量被更新System.out.println("Thread B sees updated value: " + data.sharedValue);});threadA.start();threadB.start();try {threadA.join(); // 确保线程A完成更新} catch (InterruptedException e) {e.printStackTrace();}}
}
在这个案例中,sharedValue
是一个共享变量,线程A对其进行了修改,而线程B则依赖于该变量的值进行后续操作。为了确保线程间通信的正确性,这里使用了 volatile
关键字来保证变量的可见性和禁止指令重排序。
线程间同步控制
线程间同步则是指控制不同线程间操作发生的相对顺序,以避免数据不一致和竞态条件等问题。在Java中,同步控制主要通过以下方式实现:
- synchronized关键字:它可以修饰方法或代码块,确保同一时间只有一个线程能访问被保护的资源。如下所示:
class Counter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;}
}
在这个例子中,increment()
和 getCount()
方法都被 synchronized
修饰,这样在同一时刻只能有一个线程执行这两个方法之一,防止了并发环境下计数器值的错误累加。
- Lock接口及其实现类:除了内置的
synchronized
机制,Java还提供了更灵活的Lock接口如ReentrantLock,它允许更多的同步语义,比如尝试获取锁、可中断获取锁以及公平锁等。 - volatile关键字:虽然主要用于提供内存可见性,但其也能间接起到一定的同步作用,即当一个线程修改了volatile变量时,其他线程能够立即看到这个新值。
综上所述,在Java并发编程中,线程间通信与同步控制相辅相成,共同构建了一个安全高效的并发环境。通过合理地利用Java内存模型提供的机制,开发者可以确保在多线程环境中,各个线程之间的数据交换有序且可靠。
并发模型对比
在并发编程领域,有两种主要的并发模型:消息传递并发模型和共享内存并发模型。Java多线程编程采用了共享内存并发模型,这一选择对理解Java内存模型(JMM)至关重要。

消息传递并发模型
消息传递模型中,线程之间的通信和同步是通过发送和接收消息来实现的。每个线程拥有独立的本地状态,并通过将数据封装在消息中发送给其他线程来交换信息。在这种模型下,线程之间不直接共享数据,因此不存在竞争条件或同步问题。Erlang等语言中的Actor模型就是一个典型的消息传递并发模型实例。
-module(my_actor).
-export([start_link/0, ping/0]).start_link() ->register(actor_name, spawn(fun() -> loop([]) end)).ping() ->actor_name ! {self(), ping}.loop(Msgs) ->receive{From, ping} ->From ! pong,loop(Msgs);_Other ->loop([Msg | Msgs])end.
在此Erlang示例中,actor_name
是一个进程(即线程),它通过接收并响应消息来进行工作,而不是直接读写共享变量。
共享内存并发模型
而在Java中采用的共享内存模型,则允许线程访问相同的内存区域——堆区,其中包含的共享变量可以被多个线程同时读写。这种模型下,线程间通信是通过对共享变量进行读写操作间接完成的。然而,由于共享数据,这就带来了潜在的数据一致性问题,如竞态条件、死锁以及可见性问题。为了保证线程间的正确交互,Java内存模型定义了一套规则和机制。
public class SharedCounter {private volatile int count = 0;public void increment() {count++;}public int getCount() {return count;}
}public class Main {public static void main(String[] args) throws InterruptedException {SharedCounter counter = new SharedCounter();Thread threadA = new Thread(() -> {for (int i = 0; i < 100000; i++) {counter.increment();}});Thread threadB = new Thread(() -> {while (true) {System.out.println(counter.getCount());}});threadA.start();threadB.start();threadA.join();}
}
在这个Java示例中,两个线程同时访问SharedCounter
类的共享变量count
,为了确保线程安全和可见性,我们使用了volatile
关键字修饰该变量。Java内存模型通过主内存与各线程私有本地内存间的抽象关系以及内存屏障技术,保障了线程间共享变量的更新能够及时传播到所有线程。
Java选择共享内存并发模型的原因在于其简洁性和高效性,尤其是对于基于对象和引用透明性的程序设计而言。尽管存在潜在的并发问题,但通过提供诸如synchronized
、volatile
以及更高层次的并发工具如java.util.concurrent
包下的各种锁机制和原子类等,Java提供了丰富的工具来管理和控制共享内存环境下的并发行为,使得开发者能够编写出高效的并发代码。
Java内存模型抽象结构解析
运行时数据区划分

在Java虚拟机(JVM)的运行时环境中,内存被划分为多个区域以支持程序的执行。其中,线程私有的内存区域包括程序计数器、虚拟机栈以及本地方法栈,而堆和方法区则是所有线程共享的内存区域。
- 栈:每个线程都有自己的栈空间,用于存储局部变量、方法调用时的上下文信息(如返回地址、临时变量等)。由于栈是线程私有的,因此线程间不涉及共享和可见性问题。
public class StackExample {public void localVariableVisibility() {int localVar = 10; // 局部变量存储在线程栈中,对其他线程不可见}
}
- 堆:堆内存是所有线程共享的区域,主要存储对象实例及数组。当创建一个类的对象时,其对象实例就分配在堆内存上,这些实例变量对所有能够访问到该对象的线程都是可见的。
堆内存中的内存不可见性原因
现代计算机系统为了提高性能,普遍采用了高速缓存技术,CPU有自己的缓存层级,包括L1、L2、L3等高速缓存。当线程A修改了堆内存中的共享变量时,这个更新可能只反映在了线程A所在的CPU缓存中,而不是立即同步到主内存或其他线程所在的CPU缓存中。这就是为什么即使是在共享内存区域——堆内存在多线程环境下也可能出现内存不可见性的问题。
public class CacheCoherenceIssue {private static volatile int sharedValue = 0;public static void main(String[] args) throws InterruptedException {Thread threadA = new Thread(() -> {for (int i = 0; i < 10000; i++) {sharedValue++;}});Thread threadB = new Thread(() -> {while (sharedValue == 0); // 如果没有volatile,可能会陷入循环无法退出System.out.println("Thread B observed: " + sharedValue);});threadA.start();threadB.start();threadA.join();threadB.join();}
}
上述代码中,如果没有使用volatile
关键字修饰sharedValue
,线程B可能无法观察到线程A对共享变量的更新,因为这种更新可能未及时传播至主内存或线程B的工作内存。
Java内存模型(JMM)详解
Java内存模型(JMM)是一种抽象概念,它定义了Java程序中各种变量的访问规则,尤其是针对堆内存中的共享变量。JMM确保了并发环境下的原子性、有序性和可见性:
- 主内存与本地内存的关系:根据JMM的描述,所有共享变量都存储于主内存中,每个线程具有自己的本地内存,保存了该线程读写共享变量的副本。线程间的通信必须通过主内存进行,即线程先将本地内存中的共享变量刷新回主内存,然后其他线程从主内存读取最新值并更新到自己本地内存的过程。
- 内存操作的顺序保证:JMM通过内存屏障来控制指令重排序,从而确保特定操作的顺序性,比如volatile变量的写后读操作不会被重排序。
- 内存可见性的实现:JMM提供了一系列规则和机制来保证不同线程对共享变量修改的可见性,例如volatile变量的写会强制刷入主内存,并使其他线程对该变量的读失效,进而从主内存重新加载。
综上所述,Java内存模型在多线程编程中扮演着核心角色,通过规范和约束线程如何访问和更新共享变量,有效地解决了并发环境下的内存一致性问题。
Java内存模型与Java内存区域的关系
两者区别
Java内存模型(JMM)和Java运行时内存区域是两个不同的概念层次,它们在描述并发编程的内存行为时有着各自的侧重点:
- Java内存模型(JMM):从抽象层面定义了线程之间如何通过主内存进行交互以及如何保证数据的一致性和有序性。JMM关注的是对共享变量访问规则的规范,比如可见性、原子性和有序性,它是一组关于程序中所有变量访问操作的协议或约定。
- Java运行时内存区域:这是更为具体的概念,指Java虚拟机(JVM)在运行Java程序时实际划分的内存区域,包括堆、栈、方法区、程序计数器等。这些内存区域分别存储着对象实例、局部变量、类信息、线程上下文等不同类型的内存数据,并且各区域具有不同的生命周期和管理策略。
联系与映射
尽管JMM与Java运行时内存区域在概念上有所差异,但它们之间存在着密切的联系和映射关系:
- 主内存与共享数据区域:
在JMM中,主内存对应于Java运行时内存区域中的堆和方法区。堆存放了Java对象实例,即多线程可以共享的对象数据;而方法区则包含了类的元数据和静态变量等,这些也是全局可访问的信息,因此它们都属于“主内存”的范畴。 - 本地内存与私有数据区域:
JMM中的本地内存实际上是一个抽象概念,涵盖了缓存、写缓冲区、寄存器等硬件设施,对应到Java运行时内存区域,可以理解为每个线程的私有工作空间,如程序计数器、虚拟机栈和本地方法栈。其中,虚拟机栈保存了方法调用的局部变量表,以及操作数栈等信息,这些都是严格线程私有的,符合本地内存的概念。
虽然无法直接以代码形式展示这种抽象的映射关系,但在实际编程中,我们可以观察到以下现象:
public class MemoryModelMapping {private static int sharedValue; // 存储在堆中,属于主内存区域private int threadLocalValue; // 存储在线程栈中,属于本地内存public void runInParallel() {Thread threadA = new Thread(() -> {sharedValue = 10; // 修改共享变量threadLocalValue = 20; // 修改线程局部变量});Thread threadB = new Thread(() -> {while (sharedValue == 0) {} // 等待共享变量更新System.out.println("Shared value: " + sharedValue);});threadA.start();threadB.start();try {threadA.join();threadB.join();} catch (InterruptedException e) {e.printStackTrace();}}
}
在这个示例中,sharedValue
变量由于被多个线程共享,它的修改需要遵循JMM的同步和可见性规则,而 threadLocalValue
变量仅在线程内部使用,不受JMM的跨线程可见性约束,其生命周期完全受限于所在线程的虚拟机栈范围。这样,我们便能直观地感受到JMM与Java运行时内存区域之间的关联和作用机制。
Java语言特性与JMM实现
volatile关键字的作用
在Java并发编程中,volatile关键字是一个重要的工具,它用于修饰共享变量,确保了该变量在多线程环境下的可见性和禁止指令重排序。当一个线程修改了volatile变量的值时,其他线程能够立即看到这个更新后的值,这是因为volatile变量的读写操作都会与主内存直接交互,并且会在必要时插入内存屏障以保证数据的一致性。
public class VolatileExample {private volatile int sharedValue = 0;public void increment() {sharedValue++;}public int getSharedValue() {return sharedValue;}public static void main(String[] args) throws InterruptedException {VolatileExample example = new VolatileExample();Thread threadA = new Thread(example::increment);Thread threadB = new Thread(() -> System.out.println("Thread B sees: " + example.getSharedValue()));threadA.start();threadA.join(); // 确保线程A完成操作threadB.start();}
}
在这个例子中,sharedValue
是一个被volatile修饰的变量,线程A对其进行了递增操作,而线程B可以立即获取到最新的值,体现了volatile对于共享状态同步的重要作用。
synchronized关键字的功能
synchronized
关键字提供了原子性和可见性保障,它可以应用于方法或代码块,使得在同一时间只有一个线程能访问被保护的资源,从而有效地解决了竞态条件和数据一致性问题。
public class SynchronizedExample {private int counter = 0;public synchronized void incrementCounter() {counter++;}public synchronized int getCount() {return counter;}public static void main(String[] args) {SynchronizedExample example = new SynchronizedExample();Thread threadA = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.incrementCounter();}});Thread threadB = new Thread(() -> {while (true) {if (example.getCount() >= 1000) {System.out.println("Counter reached 1000");break;}}});threadA.start();threadB.start();try {threadA.join();threadB.join();} catch (InterruptedException e) {e.printStackTrace();}}
}
在此示例中,synchronized
方法 incrementCounter()
和 getCount()
保证了计数器的增量操作是原子性的,同时多个线程对counter的读写操作不会出现竞态条件,即线程B总能看到线程A对counter修改的最新结果。
内存屏障与happens-before原则
为了更深入地理解并发控制机制,Java内存模型还引入了内存屏障(Memory Barrier)的概念,这是一种硬件级别的指令,用于确保特定内存操作顺序并刷新缓存。Java编译器会根据JMM规则,在适当的时机插入内存屏障,以实现对volatile变量和其他同步原语的正确支持。
另外,Java内存模型通过happens-before原则来简化程序员理解和推理程序行为。它定义了一系列先行发生关系,比如:程序次序规则、监视器锁规则等,这些规则明确了事件之间的执行顺序,如果A happens-before B,那么线程A对共享变量的修改对于线程B来说一定可见。
例如:
public class HappensBeforeExample {private static boolean flag = false;private static int data = 0;public static void main(String[] args) throws InterruptedException {Thread one = new Thread(() -> {data = 1; // 修改数据flag = true; // 设置标志位});one.start();one.join();// 根据happens-before原则,由于监视器锁规则// 当进入同步块时,线程将看到之前对flag的修改synchronized (HappensBeforeExample.class) {if (flag) {System.out.println("Data seen in other thread: " + data); // 输出正确的值}}}
}
在这个例子中,因为synchronized
关键字遵循happens-before原则中的监视器锁规则,因此主线程在进入同步块时,可以看到之前线程one对flag
的修改,进而确定data
变量是否已经被正确设置。

喜欢的朋友记得点赞、收藏、关注哦!!!
相关文章:

深入浅出Java多线程(六):Java内存模型
引言 大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第六篇内容:Java内存模型。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!! 在并发编程中…...
注册了个小趴菜999#it#com
注册了个 999#it#com 拿着玩玩吧 现在二级域名竟然也让注册了 不过cn.com的二级似乎早就可以了...

UE4 材质学习笔记02(数据类型/扭曲着色器)
一.什么是数据类型 首先为啥理解数据类型是很重要的。一些节点的接口插槽只接受特定类型的数据,如果连接了不匹配的数据就会出现错误,有些接口可以接受任何数据类型,但是实际上只会使用到其中的一些。并且有时可以将多个数据流合并成一个来编…...

Linux驱动开发(速记版)--设备树插件
第六十八章 设备树插件介绍 Linux 4.4之后引入了动态设备树,其中的设备树插件(Device Tree Overlay)是一种扩展机制,允许在运行时动态添加、修改或删除设备节点和属性。 设备树插件机制通过DTS(设备树源文件࿰…...
代码报错后如何定位问题
文章目录 一、查看终端报错Exception二、百度三、问 一、查看终端报错Exception 代码报错时,终端一般都会有xxxException异常提示,或者exception、error…等字样提示,就顺着这些关键字提醒找到异常即可。 二、百度 不知道这个英文的异常是…...

Python数据可视化--Matplotlib--入门
我生性自由散漫,不喜欢拘束。我谁也不爱,谁也不恨。我没有欺骗这个,追求那个;没有把这个取笑,那个玩弄。我有自己的消遣。 -- 塞万提斯 《堂吉诃德》 Matplotlib介绍 1. Matplotlib 是 Python 中常用的 2D 绘图库&a…...
美国食品等级FDA认证测试介绍
美国FDA认证概览 美国食品和药物管理局(FDA)是负责监管食品、药品、医疗设备和化妆品等的联邦机构,以确保这些产品对公众健康和安全的影响。FDA认证在美国属于强制性认证,对产品的安全性和质量有着严格的要求。通过FDA认证&#…...

Vue2如何在网页实现文字的逐个显现
目录 Blue留言: 效果图: 实现思路: 代码: 1、空字符串与需渲染的字符串的定义 2、vue的插值表达式 3、函数 4、mounted()函数调用 结语: Blue留言: 在国庆前夕,突发奇想,我想…...
mybatisplus的查询,分页查询,自定义多表查询,修改的几种写法
使用mybatisplus的Db类简化写法 使用静态调用的方式,执行CRUD方法,避免Spring环境下Service循环注入、简洁代码,提升效率需要项目中已注入对应实体的BaseMapper完整使用方式见官方测试用例:官方测试用例地址对于参数为Wrapper的&…...
括号匹配判断
本题实现求表达式中括号是否匹配。只需判断表达式中括号(本题中只会出现三种括号,分别是小括号,中括号和大括号)是否匹配,表达式中可以有其他值也可没有。 函数接口定义: int match (char *exp); 其中 …...

数据结构(栈和队列的实现)
1. 栈(Stack) 1.1 栈的概念与结构 栈是一种特殊的线性表,其只允许固定的一段插入和删除操作;进行数据插入和删除的一段叫做栈顶,另一端叫栈底;栈中的元素符合后进先出LIFO(Last In First Out&…...

Python批量处理客户明细表格数据,挖掘更大价值
批量处理 .xls 数据并进行归类分析以挖掘内在价值,通常涉及以下步骤: 读取数据:使用 pandas 库读取 .xls 文件。数据清洗:处理缺失值、异常值、重复值等。数据转换:对数据进行必要的转换,如日期格式统一、…...
NAND Flash虚拟层索引表机制
NAND Flash虚拟层的索引表用于建立逻辑块与数据块、日志块之间的关系,用于NAND Flash虚拟层在运行过程中的读写、擦除操作;由于NAND Flash虚拟层采用集中索引的方式,因此在NAND Flash虚拟层启动时需要在NAND Flash存放索引表区域扫描并确定NAND Flash中存…...

Spring Boot框架:新闻推荐系统开发新趋势
3系统分析 3.1可行性分析 通过对本新闻推荐系统实行的目的初步调查和分析,提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本新闻推荐系统采用JAVA作为开发语言,Spring Boot框…...
RK3568平台(opencv篇)opencv处理图像
一.颜色转换 cv2.cvtColor()函数功能: 将一幅图像从一个色彩空间转换到另一个色彩空间。 函数原型: cv2.cvtColor(src,code,dst=None,dstCn=None) 参数定义: src:要转换的源文件 code,转换的色彩空间,在 opencv 中有超过 150 种颜色空间转换方法,但是经常用的只有 B…...
【移动端】Viewport 视口
1. 什么是 Viewport(视口)? Viewport(视口)是指浏览器中用户可见的那部分网页内容的区域,简单来说,它是用户当前看到的网页的“窗口”区域。在不同的设备上,Viewport 的大小会有所不…...

PWM 模式
一、介绍 PWM(脉宽调制,Pulse-width modulation)是一种通过调节脉冲信号的宽度来控制电能输出的方法。PWM是一种方波信号,通常在电子和电气工程中用于调节功率输送,控制电机速度,调节LED亮度,以…...

模拟算法(3)_Z字形变换
个人主页:C忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C忠实粉丝 原创 模拟算法(3)_Z字形变换 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌 目录 1. 题目链…...
Go语言实现长连接并发框架 - 任务执行流路由模块
文章目录 前言接口结构体接口实现项目地址最后 前言 你好,我是醉墨居士,上篇博客中我们实现了任务执行流上下文部分,接下来我们实现一下任务执行流的路由模块,基于该模块可以实现将消息转发到相应注册的任务执行流中进行处理 接…...
Windows 编译 FFmpeg 源码详细教程
FFmpeg FFmpeg 是一个开源的多媒体框架,它包括了一整套工具和库,可以用来处理(转码、转换、录制、流式传输等)音频和视频。FFmpeg 支持广泛的音视频格式,并且可以在多种操作系统上运行,包括 Windows、Linux 和 macOS。 FFmpeg 的主要组件包括: ffmpeg:这是一个命令行工…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...

K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...

【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...

ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...

【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...
c# 局部函数 定义、功能与示例
C# 局部函数:定义、功能与示例 1. 定义与功能 局部函数(Local Function)是嵌套在另一个方法内部的私有方法,仅在包含它的方法内可见。 • 作用:封装仅用于当前方法的逻辑,避免污染类作用域,提升…...

图解JavaScript原型:原型链及其分析 | JavaScript图解
忽略该图的细节(如内存地址值没有用二进制) 以下是对该图进一步的理解和总结 1. JS 对象概念的辨析 对象是什么:保存在堆中一块区域,同时在栈中有一块区域保存其在堆中的地址(也就是我们通常说的该变量指向谁&…...

DeepSeek越强,Kimi越慌?
被DeepSeek吊打的Kimi,还有多少人在用? 去年,月之暗面创始人杨植麟别提有多风光了。90后清华学霸,国产大模型六小虎之一,手握十几亿美金的融资。旗下的AI助手Kimi烧钱如流水,单月光是投流就花费2个亿。 疯…...