在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。那么它们产生的原因和在Java中解决的办法又是什么呢?

一、内存模型的相关概念

​ 计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

简单的例子,比如下面的这段代码:

i = i + 1;

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

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核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种解决方法:

1)通过在总线加LOCK#锁的方式

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

2)通过缓存一致性协议

加LOCK#锁的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

二、并发编程的三个概念存在的问题

1、 线程切换带来的原子性问题

Java中的一条语句,在翻译为机器码之后,可能对应的是多个指令。

比如:i++这个操作至少需要3条指令;

  1. 把 i 的值从内存=加载到寄存器;
  2. 执行+1操作;
  3. 把值写入内存;

假如 i=0,两个线程同时执行该操作,可能线程1执行完第一步,就切换到线程2执行,本来两个线程各执行一次后 i 的值应该为 2 ,此时就出现 两次递增操作后值为 1 的现象;

2、缓存导致的可见性问题:

Java内存模型规定所有的变量存储在主内存中。每个线程都有自己的工作内存,线程在工作内存中保存了使用到的主内存中变量的副本拷贝,线程对变量的操作必须在工作内存中进行,不能直接读写主内存中的变量。不同线程之间无法访问对方工作内存的变量。线程之间共享变量值的传递均需要通过主内存来完成。

当线程1对共享变量A进行修改之后,线程2的工作内存中A可能还不是最新的值。这时候线程1的操作对线程2就不具有可见性。

3、编译优化带来的有序性问题:

为了充分利用处理器的性能,处理器会对输入的代码进行乱序执行。在计算之后将乱序执行的结果重组,并保证该结果和顺序执行的结果一致,但是并不保证程序中各个语句的计算顺序和输入代码的顺序一致。Java虚拟机也有类似的指令重排序优化。

比如:Object obj = new Object(),

这条语句对应的指令为:

  1. 分配一块内存M;
  2. 在M上初始化 Object 对象;
  3. 将M的地址赋值给 obj;

计算机经过优化后可能先执行第三步,再第二步,如果执行完第三步后切换到别的线程,若此时访问该变量则会发生空指针异常;

三、Java内存模型

在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,研究一下 Java内存模型 为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。

注意:为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存各自的缓存中中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

举个简单的例子:在java中,执行下面这个语句:

i = 10

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

那么Java语言本身对原子性、可见性以及有序性提供了哪些保证呢?

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

  • 概念:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

  • 概念:Java程序中,如果在本线程中观察,所有的操作都是有序的;如果在另一个线程观察,所有的操作都是无序的。前半句指的是线程内表现为串行的语义,后半句指的是指令重排序和主内存和工作内存同步延迟的问题。

Java并发篇_Java内存模型相关推荐

  1. java float内存结构_Java后端开发岗必备技能:Java并发中的内存模型

    欢迎关注专栏: Java架构技术进阶 .里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦. JMM通过构建一个统一的内存模型来屏蔽掉不同硬件平台和不同操作系统之间的差异,让Jav ...

  2. java并发编程系列-内存模型基础

    java线程之间的通信对程序开发人员是完全透明的,内存的可见性问题很容易困扰很多开发人员.本篇博文将揭开java内存模型的神秘面纱,来看看内存模型到底是怎样的. 并发编程中需要处理的两个关键问题: · ...

  3. java model 原则_java内存模型(Java Memeory Model)

    1.java内存模型的重要目标是定义程序中各个访问变量的访问规则,即在虚拟机中将变量存储到内存,和从内存中取出变量的低层细节:这里所说的变量主要包括实例字段,静态字段,构成数组对象的元素不包括局部变量 ...

  4. java锁上升_Java内存模型FAQ(十一)新的内存模型是否修复了双重锁检查问题?...

    臭名昭著的双重锁检查(也叫多线程单例模式)是一个骗人的把戏,它用来支持lazy初始化,同时避免过度使用同步.在非常早的JVM中,同步非常慢,开发人员非常希望删掉它.双重锁检查代码如下: // doub ...

  5. java重排序_Java内存模型FAQ(四)重排序意味着什么?

    译者:Alex 在很多情况下,访问一个程序变量(对象实例字段,类静态字段和数组元素)可能会使用不同的顺序执行,而不是程序语义所指定的顺序执行.编译器能够自由的以优化的名义去改变指令顺序.在特定的环境下 ...

  6. Java 并发编程解析 | 如何正确理解Java领域中的内存模型,主要是解决了什么问题?

    写在开头 这些年,随着CPU.内存.I/O 设备都在不断迭代,不断朝着更快的方向努力.在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异.CPU 和内存的速度差异可以形象地描述为:C ...

  7. Java 并发编程解析 , 如何正确理解Java领域中的内存模型

    这些年,随着CPU.内存.I/O 设备都在不断迭代,不断朝着更快的方向努力.在这个快速发展的过程中,有一个核心矛盾一直存在,就是这三者的速度差异.CPU 和内存的速度差异可以形象地描述为:CPU 是天 ...

  8. java内存规范_Java内存模型-jsr133规范介绍

    最近在看<深入理解Java虚拟机:JVM高级特性与最佳实践>讲到了线程相关的细节知识,里面讲述了关于java内存模型,也就是jsr 133定义的规范. 系统的看了jsr 133规范的前面几 ...

  9. 并发面试必备系列之并发基础与内存模型

    坐标上海松江高科技园,诚聘高级前端工程师/高级 Java 工程师,有兴趣的看 JD:https://www.lagou.com/jobs/6361564.html 并发面试必备系列之并发基础与内存模型 ...

最新文章

  1. linux 编译mysql_linux下编译MYSQL
  2. Linux下的samba服务配置详解
  3. Mac OS 软件包管理器Homebrew
  4. 基础算法之Dijkstra最短路径
  5. Unity3D两种方式播放视频
  6. SAP云平台架构概述
  7. C++ 以对象管理资源
  8. N多校2018d4t7Maximum Mode
  9. leetcode python3 简单题108. Convert Sorted Array to Binary Search Tree
  10. TELERIK Reporting 实践
  11. 【POJ 1113】Wall【凸包+一点思维】
  12. Axure工具概述以及Axure RP9的安装汉化和授权
  13. 理工科专业精品书系列
  14. [PTA]7-116 计算圆周率(c语言)(学习记录)
  15. java出现圅_java获取汉字拼音首字母A
  16. 80psi等于多少kpa_1kpa等于多少psi
  17. 解决 Word 中空格下划线居中后下划线不显示的问题
  18. GitExtensions 设置
  19. FMU主板程序更新说明
  20. @RequiredArgsConstructor 代替@AutoWired注解

热门文章

  1. java 实体类 代码重复_java – JPA两个单向@OneToMany关系到同一个实体导致重复输入...
  2. BugkuCTF-MISC题旋转跳跃
  3. c# 微服务学习_资深架构师学习笔记:什么是微服务?
  4. as mysql with 嵌套_MySQL_MySQL的嵌套查询,MySQl从4.11版后已经完全支持嵌 - phpStudy
  5. hdc mfc 画扇形图_MFC画图总结-DIB图形绘制
  6. python36中文手册_python36中文手册_python_36_文件操作4
  7. android jni 中jnienv,android JNI中JNIEnv類型和jobject類型的解釋
  8. JVM思维导图、正则表达式符号图、企业内部开发流程图
  9. java 素数乘积,求助2424379123 = 两个素数的乘积,求这两个素数?
  10. java 调用对象的方法_JAVA调用对象方法的执行过程