并发编程之有序性
经典案例1:
import java.util.concurrent.CountDownLatch;
public class T01_Disorder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
for (long i = 0; i < Long.MAX_VALUE; i++) {
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(2);
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
latch.countDown();
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
latch.countDown();
}
});
one.start();
other.start();
latch.await();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
}
}
}
}
分析:
只有先执行 x = b;和 y = a;后执行a = 1;b = 1;才有可能造成x=0,y=0的情况
为何要乱序?
- 在指令1等待返回的时间里,cpu可以先执行指令2的操作,
- 所以最后从结果来看,可能会出现指令2先完成,指令1后完成的情况.
乱序的原则:
- as-if-serial:看上去像是序列化(单线程)
单个线程,两条语句,未必是按顺序执行
单线程的重排序,必须保证最终一致性
经典案例2:
public class T02_NoVisibility {
private static boolean ready = false;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) throws Exception {
Thread t = new ReaderThread();
t.start();
number = 42;
ready = true;
t.join();
}
}
2大隐患问题:
- 给标志位ready 加上volatile 保证线程的可见性
- 有可能先执行ready = true;输出number=0
线程乱序会引发的问题
对象的半初始化状态
- new 的时候,对象半初始化
- m成员变量赋 ,默认值0
- 调用构造方法,成员变量赋初始值m=8
- 变量t指向new对象地址
看一个例子: this溢出
public class T03_ThisEscape {
private int num = 8;
public T03_ThisEscape() {
new Thread(() -> System.out.println(this.num)
).start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape();
System.in.read();
}
}
有可能输出num=0
分析:
仔细看汇编码指令:
- 指令—4 invokespecial 和指令— 7 astore_1,有可能乱序执行
- 即成员变量还没完成显性初始化的时候,变量 t (也可以看做this关键字),就已经指向半初始转态的对象了.
- 半初始转态的对象,只完成了默认初始化,属性num赋默认值0
结论 : 不要在构造方法中启动线程,可以new
public class T03_ThisEscape {
private int num = 8;
Thread t;
public T03_ThisEscape() {
t= new Thread(() -> System.out.println(this.num)
);
}
public void run(){
t.start();
}
public static void main(String[] args) throws Exception {
T03_ThisEscape t03_thisEscape = new T03_ThisEscape();
t03_thisEscape.run();
}
}
哪些指令可以互换顺序?
cpu级别:
不影响单线程的最终一致性
java— JVM级别:
hanppens-before原则(JVM规定重排序必须遵守的规则)
JVM规定重排序必须遵守的规则
怎样阻止乱序执行?
cpu级别:
内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行
- intel : lfence sfence mfence(CPU特有指令)
java— JVM级别:
volatile的底层实现
volatile两大作用:
1.保持线程可见性
2.禁止指令的重排序
hotspot实现
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
- LOCK 用于在多处理器中执行指令时对共享内存的独占使用。
- 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。
- 另外还提供了有序的指令无法越过这个内存屏障的作用。
1:保证线程的可见性
使用volatile,将会强制所有线程都会去堆内存中读取running的值
- 大家知道java里面是有堆内存的,堆内存是所有线程共享里面的内存,除了共享的内存之外呢,每个线程都有自己的专属的区域,都有自己的工作内存,如果说在共享内存里有一个值的话,当我们线程,某一个线程都要去访问这个值的时候,会将这个值copy一份,copy到自己的这个工作空间里头,然后对这个值的任何改变,首先是在自己的空间里进行改变,什么时候写回去,就是改完之后会马上写回去。什么时候去检查有没有新的值,也不好控制。=
- 在这个线程里面发生的改变,并没有及时的反应到另外一个线程里面,这就是线程之间的不可见 ,对这个变量值加了volatile之后就能够保证 一个线程的改变,另外一个线程马上就能看到。
2:禁止指令重新排序
- 指令重排序也是和cpu有关系,每次写都会被线程读到,加了volatile之后。cpu原来执行一条指令的时候它是一步一步的顺序的执行,但是现在的cpu为了提高效率,它会把指令并发的来执行,第一个指令执行到一半的时候第二个指令可能就已经开始执行了,这叫做流水线式的执行。在这种新的架构的设计基础之上呢想充分的利用这一点,那么就要求你的编译器把你的源码编译完的指令之后呢可能进行一个指令的重新排序。
- 这个是通过实际工程验证了,不仅提高了,而且提高了很多。