内存可见性
如何保证内存可见性?
线程操作共享资源时会将资源所在的整个cache line
(64byte)从主内存区拷贝到当前线程的工作内存区(CPU缓存)中,当线程1修改变量后,线程1对当前变量的状态被更改为modified
,然后将当前变量值写回主内存,同时将其它持有当前变量线程的cache line
的状态修改为invalid
,当其它线程使用到当前变量时候由于工作内存(CPU缓存)中的值已经为invalid
状态,因此需要重新从主内存中读取新的值。以此来实现内存可见性。
示例:
class JCounter {
int value = 0;
public void increase() {
value = 10;
}
}
public class SyncDemo {
public static void main(String[] args) {
JCounter counter = new JCounter();
//t1线程休眠1s后修改值
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
counter.increase();
System.out.println(counter.value + "*********************");
});
t1.start();
//t2线程循环检测变量值
Thread t2 = new Thread(() -> {
//while循环检测变量值
while (counter.value == 0){}
System.out.println("over");
});
t2.start();
}
}
执行上面代码,发现程序在t1线程修改变量值后,t2线程并无法感知值已经被修改而一直处于循环等待中。
解决方案:只需将JCounter中value变量使用voliate
关键字修饰,就能保证value在修改后能及时刷新到主内存以及CPU缓存的更新。
禁止指令重排序
何为指令重排?
众所周知,CPU的计算单元的速度是整个计算机体系结构中最快的单元,CPU的执行所需要的资源、指令需要从内存中获取,但是内存的速度相比于CPU的速度是很慢的,因此CPU在从内存中获取数据的是会出现等待,为了提升CPU的效率,CPU会在这个等待期间执行下一条指令(下一条指令不依赖上一条指令的情况下),因此会出现指令重排序的现象;
volatile
关键字会在JVM层级使用内存屏障实现禁止指令重排序,在volatile
修饰的变量执行区域的前后插入屏障,屏障内的指令执行必须依次进行,以此实现禁止指令重排序。
屏障指令类型包括:
- LoadLoad
- StoreStore
- LoadStore
- StoreLoad