一、cpu 结构以及原子指令简介
1、处理器结构发展历史
单核心
1、core —> 内存
2、多核心
core1 —> 内存
core2 —> 内存
3、 多核+缓存 (内存和cpu 频率差别很大)
core1 —> cache1(1-2) ->cahce3 —>内存
core2 —> cahce2(1-2)->cache3—> 内存
4、多核+缓存+缓冲区
core1 —>[ buffue1] —> cacheN —>[invalidQueue1]—>内存
core2 —>[buffue2] —> cacheN —>[invalidQueue 1]—>内存
2、处理理原子操作/同步支持
原子操作定义:
同一时间只能一个cpu 访问同一区域数据,并且该指令执行过程中不能被中断,不能上下文切换。一般用锁来实现
锁实现:
对同一个标识位(内存/寄存器)更新,用1更新标志位,如果标志位为1 ,则不能更新,表示已加锁,返回0. 反之成功返回1,如果锁操作同时进行,那么将会排序。也会串行执行指令。
3、intel 原子操作实现历史(与cpu 结构设计相关)
0、基础操作类型(读写)
1、读或写一个字节
2、读或写一个在16位边界对齐的字
3、读或写一个在32位边界对齐的双字
1、可靠的原子操作(无缓存,单核),如下操作是原子的
基础操作类型cpu 默认为原子操作
2、总线加锁,使用LOCK#信号和LOCK指令前缀(多核cpu,有缓存或者无缓存)
1、如果有缓存,并且实现了缓存一致性协议
Lock指令将无效, 自动由缓存一致性协议来保证 基础操作类型cpu 默认为原子操作,
缓存一致性协议还保证,共享的缓存数据只能由一个cpu 更新。
2、没有实现缓存一致性协议或者无缓存结构
LOCK 指令将在cpu 控制总线上面发出一个lock 信号,只有一个cpu 能够访问共享数据,并且完成原子操作。(如果多个cpu 同时发出,总线仲裁设备也只会让一个cpu 能够申请成功)
3、内存屏障指令LFENCE,SFECE,MFENCE(多核cpu,除缓存结构外,还有buffer 结构)
0、为什么增加了buffer ,invalidQueue等结构。 为了让cpu 更快的执行。写数据先回buffer ,不写cache 这样 写变成了临时写(类似于异步编程),然后就可以继续执行后续的读操作(这样cpu 就会乱序执行指令)。
1、为了保证cpu 执行的顺序性,提供了内存屏障指令。并且规定: 读操作不能越过LFENCE和MFENCE指令,读写操作都不能跨越I/O指令,加锁指令,写操作不能越过SFECE和MFENCE指令
4、lock 指令与内存屏障与缓存一致性协议的关系 ?
1、 lock 只能保证单条指令的原子操作。lock 保证只有同时只有一个cpu 写,并且写完成后,保证写的变量在所有cpu里面数据一致
2、 如果有缓存一致性协议的情况下,lock 指令无效。有缓存一致性协议保证。
3、缓存一致性协议,能报保证数据写入到cache 后,各个cpu 里面的数据一致,但是如果数据写入的是buffer,
那么此时缓存一致性协议也将无效。需要通过内存屏障指令。让buffer 里面数据全部写入缓存后。才能执行
后续的指令。buffer flush 到cache 后。就能够保证所有cache 数据一致。
4、内存屏障指令可以保证一部分指令在另一部分指令之前先完成操作。有可能部分指令见是乱序的。部分与部分之间是有序的。
数据同步的粒度大于lock 指令
5、lock 指令与内存屏障指令是cpu 提供给程序自定义使用的指令。让程序有更大的灵活性,充分利用cpu资源(lock 与内存屏障同步的粒度不一样)
二、java volitale 实现
1、 java 语言规范定义 :
java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量
2、volitale 实现目标 :
需要兼容多种不同cpu 结构,不同操作系统。volitale 关键字都能够达到java 语言规范定义的效果
3、volitale 实现细节
1、lock 指令的使用, JIT 编译 会判断当前的cpu 体系结构,来选择是否添加lock 指令。(因为如果cpu 有缓存一致性协议,那么将由缓存一致性协议来保证原子操作以及数据一致)
2、内存屏障指令的使用。
1、为什么要 加强了volitale 的语义?
由于volitale 变量可能读取普通变量的值。需要保证普通变量的加载必须要在volitale 读之前先执行。所以需要内存屏障。写同理
2、内存屏障粒度,由于处理器对内存顺序实现不一样。volitale 也需要动态选择使用哪一种屏障指令。比如 :
写读:写指令必须先与后续读完成
读读:前部分读必须先于后部分读完成
读写:前部分读指令必须先于后部分写完成
写写:前部分写必须先于后部分写
3、编译器必须保证编译的时候不能乱序。如果编译的时候乱序了,那么执行的顺序将是错误的。编译器还必须实现针对不同的处理器结构生成正确的内存屏障指令。
三、参考资料
1、《java 并发编程艺术》
2、《深入理解并行编程》
3、《计算机组成与设计/硬件软件接口》
4、 英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A
网友评论