Java并发编程系列-内存模型

本文详细介绍了Java内存模型的基础知识,包括CPU、Java内存结构的工作原理,以及并发编程中的原子性、可见性和有序性三个核心概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

Java并发编程系列-内存模型

1. CPU

2. Java内存结构

3. 工作原理

4. 并发编程的三个概念


1. CPU

CPU(Central Processing Unit),又称中央处理器,是由一块超大的集成电路组成,是一台计算机的运算和控制核心。它的主要功能是解释计算机指令和处理计算机软件中的数据。

中央处理器主要包括运算器(算数逻辑单元 ALU Arithmetic Logic Unit)和高速缓冲处理器(Cache)以及实现它们之间联系的数据、控制及状态的总线(Bus)。它与内部存储器(Memary)和输入输出(I/O)设备是计算机中比较重要的三部分。如图所示:

 

计算机在执行程序时,每条指令都是在CPU中进行的,在执行指令的过程中,会涉及数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)中,因为CPU的执行速度很快,而从内存读取数据和写入内存数据的过程跟CPU的执行速度比起来要慢很多,会大大降低指令的执行速度。因此,CPU里面就有了高速缓存。

2. Java内存结构

Java内存结构,即程序运行时的数据区域。

Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它管理的内存划分为几个不同的数据区域,这些区域有各自的用途、创建时间和销毁时间。

如图所示:

(备注:以下不是本文主要内容,不作深入探讨)

线程共享区

堆(Heap):堆是JVM管理内存中最大的一块,是被所有Java线程所共享的,不是线程安全的,在JVM启动时创建。堆是存储对象实例和数组的地方,是GC(垃圾回收)管理的主要区域。

方法区(Method Area):方法区是被Java线程所共享的,不像Java堆中其他部分一样会频繁被GC回收。它存储的信息相对稳定,在一定条件下会被GC回收,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemary的错误信息。方法区也是堆中的一部分,就是我们通常所说的Java堆中的永久区Permanet Generation,大小可以通过参数设置,-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、属性和方法信息。

常量池(Constant Pool):常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并且保存在.class文件中。一般分为两类:字面量和引用量。字面量如字符串、final变量等,引用量如类名和方法名等。

线程独享区

本地方法栈(Native Method Stack):本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

程序计数器(Program Counter Register):严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于ThreadLocal,是线程安全的。

Java栈(Java Stack):Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。

由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

 

3. 工作原理

1.引出问题

以代码为例,进行分析。

i = i + 1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。

比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

2.主内存和工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不同步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,因为后者是线程私有的,不会共享,当然不存在数据竞争问题(如果局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,但是reference引用本身在Java栈的局部变量表中,是线程私有的)。为了获得较高的执行效能,Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

 

4. 并发编程的三个概念

1.原子性

一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

例如

i = 10; //语句1

i++;//语句2

语句1是原子性操作,语句2不是原子性操作。

语句1直接将数值10赋值给i,即线程执行这个语句的会直接将数值10写入到工作内存中。语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

2.可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

对于可见性,Java提供了volatile关键字来保证可见性。(下节探讨内容,值得关注)

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3.有序性

程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

更多资源信息,请关注知识星球:java小密圈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值