并发编程之并发理论篇--内存模型

目录

一、Java内存模型的介绍

二、内存模型抽象结构

三、主内存与工作内存

四、内存间交互操作

五、内存模型三大特性

六、内存屏障

七、先行发生原则

八、代码示例


一、Java内存模型的介绍

线程安全是指在多个线程同时访问同一个对象时,无论线程调度和交替运行的方式如何,以及是否需要额外的同步或协调操作,该对象的行为都能够正确地获得预期的结果。

根据《深入理解Java虚拟机》所提供的定义,线程安全的对象可以保证在多线程环境下的正确性。这意味着对象的方法或操作可以被多个线程并发地调用,而不会导致数据的不一致性或产生竞态条件等问题。

线程安全问题通常由于主内存和工作内存之间的数据不一致性和指令重排序导致。主内存是所有线程共享的内存区域,用于存储对象的实例和变量等数据。而工作内存是每个线程私有的内存区域,用于存储对主内存中数据的副本。

  • 为了提高性能,编译器和处理器可能会对指令进行重排序,这可能导致在不同的线程中看到的指令执行顺序不一致。此外,在多线程环境下,线程之间相互协作需要进行通信,用来告知彼此的状态和当前的执行结果。
  • 为了解决线程安全问题,理解Java内存模型(JMM)至关重要。JMM定义了线程之间如何交互以及如何与主内存进行数据交互的规则。它提供了原子性、可见性和有序性等概念,以确保线程之间对共享变量的访问是正确、可见和有序的。

通过理解JMM的规则以及主内存和工作内存之间的交互机制,开发者可以采取适当的同步手段,如使用锁、volatile关键字、原子类等,来实现线程安全的程序设计,避免数据不一致性和竞态条件的问题。

二、内存模型抽象结构

内存模型是计算机系统中用来描述线程间通信和同步的抽象结构。在并发编程中,线程之间需要通过某种机制来进行通信和同步,而内存模型定义了线程如何访问和操作共享变量的规则。

共享变量是指在多个线程之间可以被访问和修改的变量。在Java程序中,所有实例域、静态域和数组元素都属于共享变量,它们存储在堆内存中,可以被所有线程访问到。而局部变量、方法定义参数和异常处理器参数等则不属于共享变量,它们是线程私有的,不会在线程间共享。

Java内存模型(Java Memory Model,JMM)是一种共享内存模型,它规定了线程如何与主内存和工作内存进行交互。每个线程都有自己的工作内存,其中包含了从主内存中读取的共享变量的副本。线程对共享变量的读写操作都是在工作内存中进行的,并在适当的时候将变量的值同步回主内存。

JMM定义了线程对共享变量的读写操作具有原子性、可见性和有序性这三个特征。原子性保证了对于单个共享变量的读写操作是不可分割的,要么完成,要么不完成,没有中间状态。可见性保证了一个线程对共享变量的修改对其他线程是可见的,即当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。有序性保证了程序执行的顺序与代码的顺序一致,即程序的执行结果是可以预测的。

为了解决线程间的通信和同步问题,JMM提供了一些机制,如锁和volatile关键字。锁机制可以控制不同线程之间对共享变量的访问顺序,从而实现线程间的同步。而volatile关键字可以保证对于每次对volatile变量的读写操作都能强制刷新到主内存,从而对所有线程都可见。

总之,内存模型是描述线程间通信和同步的抽象结构,Java内存模型是一种共享内存模型,定义了线程如何访问和操作共享变量,以及如何保证线程间通信和同步的正确性。

三、主内存与工作内存

主内存是计算机系统中存储所有变量的地方,它由物理内存构成,并存储程序的代码和数据。由于主内存的访问速度相对较慢,无法与处理器的速度保持一致。

为了解决速度矛盾问题,引入了高速缓存。高速缓存位于处理器内部,读写速度比主内存快得多。它用于缓存主内存中经常使用的数据和指令,以提高处理器的读写操作速度。

然而,引入高速缓存也带来了一个新问题,即缓存一致性。当多个缓存共享同一块主内存区域时,如果它们的缓存副本不一致,就会导致数据不一致的情况。因此,需要一些协议来解决这个问题,例如MESI(修改、独占、共享、无效)协议。

所有的变量都存储在主内存中,每个线程还有自己的工作内存。工作内存可以存储在高速缓存或寄存器中,保存了该线程使用的变量的主内存副本拷贝。

线程只能直接操作工作内存中的变量,对变量的读写操作都是在工作内存中进行的。如果线程需要与其他线程共享变量的值,需要通过主内存来进行变量值的传递。

当线程需要读取变量时,它首先从主内存中获取变量的副本到自己的工作内存中操作。修改后的值在合适的时机刷新回主内存,使其他线程能够获取到最新的值。

通过主内存和工作内存之间的数据交互以及缓存一致性协议的配合,可以保证多线程环境下对共享变量的操作的一致性和正确性。

四、内存间交互操作

主内存和工作内存之间进行数据交互的操作主要涉及变量的读取、修改和写回。下面是一些常见的内存间交互操作:

  • lock(锁定):作用于主内存中的变量,将一个变量标记为线程独占状态,确保只有一个线程可以访问该变量。
  • unlock(解锁):作用于主内存中的变量,释放一个被锁定的变量,使其他线程可以访问该变量。
  • read(读取):作用于主内存中的变量,从主内存中读取一个变量的值,并将其传输到线程的工作内存中。它为后续的 load 操作提供数据。
  • load(载入):作用于工作内存中的变量,将读取操作获取到的值放入线程的工作内存中的变量副本中。
  • use(使用):作用于工作内存中的变量,将工作内存中的变量值传递给执行引擎,在执行引擎中使用该值。
  • assign(赋值):作用于工作内存中的变量,将执行引擎接收到的值赋给工作内存中的变量。在遇到变量赋值指令时执行该操作。
  • store(存储):作用于工作内存中的变量,将工作内存中的变量值传输到主内存中,以便后续的 write 操作使用。
  • write(写操作):作用于工作内存中的变量,将store(存储)操作获取到的值放入主内存的变量中。

这些操作保证了在多线程环境中对变量的读写和操作的一致性和可见性。通过使用锁和内存屏障等机制,Java 内存模型确保了线程间的数据同步和正确的执行顺序,从而避免了由于多线程并发访问导致的数据不一致或错误的问题。

五、内存模型三大特性

Java内存模型(Java Memory Model,JMM)是一种规范,用于描述多线程程序中的内存访问和操作行为。它确保了原子性、可见性和有序性这三个重要的特性。

1、原子性:

原子性指的是一个操作要么全部执行完毕,要么完全不执行,不存在中间状态。在Java内存模型中,read、load、use、assign、store、write、lock和unlock等操作都具有原子性。但是对于64位数据(如long和double),虚拟机允许将其读写操作分为两次32位的操作,因此这些操作可能不具备原子性。

需要注意的是,int等原子类型的变量在多线程环境中也可能出现线程安全问题。例如,在多个线程对一个int类型变量进行自增操作时,由于自增操作不是原子操作(包含多个步骤:读取变量值、加一、写回变量),可能导致结果不正确。

2、可见性:

可见性指的是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性。可以使用volatile关键字、synchronized关键字或final字段来实现可见性。

使用volatile修饰的变量可以保证内存可见性,但并不能保证操作的原子性。对于保证变量的原子性,需要满足两个条件:运算结果不依赖于变量的当前值,或只有一个线程修改变量的值;变量不与其他状态变量共同参与不变约束。

3、有序性:

有序性指的是在一个线程内观察,所有操作都是有序的;但在多线程并发执行时,操作可能会被重排序。Java内存模型允许编译器和处理器对指令进行重排序,这不会影响单线程程序的执行,但可能影响多线程并发执行的正确性。

为了保证有序性,可以使用volatile关键字或synchronized关键字。volatile关键字通过添加内存屏障来禁止指令重排;synchronized关键字则保证每个时刻只有一个线程执行同步代码,从而实现顺序执行。

总之,Java内存模型通过原子性、可见性和有序性这三个特性来确保多线程程序的正确性和可靠性。在编写多线程程序时,需要合理地应用这些特性,避免出现线程安全问题。

总结

  • synchronized:具有原子性,有序性和可见性
  • volatile:具有有序性和可见性
  • final:具有可见性

六、内存屏障

在Java内存模型(JMM)中,为了保持多线程程序的正确性,JMM允许编译器和处理器对指令序列进行重排序,前提是不能改变程序的语义。然而,如果我们希望阻止重排序,可以添加内存屏障(也称作内存栅栏或内存栅障)。

JMM定义了四种类型的内存屏障:

  • LoadLoad屏障:禁止下面的普通读操作和上面的普通读操作重排序。确保上面的读操作先于下面的读操作。
  • StoreStore屏障:禁止上面的普通写操作和下面的普通写操作重排序。确保上面的写操作先于下面的写操作。
  • LoadStore屏障:禁止下面的普通写操作和上面的普通读操作重排序。确保上面的读操作先于下面的写操作。
  • StoreLoad屏障:是一个全能型屏障,它禁止了上面的普通写操作和下面的volatile读/写操作重排序。同时,它还保证了上面的所有数据对其他处理器可见,避免了内存可见性问题。

这些内存屏障通过在指令序列中插入适当的屏障指令,来限制编译器和处理器对指令序列的重排序,从而保证多线程程序的正确性和一致性。 

Java编译器会根据volatile内存语义的需求,在适当的位置插入内存屏障指令来禁止特定类型的处理器重排序。具体地,

  • 在每个volatile写操作的前面插入一个StoreStore屏障;
  • 在每个volatile写操作的后面插入一个StoreLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障,以确保volatile变量的可见性和有序性。

然而,请注意,由于编译器无法找到最优的指令插入位置,JMM采取了保守策略,为每个volatile写操作和读操作插入不同类型的内存屏障,以最大程度地确保volatile内存语义的正确实现。

七、先行发生原则

在多线程编程中,先行发生原则是指在一个线程中,如果一个操作先行发生于另一个操作,那么第一个操作的执行结果对于后续操作是可见的。

先行发生原则是Java内存模型(JMM)的基础之一,它定义了多线程程序中操作之间的可见性和顺序性保证。根据先行发生原则,下面是一些规则:

  1. 单线程规则:在单个线程中,按照程序的顺序执行的操作具有先行发生关系。也就是说,前一个操作的结果对于后续操作是可见的。

  2. 管程锁定规则:对于一个监视器锁的解锁操作先行发生于后续对同一个监视器锁的加锁操作。

  3. volatile变量规则:对一个volatile变量的写操作先行发生于后续对同一个变量的读操作。这确保了volatile变量的修改对所有线程是可见的。

  4. 线程启动规则:在一个线程调用另一个线程的start()方法之后,在调用线程的任何操作之前,被启动的线程的操作都先行发生。

  5. 线程加入规则:在一个线程调用另一个线程的join()方法之后,调用线程会等待被加入的线程执行完毕,被加入线程的结束操作先行发生于join()方法返回。

  6. 线程中断规则:在一个线程调用另一个线程的interrupt()方法之后,被中断线程的代码检测到中断事件的发生先行发生于interrupted()方法的调用,可以通过该方法检测是否有中断发生。

  7. 对象终结规则:在一个对象的初始化完成(构造函数执行结束)之后,其finalize()方法的开始操作先行发生。

  8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,则可以推断操作A先行发生于操作C。

先行发生原则提供了一种在多线程环境下保证可见性和顺序性的机制。通过遵循这些原则,可以减少并发编程中出现的问题,确保多线程程序的正确性和稳定性。

请注意:

  • 这些规则是由Java虚拟机(JVM)定义的,旨在保证多线程程序的行为一致性和可预测性。
  • 先行发生原则是为了描述多线程程序的行为规范,并不直接对编译器和处理器的具体实现做出限制。具体的实现可能会通过内存屏障等机制来保证这些原则的实现。

八、代码示例

下面是一个演示了部分Java内存模型的概念和规则的示例代码:

public class Main {private static int counter = 12; // 共享计数器变量public static void main(String[] args) throws InterruptedException {Thread incrementThread = new Thread(() -> {for (int i = 0; i < 1000000; i++) {synchronized (Main.class) { // 创建互斥区域counter++; // 原子操作}}});Thread decrementThread = new Thread(() -> {for (int i = 0; i < 1000000; i++) {synchronized (Main.class) { // 创建互斥区域counter--; // 原子操作}}});incrementThread.start();decrementThread.start();incrementThread.join();decrementThread.join();System.out.println("Counter: " + counter); // 输出计数器的值}
}

在这个示例中,有两个线程:一个增加线程和一个减少线程。它们同时访问一个共享的计数器变量counter。

关键点:

  • 使用synchronized关键字来创建互斥区域,确保对counter的操作是原子的。
  • 通过synchronized块的锁定对象使用了MemoryModelExample.class,这样两个线程能够共享同一个锁。
  • 在增加线程和减少线程的循环中,对counter的读取、修改和写入操作都处于同一个互斥区域内。
  • 通过使用同步机制,保证了对counter的访问是按序进行的,遵循管程锁定规则。

这个示例演示了互斥访问共享变量的情况,使用synchronized关键字确保了线程安全性。最终输出的结果应该是12,因为增加线程和减少线程的操作会互相抵消。

请注意,在实际应用中,需要根据具体场景选择适当的同步机制(如synchronized、Lock等)来处理并发访问共享数据的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/86592.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Django之视图

一&#xff09;文件与文件夹 当我们设定好一个Djiango项目时&#xff0c;里面会有着view.py等文件&#xff0c;也就是文件的方式&#xff1a; 那么我们在后续增加app等时&#xff0c;view.py等文件会显得较为臃肿&#xff0c;当然也根据个人习惯&#xff0c;这时我们可以使用…

2023-9-23 最大不相交区间数量

题目链接&#xff1a;最大不相交区间数量 #include <iostream> #include <algorithm>using namespace std;const int N 100010;int n;struct Range {int l, r;bool operator< (const Range &W) const {return r < W.r;} }range[N];int main() {cin >…

IDEA最新激 20活23码

人狠话不多 大家好&#xff0c;最近Intelli Idea官方的校验规则进行了更新&#xff0c;之前已经成功激20活23的Idea可能突然无法使用了。 特地从网上整理了最新、最稳定的激20活23码分享给大家&#xff0c;希望可以帮助那些苦苦为寻找Idea激20活23码而劳累的朋友们。 本激23…

前端框架之争:Vue.js vs. React.js vs. Angular

文章目录 Vue.js - 渐进式框架的魅力简单易用组件化开发生态系统和工具适用场景 React.js - 高性能的虚拟DOM虚拟DOM单向数据流社区和生态系统适用场景 Angular - 一站式框架完整的框架双向数据绑定类型安全适用场景 如何选择&#xff1f;项目规模生态系统技能和经验性能需求 结…

MyBatis基础之SqlSession

SqlSession 线程安全问题 当你翻看 SqlSession 的源码时&#xff0c;你会发现它只是一个接口。我们通过 MyBatis 操作数据库&#xff0c;实际上就是通过 SqlSession 获取一个 JDBC 链接&#xff0c;然后操作数据库。 SqlSession 接口有 3 个实现类&#xff1a; #实现类1Defa…

redis7==源码阅读1:Makefile构成

1命令 有两份Makefile&#xff0c;第一份指向第二份。 编译时候使用的命令是make 清理命令是make clean 只编译自带的客户端是make redis-cli 只清理自带的客户端是make redis-cli clean 可执行文件redis-cli来自anet.o adlist.o dict.o redis-cli.o zmalloc.o release.o…

【Java 基础篇】Java函数式接口详解

Java是一门强类型、面向对象的编程语言&#xff0c;但在Java 8引入了函数式编程的概念&#xff0c;这为我们提供了更多灵活的编程方式。函数式接口是函数式编程的核心概念之一&#xff0c;本文将详细介绍Java函数式接口的概念、用法以及一些实际应用。 什么是函数式接口&#…

第一个Servlet程序

目录 一、Servlet是什么 二、第一个Servlet项目 2.1 创建Maven项目 2.2 引入Servlet依赖 2.3 创建目录 三、Servlet启动 3.1 编写代码 3.2 打包程序 3.3 部署程序 四、更便捷的部署方式 4.1 安装Smart Tomcat插件 一、Servlet是什么 Servlet 是一种实现动态页面的技术。是一组…

LeetCode 75-02:字符串的最大公因子

前置知识&#xff1a;使用欧几里得算法求出最大公约数 func gcdOfStrings(str1 string, str2 string) string {if str1str2 ! str2str1 {return ""}return str1[:gcd(len(str1), len(str2))] }func gcd(a, b int)int{if b 0{return a}return gcd(b, a%b) }

rust字符串

标准库提供了String结构体表示字符串。 String实际上就是Vec<u8>的封装。唯一的不同是String的方法假定Vec<u8>中的二进制都是utf8编码的 pub struct String {vec: Vec<u8>, }一、定义String 1.使用new方法创建空字符串 let string String::new();2.使用…

【DRAM存储器六】DRAM存储器的架构演进-part3

&#x1f449;个人主页&#xff1a;highman110 &#x1f449;作者简介&#xff1a;一名硬件工程师&#xff0c;持续学习&#xff0c;不断记录&#xff0c;保持思考&#xff0c;输出干货内容 参考书籍&#xff1a;《Memory Systems - Cache, DRAM, Disk》 目录 以降低延时…

【C语言精髓 之 指针】指针*、取地址、解引用*、引用

/*** file * author jUicE_g2R(qq:3406291309)————彬(bin-必应)* 一个某双流一大学通信与信息专业大二在读 * copyright 2023.9* COPYRIGHT 原创技术笔记&#xff1a;转载需获得博主本人同意&#xff0c;且需标明转载源* language …

【C】指针初阶

指针 为了后续一些安排打基础&#xff0c;决定使用C/C作为算法主语言&#xff0c;所以从这篇文章开始&#xff0c;从指针开始总结 指针 -> 指针进阶 -> 字符串函数 -> 自定义类型 -> 动态内存管理 -> 数据结构 还有C一些基础语法的回顾&#xff08;基于算法…

【postgresql 】 ERROR: “name“ is not supported as an alias

org.postgresql.util.PSQLException: ERROR: "name" is not supported as an alias 错误&#xff1a;不支持将“name”作为别名 SELECT real_name name FROM doc_user 加上 在关键词上加上 “” 示例&#xff1a; SELECT real_name "name" FROM do…

SWIFT中最常见的内存泄漏陷阱

SWIFT中最常见的内存泄漏陷阱 如果您有内存循环&#xff0c;它将在调试器中向您显示警告&#xff1a; 如果确实有一个&#xff08;或通常是一堆&#xff09;&#xff0c;则表示您有一个泄漏的物体。 您如何预防呢&#xff1f; 就像在关闭的第一行中添加[unowned self]一样简…

“Vue进阶:深入理解插值、指令、过滤器、计算属性和监听器“

目录 引言&#xff1a;Vue的插值Vue的指令Vue的过滤器Vue的计算属性和监听器vue购物车案例总结&#xff1a; 引言&#xff1a; Vue.js是一款流行的JavaScript框架&#xff0c;它提供了许多强大的功能来简化前端开发。在本篇博客中&#xff0c;我们将深入探讨Vue的一些高级特性…

Java项目:SSM的网上书城系统

作者主页&#xff1a;Java毕设网 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 一、相关文档 1、关于雅博书城在线系统的基本要求 &#xff08;1&#xff09;功能要求&#xff1a;可以管理个人中心、用户管理、图书分类管理、图书信息管理、…

Java 函数式编程思考 —— 授人以渔

引言 最近在使用函数式编程时&#xff0c;突然有了一点心得体会&#xff0c;简单说&#xff0c;用好了函数式编程&#xff0c;可以极大的实现方法调用的解耦&#xff0c;业务逻辑高度内聚&#xff0c;同时减少不必要的分支语句&#xff08;if-else&#xff09;。 一、函数式编…

【Vue3+Vite+Ts+element-plus】vue 使用 tsx语法详解

系列文章目录 【Vue3ViteTselement-plus】 超级详细 最新 vite4vue3tselement-pluseslint-prettier 项目搭建流程 【Vue3ViteTselement-plus】使用tsx实现左侧栏菜单无限层级封装 【Ts 系列】 TypeScript 从入门到进阶之基础篇(一) ts类型篇 文章目录 系列文章目录前言一、必要…

MySQL 数据库学习(六)备份与binlog日志

1 案例1&#xff1a;完全备份与恢复 1.1 问题 练习物理备份与恢复练习mysqldump备份与恢复 1.2 方案 在数据库服务器192.168.88.50 练习数据的备份与恢复 1.3 步骤 实现此案例需要按照如下步骤进行。 步骤一&#xff1a;练习物理备份与恢复 冷备份&#xff0c;需停止数…