2019独角兽企业重金招聘Python工程师标准>>>

背景知识

CPU Cache

如上简易图所示,在当前主流的CPU中,每个CPU有多个核组成的。每个核拥有自己的寄存器,L1,L2 Cache。 多个核之间共享L3 Cache。内存的数据也被多个核共享。在多核系统中,这些的缓存通过加速数据访问和降低共享内存在总线上的通讯来提高CPU性能。

比较经典的Cache一致性协议当属MESI协议,很多处理器都是使用它或者它的的变种。M表示Modified,E表示Exclusive,S表示Shared,I表示Invalid。详见《大话处理器》Cache一致性协议之MESI


CPU乱序执行(CPU Out-of-order Execution)

现在的CPU一般采用流水线来执行指令. 一个指令的执行被分成: 取指, 译码, 访存, 执行,写回, 等若干个阶段.

当CPU在执行指令时,如果发现所需要的数据不在Cache时,则需要从外部存储器中取,而这个过程通常需要几十,甚至几百个Cycle。如果CPU是顺序执行这些指令时,则后面的指令需要等待。所以如果CPU支持乱序执行的话,那么就可以先执行后面不依赖该数据的指令,进而提升CPU计算性能。


编译器重排序优化(Compiler Reordering Optimizations)

由于CPU流水线的指令预取范围有限,所以只能在很小的范围内判断指令是否能够并发执行。编译器可以分析相当长的一段代码,从而把能够并发执行的代码尽量靠近,这就是所谓的编译器优化。


内存屏障

如上面所说,对于” a++;b = f(a);c = f”等存在依赖关系的指令,CPU则会在“b= f(a)”执行阶段之前被阻塞;另一方面,编译器也有可能将依赖关系很近“人为地”拉开距离以防止阻塞情况的发生,从而导致编译器乱序,如“a++ ;c = f;b = f(a)”。

没有关联的内存操作会被按随机顺序有效的得到执行, 但是在CPU与CPU交互时或CPU与IO设备交互时,这可能会成为问题。我们需要一些手段来干预编译器和CPU, 使其限制指令顺序。

内存屏障能保证处于内存屏障两边的内存操作满足部分有序。这里"部分有序"的意思是,内存屏障之前的操作都会先于屏障之后的操作,但是如果几个操作出现在屏障的同一边,则不保证它们的顺序。

内存屏障分为编译器内存屏障和CPU内存屏障。

在GCC中,内存屏障是asm volatile("" ::: "memory");或者__asm__ __volatile__ ("" ::: "memory");

在Linux中,内存屏障根据 UP还是SMP而有所不同。详见LINUX内核之内存屏障

内存屏障是否保证CPU将CPU缓存刷写回内存?我在@何_登成大牛的blog 上面提问了下,但是没有得到明确地回复。


锁(信号)

在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取或修改这个内存地址。这种能力是通过 LOCK 指令前缀再加上下面的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

在一些处理器,包括 P6 家族,奔腾4(Pentium4)系列,至强(Xeon)处理器,lock 操作可能不会宣告一个 LOCK# 信号。从 P6 家族处理器开始,当使用 LOCK 指令访问的内存已经被处理器加载到缓存中时,LOCK# 信号通常不会被宣告。取而代之的是,仅是锁定了处理器的缓存。这里,处理器缓存的相干性(coherency)机制确保了可以原子性的对内存进行操作。

这个lock 仅仅是锁定单条汇编语言的执行,并不是范围锁。至于范围锁的具体实现,还是需要看下Linux底层的pthread_mutex_lock的源码。但是目前Linux底层功力还很浅薄。。


JMM

在多线程环境下,由于CPU是多核的,各个核之间的数据修改对方可能感受不到,所以会产生可见性问题; 由于上文提到的编译器的重排序优化(编译器)和CPU乱序执行(执行期),产生无序性问题

JMM为了解决如下三个相互牵连的问题:

  • 原子性:哪些指令必须是不可分割的。在Java内存模型中,这些规则需声明仅适用于-—实例变量和静态变量,也包括数组元素,但不包括方法中的局部变量-—的内存单元的简单读写操作。
  • 可见性:在哪些情况下,一个线程执行的结果对另一个线程是可见的。这里需要关心的结果有,写入的字段以及读取这个字段所看到的值。
  • 有序性:在什么情况下,某个线程的操作结果对其它线程来看是无序的。最主要的乱序执行问题主要表现在读写操作和赋值语句的相互执行顺序上。

详细介绍见深入理解Java内存模型 系列文章,下文仅是用通俗的文字,概略介绍下。

原子性

无论是数据库的ACID还是其他什么模型,谈到原子必然是指一个操作的结果要么是成功,要么是失败,不允许存在中间的状态。在C语言的多线程环境下,如果两个线程同时写一个内存地址,则可能出现混乱的数据。所以JMM约定了在JAVA中,哪些数据的操作是原子的,防止产生混乱的数据。


可见性

由于CPU的寄存器是不共享的,而内存才是共享的。所以一个CORE的对数据的修改,无法立即让另外的CORE感知(MESI仅是维护缓存一致性,而不是内存一致性)。

所以,JVM以及底层系统提供了大概3种实现机制来保证可见性:

  • spinlock 和 mutex 都能保证原子性,可见性和下个章节的介绍的有序性
  • volatile能够保证可见性和下个章节的介绍的有序性,能够保证对volatile的单个操作的原子性,但是volatile var ++这种操作时非原子的。

究其本质实现,都是指令执行完成后,强制将所有写过的变量刷新到内存中区。其他线程取值时,需要重新从内存中加载。


有序性

从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。

这个线程“观察”到其他线程并发地执行非同步的代码时,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块以及volatile字段的操作仍维持相对有序。

通俗理解happen-before规则

与程序员密切相关的happens-before规则如下:1.程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
2.监视器锁规则:对一个监视器锁的释放,happens- before 于随后对这个监视器锁的获取。
3.volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
4.传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

第一条规则针对线程内的代码执行。后面3条规则都是线程间的。

第二条针对锁,强调一个线程监视器锁的释放必然早于另一个线程监视器锁的获得,因为这个锁只能同时被一个线程使用。

第三题条针对volatile,写早于读,这样就能及时最新的数据,否则一直读旧数据,肯定是有问题的。

第四条纯粹是数学逻辑游戏。


后记

理解多线程实现原理需要大量的知识,需要从硬件的CPU体系结构,汇编原理,Linux的库函数...,很是痛苦。虽然收获也比较大,但是感觉还是没有彻底串起来。需要进一步加油。

参考

Memory Barrier in Compiler and CPU

LINUX内核之内存屏障简介

Cache Concurrency Problem - False Sharing

LINUX内核内存屏障 LINUX官方文档翻译

LINUX内核内存屏障 LINUX官方文档英文版

WIKI的MESI_protocol

INFOQ的深入理解Java内存模型 系列文章

何登成的技术博客 CPU Cache and Memory Ordering——并发程序设计入门

转载于:https://my.oschina.net/geecoodeer/blog/191776

JAVA学习笔记--4.多线程编程 part1.背景知识和内存模型相关推荐

  1. JAVA学习笔记--4.多线程编程 part5.这些年的那些坑

    2019独角兽企业重金招聘Python工程师标准>>> 基本要求 没指定线程name. 没有限定线程个数. Thread#stop等被废弃的方法不安全,详见TODO. 注意锁作用的对 ...

  2. java学习笔记15--多线程编程基础2

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note15.html,转载请注明源地址. 线程的生命周期 1.线程的生命周期 线程从产生到消亡 ...

  3. java学习笔记14--多线程编程基础1

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note14.html,转载请注明源地址. 多线程编程基础 多进程 一个独立程序的每一次运行称为 ...

  4. Java学习笔记5-1——多线程

    目录 前言 核心概念 线程创建 继承Thread类 实现Runnable接口 上述两个方法小结 实现Callable接口 并发问题简介 静态代理模式 线程状态 线程停止(stop) 线程休眠(slee ...

  5. Java学习笔记2 多线程简单总结

    多线程简单总结 1. 相关概念 1.1 线程与进程 进程 线程 1.2 线程调度 分时调度 抢占式调度 1.3 同步与异步 同步 异步 1.4 并发与并行 并发 并行 2. 创建线程 2.1 继承Th ...

  6. VB.NET学习笔记:多线程编程

    在<多线程加委托实现等待窗体(loading正在加载界面),运行超时可以取消操作>一文中使用到了多线程编程,在这里做个笔记. 我们继续使用<再谈委托--同步.异步.Lambda 表达 ...

  7. 【cpp学习笔记】多线程编程

    1.前言 在学习利用多线程来实现多模型的目标检测时,接触到了 condition_variable.future等多线程编程知识,感觉自己对这方面的知识还不熟悉,于是便找了一些学习资料进行学习. 2. ...

  8. JAVA学习笔记 -- JUC并发编程

    1 进程.线程 进程就是用来加载指令.管理内存.管理IO.当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程.进程可以被视为程序的一个实例. 线程,一个线程就是一个指令流,将指令流 ...

  9. JAVA学习笔记(并发编程-叁)- 线程安全性

    文章目录 线程安全性-原子性 原子性-Atomic包 AtomicXXX: CAS, Unsafe.compareAndSwapInt AtomicLong LongAdder AtomicRefer ...

最新文章

  1. 代码解说Android Scroller、VelocityTracker
  2. Linux进程ID号--Linux进程的管理与调度(三)
  3. 我国的人工智能芯片的市场规模及发展前景
  4. nagios配置文档
  5. 前端实例练习 - 动效按钮
  6. 借力阿里云数据中台,日播集团“数”识消费者
  7. Spring 梳理 - @Component
  8. DOS文件格式 与 UNIX文件格式 转换
  9. 多线程的三种实现方法
  10. JFinal Db + Record模式 - ORM 框架
  11. sql server 2008 数据库可疑的解决步骤
  12. vmware通过vCerter Converter Standalone实现不同VC的V2V虚拟机迁移
  13. Lync 2010 企业版安装
  14. asp.net处理机制管道事件
  15. CleanMyMac v4.10.1支持Monterey 12.x
  16. c++ builder 存储过程 mysql mssql_C++ Builder实现Microsoft SQL Server 2000 的扩展存储过程...
  17. 人工智能深度学习数据集
  18. IP6826无线充电底座方案IC芯片,兼容WPC Qi v1.2.4
  19. WeightBiases教程
  20. Mininet连接真实网络的实现

热门文章

  1. fail2ban使用教程
  2. 我们梳理了一下VR教育,感觉它将会是下一个蓝海
  3. noip2005 过河
  4. 唠唠SE的集合-10——Collections工具类
  5. UVA 10214 Trees in a Wood
  6. 数据导入报错:Got a packet bigger than‘max_allowed_packet’bytes的问题
  7. Struts2 分割字符串标签s:generator
  8. [.net 面向对象编程基础] (18) 泛型
  9. JS小数位保留两位小数
  10. 【转载】Asp.Net MVC3网站并成功的连接了MongoDB