Java并发编程(二)线程的安全性问题

线程的安全性问题

总所周知,多线程提高了系统的性能,但令人头痛的是线程会存在的安全性问题.

为什么会存在安全性问题,并且我们应该怎么去解决这类的问题。 其实线程安全问题可以总结为: 可见性、原子性、有序性这几个问题,我们搞 懂了这几个问题并且知道怎么解决,那么多线程安全性问题也就不是问题

CPU 高速缓存

线程是 CPU 调度的最小单元,线程涉及的目的最终仍然是更充分的利用计算机处理的效能,但是绝大部分的运算任务不能只依靠处理器“计算”就能完成,处理器还需要与内存交互,比如读取运算数据、存储运算结果,这个 I/O 操作是很难消除的。而由于计算机的存储设备与处理器的运算速度差距非常大,所以 现代计算机系统都会增加一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中

高速缓存从下到上越接近 CPU 速度越快,同时容量也越小。现在大部分的处理器都有二级或者三级缓存,从下到上依次为 L3 cache, L2 cache, L1 cache.

L1 Cache 一级缓存 本地 core 的缓存 分成 32K 的数据缓存 L1d 和 32k 指令缓存 L1i

访问 L1 需要 3cycles,

耗时大约 1ns;

L2 Cache 二级缓存 本地 core 的缓存 被设计为 L1 缓存与共享的 L3 缓存之间的缓冲,大小为 256K

访问 L2 需要 12cycles,

耗时大约 3ns;

L3 Cache 三级缓存 在同插槽的所有 core 共享 L3 缓存 分为多个 2M 的段

访问 L3 需要 38cycles,

耗时大约 12ns

 

缓存一致性问题

CPU-0 读取主存的数据,缓存到 CPU-0 的高速缓存中,CPU-1 也做了同样的事情,而 CPU-1 把 count 的值修改成了 2,并且同步到 CPU-1 的高速缓存,但是这个修改以后的值并没有写入到主存中,CPU-0 访问该字节,由于缓存没有 更新,所以仍然是之前的值,就会导致数据不一致的问题

引发这个问题的原因是因为多核心 CPU 情况下存在指令并行执行,而各个CPU 核心之间的数据不共享从而导致缓存一致性问题,为了解决这个问题,CPU 生产厂商提供了相应的解决方

总线锁

当一个 CPU 对其缓存中的数据进行操作的时候,往总线中发送一个 Lock 信号。其他处理器的请求将会被阻塞,那么该处理器可以独占共享内存。总线锁 相当于把 CPU 和内存之间的通信锁住了,所以这种方式会导致 CPU 的性能下降,所以 P6 系列以后的处理器,出现了另外一种方式,就是缓存锁

缓存锁

如果缓存在处理器缓存行中的内存区域在 LOCK 操作期间被锁定,当它执行锁操作回写内存时,处理不在总线上声明 LOCK 信号,而是修改内部的缓存地址,然后通过缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻 止同时修改被两个以上处理器缓存的内存区域的数据,当其他处理器回写已经 被锁定的缓存行的数据时会导致该缓存行无效。 所以如果声明了 CPU 的锁机制,会生成一个 LOCK 指令,会产生两个作用

  1. Lock 前缀指令会引起引起处理器缓存回写到内存,在 P6 以后的处理器中, LOCK 信号一般不锁总线,而是锁缓存

  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

缓存一致性协议 MESI

处理器上有一套完整的协议,来保证 Cache 的一致性,比较经典的应该就是 MESI 协议了,它的方法是在 CPU 缓存中保存一个标记位,这个标记为有四种 状态

缓存行(Cache line):缓存存储数据的单元。

状态 描述 监听任务
M(Modified) 修改 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。
E(Exclusive) 独占互斥 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S(Shared) 共享 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I(Invalid) 失效 该Cache line无效  

对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。

每个 Core 的 Cache 控制器不仅知道自己的读写操作,也监听其它 Cache 的读写操作,嗅探(snooping)"协议 CPU 的读取会遵循几个原则

  1. 如果缓存的状态是 I,那么就从内存中读取,否则直接从缓存读取

  2. 如果缓存处于 M 或者 E 的 CPU 嗅探到其他 CPU 有读的操作,就把自己的缓存写入到内存,并把自己的状态设置为 S

  3. 只有缓存状态是 M 或 E 的时候,CPU 才可以修改缓存中的数据,修改后,缓存状态变为 MC

MESI状态转换

对于MESI更深入理解可以去 并发研究之CPU缓存一致性协议(MESI) 这位朋友做了更详细的阐述

CPU 的优化执行

除了增加高速缓存以为,为了更充分利用处理器内内部的运算单元,处理器可 能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果 重组,保证该结果与顺序执行的结果一致,但并不保证程序中各个语句计算的 先后顺序与输入代码中的顺序一致,这个是处理器的优化执行;还有一个就是 编程语言的编译器也会有类似的优化,比如做 指令重排 来提升性能

 

并发编程的问题

前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关 系,其实原子性、可见性、有序性问题,是我们抽象出来的概念,他们的核心 本质就是刚刚提到的缓存一致性问题、处理器优化问题导致的指令重排序问 题。 比如缓存一致性就导致可见性问题、处理器的乱序执行会导致原子性问题、指 令重排会导致有序性问题。为了解决这些问题,所以在 JVM 中引入了 JMM 的 概念

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章