三级缓存
volatile 和 三大并发问题
-
原子性
-
可见性
-
有序性
原子性
public class ThreadSafeInteger {
private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
public class ThreadSafeInteger {
private volatile int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
可见性
public class Visibleness {
private boolean running = true;
void func() {
System.out.println("thread start……");
while (running){}
System.out.println("thread end……");
}
public static void main(String[] args) throws InterruptedException {
System.out.println("main start……");
Visibleness object = new Visibleness();
new Thread(object::func,"task01").start();
Thread.sleep(2 * 1000);
object.running = false;
System.out.println("main end……");
}
}
//输出:
main start……
thread start……
main end……
有序性
lock前缀
lock add1 $0x0,(%esp)
1、会将处理器的缓存行立刻写入到内存中。
2、将其他内核缓存了该内存地址的数据设置为无效。
接下来探究一下lock前缀为什么能解决可见性和有序性问题。
Lock指令有两种实现方式:总线锁和缓存一致性机制。
总线锁
1、CPU总线负责CPU和外部(高速缓存、内存等)通信。
2、使用总线锁会选择一个核心独占总线,其他内核不能和内存通信。
3、对于跨缓存行的数据使用总线锁。(缓存行后面会讲)
缓存一致性机制
空间局部性是指被引用过的存储器位置附近的数据很可能将被引用;
int[][] array = new int[64 * 1024][1024];
long startTime1=System.currentTimeMillis(); //获取开始时间
// 横向遍历
for(int i = 0; i < 64 * 1024; i ++)
for(int j = 0; j < 1024; j ++)
array[i][j] ++;
long endTime1=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间:"+(endTime1-startTime1)+"ms");
// 纵向遍历
long startTime2=System.currentTimeMillis(); //获取开始时间
for(int i = 0; i < 1024; i ++)
for(int j = 0; j < 64 * 1024; j ++)
array[j][i] ++;
long endTime2=System.currentTimeMillis(); //获取结束时间
System.out.println("程序运行时间:"+(endTime2-startTime2)+"ms");
因为有缓存行的存在,顺序读取数据比无序读数据快很多
程序运行时间: 176ms
程序运行时间: 2513ms
缓存结构
-
一个缓存有S个组
-
一个组有E个缓存行
-
每个缓存行包含三部分:vaild、tag和block
-
vaild 用于标识该数据的有效性
-
tag 用于指示数据对应的内存地址
-
block 用于存储数据(64byte)
MESI(缓存一致性协议)
写失效:当一个内核修改了数据,其他内核如果有这份数据,就把valid标识为无效
写更新:当一个内核修改了数据,其他内核如果有这份数据,就更新为新值,标记valid有效。
缓存行状态
当前缓存行中的数据和主存中的数据不一致,需要在某个时间写回到主存中。
-
内核0修改data的数据为0,会先向所有内核广播,告诉其他内核要修改的数据
-
内核1接收内核0的广播消息之后,会检查缓存中的包含该数据的缓存行
-
内核1将查找到的缓存行删掉或者设置为无效状态
-
内核0收到内核1的反馈后,会将数据保存到缓存行中,并修改valid为E或者M
-
内核0保存数据到内存中。
存储缓冲(Store Buffer)
无效队列(Invalidate Queue)
当其他CPU收到无效指令时,不需要确认缓存行是否真正失效,而是先放到Invalidate Queue中,并返回无效指令确认;等待CPU空闲时再处理Invalidate Queue中的无效指令。
Invalidate Queue的引入解决了CPU不能及时回复消息的问题,但也带来了一些问题,比如未及时将缓存行设置为无效状态并使用了该缓存行。
为此,针对Invalidate Queue,执行后需要等待Invalidate Queue完全应用到内存后,后续读操作才继续执行,保证执行前后的读操作对其他CPU是顺序的,这也称为内存屏障中的读屏障(Load Barrier)。
读屏障:获取其他内核修改,让当前内核中的数据为最新的值,也就是将Invalidate Queue中的数据应用到内核。
写屏障:将内核的修改让其他内核可见,将storeBuffer的数据写入缓存/内存中
CAS
lock cmpxchg
备注
原文始发于微信公众号(Java不惑):一文带你深入了解volatile
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24503.html