Java 并发编程:多线程并发内存模型

简介: 首发公众号:码农架构

多任务处理在现代计算机操作系统中几乎已是一项必备的功能了。在许多情况下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,还有一个很重要的原因是计算机的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O、网络通信或者数据库访问上。如果不希望处理器在大部分时间里都处于等待其他资源的状态,就必须使用一些手段去把处理器的运算能力“压榨”出来,否则就会造成很大的浪费,而让计算机同时处理几项任务则是最容易想到、也被证明是非常有效的“压榨”手段
image.png

关于可见性

在多核多线程环境中,为了提升运行时数据访问性能,经常会使用多层缓存策略。这种架构也就带来了共享变量可见性问题,每个核或线程都有自己的本地副本,对于共享变量的读写操作可能不会被其它核或线程知晓,也就是不可见了。

## Java内存模型

Java的世界也有属于它自己的内存模型,Java内存模型(Java Memory Model),简称JMM。由于Java被定义成一种跨平台的语言,所以在内存的描述上面也要能是跨平台的,Java虚拟机试图定义一种统一的内存模型,能将各种底层硬件及操作系统的内存访问差异进行封装,使Java程序在不同硬件及操作系统上都能达到相同的并发效果。它描述了程序中各个变量之间的关系,包括实例域、静态域、数据元素及在实际计算机系统中将变量存储到内存和从内存中取出变量的底层细节。
image.png

为更好理解JMM的工作机制,我们通过下图进行理解。从整体上看有几个比较重要的概念:主内存、工作(本地)内存、共享变量、共享变量副本、线程等。首先看主内存与工作内存及他们的关系,主内存保存了Java程序的所有变量,当然这个变量不包括局部变量和方法参数。工作内存则包含了这些共享变量的副本。其次是线程与工作内存的关系,每个线程都有一个属于自己的工作内存,不同线程之间的工作内存是互相不可见的,且线程对变量的操作也只能是针对自己的工作内存。最后是关于线程之间的通信机制,线程之间不可直接传递。假如某个线程对一个变量进行了重新赋值,那么该如何让另一个条线程知道呢?线程A将变量改变反应到主存中,线程B再从主存中读取,这样才能完成线程之间的通信。

JMM主要操作

JMM定义了八个主要的内存操作来完成工作内存与主存的通信。假如一条线程准备对一个变量进行新的赋值操作,它可能会先用lock操作锁住主内存中的某个变量,不让其他线程获得此变量的锁,直至使用unlock操作释放该变量的锁。接着使用read操作将变量从主存读到工作内存中,紧接着load操作将得到的变量值放到工作内存中的变量副本。use操作则将变量值传给线程执行引擎进行运算操作,assign操作把新的变量值从线程执行引擎中传递到工作内存。store操作则把变量值从工作内存传送到主存中,接着write操作将得到的值写入主存相应的变量中,最后使用unlock操作释放变量的锁。

### JMM可见性
在Java内存模型中,如果一个线程更改了共享变量的值,其他线程能马上知道这个更改,则我们说这个变量具有可见性。一般来说有四种方式能保证变量的可见性,分别为volatile、synchronized、final和锁。

首先谈谈volatile,被此关键词声明的变量,每当有任何更改时都将立即同步到主存中,而每个线程要使用这个变量时都要重新从主存刷新到工作内存,这样就确保了变量的可见性。当然,普通变量最终也会同步到主存,再由主存同步到每个线程的工作内存,只是这个最终可能比较“长久”,不能保证可见性。

其次,关于synchronized,由于synchronized底层也是通过锁进行实现,所以synchronized和锁的本质是一样的。当一个线程释放一个锁时,将会强制刷新工作内存中的变量值到主存中。而当另一个线程获取此锁的时候将会强制重新装载此变量值。当然这两个线程获取的是同一个锁,这样就保证了变量的可见性。

最后,被final声明的变量一旦完成初始化,其他线程就能看到这个final变量。其实,可见性其实可以看成是一种机制,线程在进入/退出同步块程序时,它将发送/接收一个变量的更改。

JMM有序性

有序性指在线程内看方法的执行,所有的指令都是有序的,都按照一种串行方式执行。而在线程内观察其他线程,所有指令都是无序的,指令都可能交叉执行。Java中提供了volatile和synchronized两个关键词保证线程之间操作的有序性,而这个有序性仅仅是相对的,volatile禁止指令重排序,synchronized则保证持有同一个锁的同步块只能串行运行。

JMM原子性

Java内存模型保证了read、load、assign、use、store、write等操作具有原子性,我们可以认为除了long和double类型外,对其他基本数据类型所对应的内存单元的访问读写都是原子的。但由于这个原子性的颗粒度太小,通常情况下我们需要更大颗粒度的原子性,这时就需要用锁来保证了。
image.png

## 总结
JMM可以说是Java的基础,也是Java多线程的基础,它的定义将直接影响JVM及Java多线程实现的机制。要想深入了解多线程并发中的相关问题现象,对Java内存模型的深入研究是必不可少的。它的定义必须考虑下面几个方面,其一是如何更加有效地提高线程的性能效率;其二是如何将底层物理硬件及操作系统的差异屏蔽掉提供统一的对外概念;最后是如何使它的模型既严谨又宽松,保证语义不会产生歧义和一些优化扩展。

Java 并发编程

相关文章
|
1月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
128 6
|
29天前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
158 0
|
2月前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
448 0
|
1月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
140 0
|
1月前
|
设计模式 缓存 安全
【JUC】(6)带你了解共享模型之 享元和不可变 模型并初步带你了解并发工具 线程池Pool,文章内还有饥饿问题、设计模式之工作线程的解决于实现
JUC专栏第六篇,本文带你了解两个共享模型:享元和不可变 模型,并初步带你了解并发工具 线程池Pool,文章中还有解决饥饿问题、设计模式之工作线程的实现
121 2
|
22天前
|
Java 调度 数据库
Python threading模块:多线程编程的实战指南
本文深入讲解Python多线程编程,涵盖threading模块的核心用法:线程创建、生命周期、同步机制(锁、信号量、条件变量)、线程通信(队列)、守护线程与线程池应用。结合实战案例,如多线程下载器,帮助开发者提升程序并发性能,适用于I/O密集型任务处理。
182 0
|
4月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
1387 0
|
4月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
411 0
|
4月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
385 0