当前位置:首页 > 技术学院 > 技术前线
[导读]在多核处理器成为计算设备标配的今天,并发编程已经成为开发者必须掌握的核心技能。它能充分发挥硬件性能,提升程序运行效率,但同时也带来了诸多复杂问题,其中内存模型是理解并发编程底层逻辑、解决线程安全问题的关键所在。

在多核处理器成为计算设备标配的今天,并发编程已经成为开发者必须掌握的核心技能。它能充分发挥硬件性能,提升程序运行效率,但同时也带来了诸多复杂问题,其中内存模型是理解并发编程底层逻辑、解决线程安全问题的关键所在。

一、内存模型的基本概念与意义

内存模型是计算机系统中描述线程间通信和同步的抽象结构,它定义了线程如何访问和操作共享变量的规则。在并发编程场景下,多个线程同时执行,共享变量的读写操作如果缺乏规范,极易引发数据不一致、竞态条件等问题,而内存模型正是为解决这些问题而生。

以Java语言为例,Java内存模型(Java Memory Model,JMM)是Java虚拟机规范中定义的一种抽象机制,它围绕原子性、可见性和有序性三大特性,规范了多线程环境下共享变量的访问规则。理解内存模型,是编写高效且正确的并发程序的基础,能帮助开发者从根源上避免并发BUG,保障程序的稳定性和可靠性。

二、主内存与工作内存:数据存储的双层次结构

在JMM的定义中,所有变量都存储在主内存中,主内存是所有线程共享的内存区域,由物理内存构成,用于存储程序的代码和数据。而每个线程拥有自己独立的工作内存,工作内存类似于CPU的高速缓存,保存了该线程使用到的变量的主内存副本。

线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量。线程间变量值的传递也必须通过主内存完成:当线程需要读取变量时,先从主内存获取变量的副本到工作内存;修改变量后,再将新值同步回主内存。这种结构的设计初衷是为了提升性能,因为主内存的访问速度相对较慢,无法与处理器的速度匹配,工作内存的存在能减少处理器与主内存的交互次数。

但这种双层次结构也带来了缓存一致性问题。当多个线程的工作内存中存在同一主内存变量的副本时,如果某个线程修改了副本值但未及时同步回主内存,其他线程读取的就会是过期数据,从而导致数据不一致。为解决这一问题,处理器通常会采用缓存一致性协议,如MESI(修改、独占、共享、无效)协议,来保证多个缓存之间的数据一致性。

三、内存间的交互操作:原子性的保障

JMM定义了8种原子操作来实现主内存与工作内存之间的数据交互,这些操作是不可分割的最小单元,共同保障了变量操作的原子性:

read(读取):将主内存中的变量值传输到工作内存。

load(加载):将read操作获取的值放入工作内存的变量副本中。

use(使用):将工作内存中的变量值传递给线程的执行引擎,供计算使用。

assign(赋值):接收执行引擎的赋值操作,更新工作内存中的变量副本值。

store(存储):将工作内存中的变量值传送到主内存。

write(写入):将store操作传送的值写入主内存的变量中。

lock(锁定):使主内存中的变量被一个线程独占,其他线程无法访问。

unlock(解锁):释放对主内存变量的锁定,允许其他线程访问。

这些操作必须遵循一定的规则,例如不允许read和load、store和write操作单独出现,即读取变量时必须将值加载到工作内存,存储变量时必须将值写入主内存。通过这些原子操作,JMM确保了单个变量的读写操作是不可分割的,要么完全完成,要么完全不执行,避免了中间状态的出现。

四、内存模型的三大特性:原子性、可见性与有序性

(一)原子性

原子性是指一个操作是不可中断的,在执行过程中不会被其他线程干扰。除了JMM定义的8种原子操作外,Java中的基本数据类型(如int、boolean等)的读写操作本身具有原子性,但像i++这样的复合操作,实际上包含了读取、加一、赋值三个步骤,并不具备原子性,需要通过同步机制(如锁、原子类)来保证其原子性。

(二)可见性

可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。在没有同步机制的情况下,由于工作内存的存在,线程修改的变量值可能只存在于自己的工作内存中,未及时同步回主内存,导致其他线程读取到的是旧值。

volatile关键字是实现可见性的重要手段之一。当一个变量被声明为volatile时,JVM会确保该变量的修改立即写回主内存,并且使其他线程工作内存中的该变量副本失效,从而让其他线程读取时必须从主内存获取最新值。此外,synchronized关键字和final关键字也能保证可见性:synchronized通过锁定和解锁操作,确保线程在解锁前将变量值同步回主内存;final关键字修饰的变量一旦初始化完成,其他线程就能看到其值。

(三)有序性

有序性是指程序执行的顺序与代码的顺序一致。但在实际运行中,编译器和处理器为了提升性能,可能会对指令进行重排序,这在单线程环境下不会影响程序结果,但在多线程环境下可能导致执行结果与预期不符。

JMM通过happens-before(先行发生)规则来保证有序性,它定义了操作之间的偏序关系:

程序顺序规则:同一线程内,按照代码顺序,前面的操作先行发生于后续的操作。

监视器锁规则:对同一锁的解锁操作先行发生于后续的加锁操作。

volatile变量规则:对volatile变量的写操作先行发生于后续的读操作。

传递性规则:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

此外,volatile关键字还能禁止指令重排序,JVM会通过插入内存屏障来防止编译器和处理器对volatile读写操作前后的指令进行重排序,从而保证程序执行顺序与代码顺序一致。

五、内存屏障与先行发生原则:底层保障机制

(一)内存屏障

内存屏障是一种CPU指令,用于控制内存操作的执行顺序,避免指令重排序。它主要分为四类:

LoadLoad屏障:确保在该屏障之后的读操作之前,所有之前的读操作已经完成。

StoreStore屏障:确保在该屏障之后的写操作之前,所有之前的写操作已经完成并刷新到主内存。

LoadStore屏障:确保在该屏障之后的写操作之前,所有之前的读操作已经完成。

StoreLoad屏障:确保在该屏障之后的读操作之前,所有之前的写操作已经完成并刷新到主内存,这是功能最强大的内存屏障。

JMM会在volatile变量的读写操作前后插入相应的内存屏障,从而保证可见性和有序性。例如,在volatile变量的写操作后插入StoreStore屏障和StoreLoad屏障,确保写操作立即刷新到主内存,并且后续的读操作能获取到最新值。

(二)先行发生原则

先行发生原则是JMM中判断线程操作是否存在竞争、是否需要同步的重要依据。它为开发者提供了一种无需深入了解底层实现,就能判断操作是否安全的方法。只要符合先行发生规则,就能保证操作的有序性和可见性,无需额外的同步措施。

比如在使用volatile变量的场景中,线程对volatile变量的写操作先行发生于后续的读操作,这就保证了当一个线程读取到volatile变量的新值时,之前对该变量的修改已经完成并可见。

六、实战应用:如何利用内存模型保证线程安全

在实际开发中,开发者可以根据内存模型的特性和规则,采取多种手段来保证线程安全:

使用volatile关键字:适用于变量被多个线程读取,但只有一个线程修改的场景,能保证可见性和有序性,但无法保证复合操作的原子性。

使用synchronized关键字:通过锁定对象,保证同一时间只有一个线程执行同步代码块,能同时保证原子性、可见性和有序性,但性能开销相对较大。

使用原子类:如AtomicInteger、AtomicBoolean等,这些类利用CAS(Compare And Swap)操作实现了原子性,性能优于synchronized。

合理设计线程间通信:通过wait()、notify()、notifyAll()等方法结合synchronized关键字,实现线程间的协作,确保操作的有序性。

例如,在一个多线程共享计数器的场景中,如果使用普通的int变量,可能会因为多个线程同时执行i++操作导致计数错误。此时可以使用AtomicInteger类,它的incrementAndGet()方法能保证原子性,避免数据不一致问题;也可以使用synchronized关键字包裹i++操作,确保同一时间只有一个线程执行该操作。

七、总结

内存模型是并发编程的核心基础,它定义了线程间共享变量的访问规则,通过原子性、可见性和有序性三大特性,以及内存屏障、先行发生原则等底层机制,保障了多线程程序的正确执行。深入理解内存模型,能帮助开发者从根源上解决并发编程中的线程安全问题,编写出高效、稳定的并发程序。在实际开发中,开发者需要根据具体场景,合理运用volatile、synchronized、原子类等同步手段,充分利用内存模型的特性,提升程序的性能和可靠性。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

大家好,我是闪客,感谢喵哥提供的平台让我在这里给大家介绍自己,这是我的公众号卡片。为了防止大家看到这里就点击了返回按钮,我先放一张图勾引一下您。这是我公众号做的第一张动图,好多读者当时说被这张图的魔性所吸引。这个动图,来...

关键字: 并发编程

↓推荐关注↓为什么要并发编程大型的软件项目常常包含非常多的任务需要处理。例如:对于大量数据的数据流处理,或者是包含复杂GUI界面的应用程序。如果将所有的任务都以串行的方式执行,则整个系统的效率将会非常低下,应用程序的用户...

关键字: 并发编程

↓推荐关注↓为什么要并发编程大型的软件项目常常包含非常多的任务需要处理。例如:对于大量数据的数据流处理,或者是包含复杂GUI界面的应用程序。如果将所有的任务都以串行的方式执行,则整个系统的效率将会非常低下,应用程序的用户...

关键字: 并发编程

并发编程学什么? 针对小伙伴们的疑问,今天,我就将并发编程需要学习的知识汇总成下图所示,希望能够为小伙伴们带来实质性的帮助。 特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下: 长按订阅更多精彩▼...

关键字: 嵌入式 并发编程

在计算机最早期的时候,没有操作系统,执行程序只需要一种方式,那就是从头到尾依次执行。任何资源都会为这个程序服务,在计算机使用某些资源时,其他资源就会空闲,就会存在 浪费资源 的情况。

关键字: 并发编程

来自:冰河技术 写在前面 最近正在写【高并发专题】的文章,其中,在【高并发专题】中,有不少是分析源码的文章,很多读者留言说阅读源码比较枯燥!问我程序员会使用框架了,会进行CRUD了,是否真的有必要阅读框架源码?! 对于这...

关键字: 源码 代码 程序员 并发编程

背景 C/C++语言的并发程序(Concurrent Programming)设计,一直是一个比较困难的话题。很多朋友都会尝试使用多线程编程,但是却很难保证自己所写的多线程程序的正确性。多线程程序,如

关键字: 多线程 并发编程
关闭