并发编程(三)volatile禁止重排序原理
2021-06-06 15:05
标签:lazy 替代 例子 net current 64bit fonts get details 上篇文章记录到volatile在硬件层面怎么保证线程间可见性的,是通过lock锁缓存行缓存一致性协议来实现的。但是这样会有一个伪共享的问题。 首先缓存行在64bit机中一般为64字节,具体缓存行大小可以通过下面的命令查看: 假设有一个对象有两个long类型的数据x,y,在java中long类型数据长度为8个字节,所以读取一个对象数据后x,y会在一个缓存行中。有两个CPU读取这个对象如下图的Core1,Core2,其中Core1只对x进行修改,Core2只对y进行修改。 根据缓存一致性协议知道,当Core1中数据修改后会导致Core2中的缓存行失效,Core2中的数据就要从主存中重新获取,同理:当Core2中数据修改后会导致Core1中的缓存行失效,Core1中的数据就要从主存中重新获取。两核之间存在缓存行的竞争关系。 这会导致性能损耗。下面举个例子看下这种情况。 上面的代码执行结果: 将近三秒执行。 为了避免让x,y在同一个缓存行,我们可以把x,y分开在不同缓存行,可以通过在x变量后面添加七个long类型变量,这样x,y就位于不同的缓存行上了。 如下: 这样运行结果看下: 速度提升还是很明显的。 但是上面消除伪共享的方法不太合适,还要再单独定义 其它变量。在jdk8中有新的方式可以消除伪共享的情况。 就是: 这种方式的执行时间也提升了很多。 我上面以及上一篇都是在讲volatile的可见性是如何保证的,volatile还有一种作用就是有序性。 我们可以从double check中看到场景。 这个要说下对象的创建过程说起了,对象创建也就是说new这个操作JVM到底做了什么事,这个问题之前在CSDN上记录过可以去看下:https://blog.csdn.net/A7_A8_A9/article/details/105730007 我把new操作的汇编贴一下: JIT可能会对指令重排序,处理器也有可能是乱序执行,因为这样可以提升性能。对于如何提升性能的我们可以从下面的伪代码中窥探一二: 右边调整了顺序之后可以减少i的加载和store,虽然例子只是一个int变量,如果是一个很大的对象/数据,这个节约的时间就是值得的。这种指令重排是JIT编译之后的结果。处理器的乱序执行是当前指令在Load数据等待中,cpu可能先执行下面的指令了。 为什么JIT会进行重排序呢?我们知道我们java文件编译成class文件,然后JVM编译成指令序列再到汇编指令,JVM有两个编译方式,一种是读取一行解释一行,这种效率自然是很慢的,还有一种是JIT即时编译它会检测一些频繁使用的代码编译成热点代码,这样不用每次使用都要编译了。 指令重排的重现例子: 结果: 指令重排出现了。 为了避免指令重排,可以给变量加上volatile关键字,禁止重排序,保证有序性也保证了可见性。我们从上篇知道,可见性它是靠Lock,触发缓存一致性协议保证的。禁止指令重排也是靠这个Lock保证的, 它起到了内存屏障的作用,它虽然不是内存屏障指令。注意在linux_x86的系统中内存屏障mfence用lock来替代了。 我们也可以手动添加内存屏障来禁止重排序,使用Unsafe类。 使用的地方。 storefence是本地方法,我们可以去看下jdk的源码。 可以看到 jvm屏蔽掉了不同的cpu。 我以linux_x86为例来看。 查看orderAccess 我们看到定义了四种"内存屏障操作"但是对于x86处理器来说,它只关心sotreload这一种内存屏障,其它的它不管。这里我们只要看fence就可以了, if里判断是否是多核处理架构,根据上面的注释也可以知道使用 lock; addl来代替mfence。这是x86里面的内存屏障实现方式。 Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令
为什么只关注StoreLoad指令呢,因为在JSR-133规范中定义了x86内核是如何对内存屏障和原子性进行支持的。 x86就是靠lock指令实现的。具体这个的解释如下 ,参考了:https://www.zhihu.com/question/65372648 JMM模型就是在先行发生模型上面的优化。线程的可见性,有序性,原子性都可以从先行发生模型来考虑。 我们从一个例子来看下: 我们约定线程A执行write(),线程B执行read(),且线程A优先于线程B执行(时间先后顺序),那么线程B获得结果是什么?这段代码是线程安全的吗 按上面的执行是线程不安全的,虽然线程A先于线程B执行,但是线程B对于线程A的执行结果确实不可见的,这也是先行发生模型强调的点,这个先行不是代码执行时间上的先后顺序而是执行结果是顺序的。 线程内存模型有: 并发编程(三)volatile禁止重排序原理 标签:lazy 替代 例子 net current 64bit fonts get details 原文地址:https://www.cnblogs.com/krock/p/14596247.htmlcat /proc/cpuinfo
public class HuanCun {
public static void main(String[] args) throws InterruptedException {
testPointer(new Pointer());
}
private static void testPointer(Pointer pointer) throws InterruptedException {
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i 100000000; i++) {
pointer.x++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i 100000000; i++) {
pointer.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(System.currentTimeMillis() - start);
}
}
class Pointer {
// 避免伪共享: @Contended + jvm参数:-XX:-RestrictContended
//@Contended
volatile long x;
//避免伪共享: 缓存行填充
//long p1, p2, p3, p4, p5, p6, p7;
volatile long y;
}
避免伪共享: @Contended + jvm参数:-XX:-RestrictContended
volatile的有序性
JMM中关于synchronized有如下规定,线程加锁时,必须清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取;线程在解锁时,需要把工作内存中最新的共享变量的值写入到主存,以此来保证共享变量的可见性。(ps这里是个泛指,不是说只有在退出synchronized时才同步变量到主存)。
那么加了 synchronized, myInstance的可见性已经保证了,为什么还要加上volatile呢? private volatile static SingletonFactory myInstance;
public static SingletonFactory getMyInstance() {
if (myInstance == null) {
synchronized (SingletonFactory.class) {
if (myInstance == null) {
myInstance = new SingletonFactory();
}
}
}
return myInstance;
} private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException{
int i=0;
while (true) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
/**
* x,y:
* 每次循环,x,y,a,b都会赋初值0,两个线程各执行一次,执行完,x,y可能的值有:
* 0,1 1,0 1,1 这是我们按正常逻辑思考可能出现的结果,但是有一种情况是只会在重排序的情况出现
* 就是 0,0 也就是这种情况下两个线程变成了这样的逻辑。
* thread1:
* x=b;
* a=1;
* thread2:
* y=a;
* b=1;
* 我们执行看下是否会出现这样的情况呢?
*/
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(20000);
a = 1;
x = b;
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("第" + i + "次(" + x + "," + y + ")");
if (x==0&&y==0){
break;
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
public class UnsafeUtil {
public static Unsafe getUnsafe(){
try {
Field field= Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe)field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
先行发生模型--happens-before
private int i = 0;
public void write(int j ){
i = j;
}
public int read(){
return i;
}
上一篇:golang线程池【简易版】
下一篇:C++之类和对象
文章标题:并发编程(三)volatile禁止重排序原理
文章链接:http://soscw.com/index.php/essay/91333.html