你们都知道,线程会存在安全性问题,那接下来咱们从原理层面去了解线程为编程
什么会存在安全性问题,而且咱们应该怎么去解决这类的问题。数组
其实线程安全问题能够总结为: 可见性、原子性、有序性这几个问题,咱们搞缓存
懂了这几个问题而且知道怎么解决,那么多线程安全性问题也就不是问题了安全
CPU 高速缓存多线程
线程是 CPU 调度的最小单元,线程涉及的目的最终仍然是更充分的利用计算机并发
处理的效能,可是绝大部分的运算任务不能只依靠处理器“计算”就能完成,处编程语言
理器还须要与内存交互,好比读取运算数据、存储运算结果,这个 I/O 操做是ide
很难消除的。而因为计算机的存储设备与处理器的运算速度差距很是大,因此oop
现代计算机系统都会增长一层读写速度尽量接近处理器运算速度的高速缓存性能
来做为内存和处理器之间的缓冲:将运算须要使用的数据复制到缓存中,让运
算能快速进行,当运算结束后再从缓存同步到内存之中。
高速缓存从下到上越接近 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 指令,会产生两个做用
Lock 前缀指令会引发引发处理器缓存回写到内存,在 P6 之后的处理器中,LOCK 信号通常不锁总线,而是锁缓存
一个处理器的缓存回写到内存会致使其余处理器的缓存无效
缓存一致性协议
处理器上有一套完整的协议,来保证 Cache 的一致性,比较经典的应该就是
MESI 协议(梅西协议)了,它的方法是在 CPU 缓存中保存一个标记位,这个标记为有四种状态:
M(Modified) 修改缓存,当前 CPU 缓存已经被修改,表示已经和内存中的数据不一致了
I(Invalid) 失效缓存,说明 CPU 的缓存已经不能使用了
E(Exclusive) 独占缓存,当前 cpu 的缓存和内存中数据保持一直,并且其余处理器没有缓存该数据
S(Shared) 共享缓存,数据和内存中数据一致,而且该数据存在多个 cpu缓存中
每一个 Core 的 Cache 控制器不只知道本身的读写操做,也监听其它 Cache 的读
写操做,嗅探(snooping)"协议
CPU 的读取会遵循几个原则:
若是缓存的状态是 I(失效缓存),那么就从内存中读取,不然直接从缓存读取
若是缓存处于 M 或者 E 的 CPU 嗅探到其余 CPU 有读的操做,就把本身的缓存写入到内存,并把本身的状态设置为 S
只有缓存状态是 M 或 E 的时候,CPU 才能够修改缓存中的数据,修改后,缓存状态变为 MC
CPU 的优化执行
除了增长高速缓存觉得,为了更充分利用处理器内内部的运算单元,处理器可
能会对输入的代码进行乱序执行优化,处理器会在计算以后将乱序执行的结果
充足,保证该结果与顺序执行的结果一直,但并不保证程序中各个语句计算的
前后顺序与输入代码中的顺序一致,这个是处理器的优化执行;还有一个就是
编程语言的编译器也会有相似的优化,好比作指令重排来提高性能。
并发编程的问题
前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关
系,其实原子性、可见性、有序性问题,是咱们抽象出来的概念,他们的核心
本质就是刚刚提到的缓存一致性问题、处理器优化问题致使的指令重排序问
题。
好比缓存一致性就致使可见性问题、处理器的乱序执行会致使原子性问题、指
令重排会致使有序性问题。为了解决这些问题,因此在 JVM 中引入了 JMM 的
概念
内存模型
内存模型定义了共享内存系统中多线程程序读写操做行为的规范,来屏蔽各类
硬件和操做系统的内存访问差别,来实现 Java 程序在各个平台下都能达到一致
的内存访问效果。Java 内存模型的主要目标是定义程序中各个变量的访问规
则,也就是在虚拟机中将变量存储到内存以及从内存中取出变量(这里的变
量,指的是共享变量,也就是实例对象、静态字段、数组对象等存储在堆内存
中的变量。而对于局部变量这类的,属于线程私有,不会被共享)这类的底层
细节。经过这些规则来规范对内存的读写操做,从而保证指令执行的正确性。
它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了 CPU
多级缓存、处理器优化、指令重排等致使的内存访问问题,保证了并发场景下
的可见性、原子性和有序性,。内存模型解决并发问题主要采用两种方式:限
制处理器优化和使用内存屏障。
Java 内存模型定义了线程和内存的交互方式,在 JMM (Java Memory Model)抽象模型中,分为主内存、工做内存。主内存是全部线程共享的,工做内存是每一个线程独有的。线程对变量的全部操做(读取、赋值)都必须在工做内存中进行,不能直接读写主内存中的变量。而且不一样的线程之间没法访问对方工做内存中的变量,线程间的变量值的传递都须要经过主内存来完成,他们三者的交互关系以下:
因此,总的来讲,JMM 是一种规范,目的是解决因为多线程经过共享内存进行
通讯时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会
对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性
和有序性。
因为边写是在电脑上,因此可能会致使手机端看起来排版不太美观,后期我会把这一块优化一下,尽可能作到兼容
参考:Java并发编程的艺术