java并发 排队处理实现_Java 并发笔记】并发机制底层实现整理[转发]

1. 缓存一致性问题

format,png

硬件内存架构

现代计算机一般都有 2 个以上 CPU,而且每个 CPU 还有可能包含多个核心。因此,如果应用是多线程的话,这些线程可能会在各个 CPU 核心中并行运行。

在 CPU 内部有一组 CPU 寄存器,也就是 CPU 的储存器。

CPU 操作寄存器的速度要比操作计算机主存快的多。

在主存和 CPU 寄存器之间还存在一个 CPU 缓存,CPU 操作 CPU 缓存的速度快于主存但慢于 CPU 寄存器。某些 CPU 可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称作 RAM,所有的 CPU 都能够访问主存,而且主存比上面提到的缓存和寄存器大很多。

当一个 CPU 需要访问主存时,会先读取一部分主存数据到 CPU 缓存,进而在读取 CPU 缓存到寄存器。当 CPU 需要写数据到主存时,同样会先 flush 寄存器到

CPU 缓存,然后再在某些节点把缓存数据 flush 到主存。

format,png

缓存架构

缓存大大缩小了高速 CPU 与低速内存之间的差距。以三层缓存架构为例。

Core0 与 Core1 命中了内存中的同一个地址,那么各自的 L1 Cache 会缓存同一份数据的副本。

Core0 修改了数据,两份缓存中的数据不同了,Core1 L1 Cache 中的数据相当于失效了。

除三级缓存外,各厂商实现的硬件架构中还存在多种多样的缓存,都存在类似的可见性问题。例如,寄存器就相当于 CPU 与 L1 Cache 之间的缓存。

1.2 MESI 协议

MESI(Modified Exclusive Shared Or Invalid,缓存的四种状态)协议的基本原理。

Core0 修改数据 v 后,发送一个信号,将 Core1 缓存的数据 v 标记为失效,并将修改值写回内存。

Core0 可能会多次修改数据 v,每次修改都只发送一个信号(发信号时会锁住缓存间的总线),Core1 缓存的数据 v 保持着失效标记。

Core1 使用数据 v 前,发现缓存中的数据 v 已经失效了,得知数据 v 已经被修改,于是重新从其他缓存或内存中加载数据 v。

MESI 协议可以解决 CPU 缓存层面的一致性问题。

format,png

MESI 协议缓存状态

状态说明

M(修改,Modified)

本地处理器已经修改缓存行, 即是脏行, 它的内容与内存中的内容不一样. 并且此 cache 只有本地一个拷贝(专有)。

E(专有,Exclusive)

缓存行内容和内存中的一样, 而且其它处理器都没有这行数据。

S(共享,Shared)

缓存行内容和内存中的一样, 有可能其它处理器也存在此缓存行的拷贝。

I(无效,Invalid)

缓存行失效, 不能使用。

2. 优化重排序问题

在执行程序时,为了提高性能,处理器和编译器会对指令做重排序。

指令级并行的重排序。如果不存在数据依赖性,处理器 可以改变语句对应机器指令的执行顺序。

编译器优化的重排序。编译器 在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

内存系统的重排序。处理器使用 缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。(导致的可见性问题也可以通过 MESI 协议解决)

format,png

重排序

重排序不是随意重排序,它需要满足以下两个条件。

数据依赖性

如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

as-if-serial

所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身(单线程下的执行)的应有结果是一致的,编译器、runtime 和处理器都必须遵守 as-if-serial 语义。

2.1 指令级并行的重排序(处理器)

format,png

指令级并行的重排序

只要不影响程序单线程、顺序执行的结果,就可以对两个指令重排序。

乱序执行技术是处理器为提高运算速度而做出违背代码原有顺序的优化。

不优化时的执行过程优化时的执行过程

指令获取。

指令获取。

如果输入的运算对象是可以获取的(比如已经存在于寄存器中),这条指令会被发送到合适的功能单元。如果一个或者更多的运算对象在当前的时钟周期中是不可获取的(通常需要从主内存获取),处理器会开始等待直到它们是可以获取的。

指令被发送到一个指令序列(也称执行缓冲区或者保留站)中。

指令在合适的功能单元中被执行。

指令将在序列中等待,直到它的数据运算对象是可以获取的。然后,指令被允许在先进入的、旧的指令之前离开序列缓冲区。(此处表现为乱序)

功能单元将运算结果写回寄存器。

指令被分配给一个合适的功能单元并由之执行。

结果被放到一个序列中。

仅当所有在该指令之前的指令都将他们的结果写入寄存器后,这条指令的结果才会被写入寄存器中。(重整乱序结果)

2.2 编译器优化的重排序

和处理器乱序执行的目的是一样的,与其等待阻塞指令(如等待缓存刷入)完成,不如先执行其他指令。与处理器乱序执行相比,编译器重排序能够完成更大范围、效果更好的乱序优化。

编译器层面的重排序,自然可以由编译器控制。使用 volatile 做标记,就可以禁用编译器层面的重排序。

JVM 自己维护的 内存模型 中也有可见性问题,使用 volatile 做标记,取消 volatile 变量的缓存,就解决了 JVM 层面的可见性问题。

3. 内存模型

可以把内存模型理解为在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。

在并发编程需要处理的两个关键问题是:线程之间如何通信 和 线程之间如何同步。

通信

通信是指线程之间以何种机制来交换信息。

命令式编程中,线程之间的通信机制有两种,是 共享内存 和 消息传递。

共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的 公共状态 来 隐式 进行通信。

消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的 发送消息 来 显式 进行通信。

同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。

共享内存的并发模型里,同步是 显式 进行的。程序员必须显式指定某个方法或某段代码需要在线程之间 互斥执行。

消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是 隐式 进行的。

Java 的并发采用的是 共享内存模型,线程之间的通信对程序员完全透明。

3.1 顺序一致性内存模型

顺序一致性内存模型有两大特性。

一个线程中的所有操作必须按照程序的顺序来执行。

(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

format,png

顺序一致性内存模型

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读/写操作。

在任意时间点最多只能有一个线程可以连接到内存。

当多个线程并发执行时,开关装置能把所有线程的所有内存读/写操作串行化。

format,png

一致性模型执行效果

假设这两个线程使用监视器锁来正确同步:A 线程的三个操作执行后释放监视器锁,随后 B 线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果。

format,png

未同步程序在一致性模型中执行效果

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。

之所以能得到这个保证是因为

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值