并发编程中的大坑:你的直觉&有序性问题
The following article is from 京东技术 Author 京东物流-王宝令
并发编程无疑是编程领域中的上甘岭,他的“难”主要体现在两个方面,从宏观上来讲,主要是如何确定最优化的模型,例如Redis是单线程模型,Nginx是多进程单线程模型,而Netty是主从Reactor多线程模型;从微观上来讲,主要是原子性、可见性、有序性等问题的纠缠,这些问题有一个共同点,就是直觉失效。我们大部分情况下都是靠直觉来写程序的,如果直觉失效,会意味着什么呢?意味着直觉在引导我们写bug,引导我们误入歧途。今天我们就重点来聊聊直觉失效的问题之一:有序性问题。相信你看完这篇文章,肯定会大吃一惊:“原来一不小心写了这么多bug!”好在解决方案还是很简单的,只要了解了原理就可能轻松搞定。
01
一个简单的并发程序
在下面的代码中,线程T1执行一个计算任务(简化为data=666),任务完成后通过isReady标识结束了,线程T2等待线程T1完成计算任务(while (!isReady) {}),当线程T2观察到isReady为true时,执行后续任务(简化为r = data + 222),那线程T2能得到预期的结果r==888吗?
int data=0; bool isReady=false; | |
data=666; isReady=true; | while(!isReady){}; int r = data+222; |
直觉告诉我们能看到预期的结果888,我们的直觉源自缜密的逻辑推导:线程T1中,首先对data进行了赋值操作,后对isReady进行了赋值操作,所以线程T2中观察到 isReady==true 时,data一定等于666,然后就会得到 r==888。
仅有理论推导还不够,最好跑个程序测试一下,理论联系实践,双保险。于是我们又写了下面这个验证程序,执行数次,并没有发现打印出异常数据。于是我们终于可以得出结论:一切OK!
boolean isReady=false;
int data = 0;
int r;
public void main(String[] args)
throws InterruptedException {
for (int i=0; i<10000; i++) {
Thread t1=new Thread(()->{
data = 666;
isReady = true;
});
Thread t2=new Thread(()->{
while (!isReady) {};
r = data + 222;
});
t2.start();
t1.start();
t2.join();
if (r != 888) {
System.out.println(r);
}
}
}
当然了,这一起都是假象,理论推导,其过程没有错,但是假设条件有问题;实践代码也没有问题,但是不够全面。我们先从实践代码开始剖析。
02
用jcstress测试并发程序
@JCStressTest
@Outcome(id = "888",
expect=Expect.ACCEPTABLE,
desc="符合预期.")
@Outcome(id = "0",
expect=Expect.ACCEPTABLE,
desc="符合预期.")
@Outcome(
expect=Expect.ACCEPTABLE_INTERESTING,
desc="异常结果.")
@State
public class IsReadyTest {
int data = 0;
boolean isReady = false;
@Actor
void actor1() {
data = 666;
isReady = true;
}
@Actor
void actor2(I_Result r) {
if (!isReady) {
r.r1 = 0;
} else {
r.r1 = data + 222;
}
}
}
data = 666;
isReady = true;
if (isReady) {
//测试结果说明此处data等于0!!!
}
03
指令重排导致直觉失效
isReady = true;
data = 666;
if (isReady) {
//单线程中此处data仍然等于666!!!
}
if (isReady) {
r.r1 = data + 222;
}
04
更匪夷所思的编译器优化
@JCStressTest
@Outcome(id = "888",
expect=Expect.ACCEPTABLE,
desc="符合预期.")
@Outcome(
expect=Expect.ACCEPTABLE_INTERESTING,
desc="异常结果.")
@State
public class IsReadyTestError {
int data = 0;
boolean isReady = false;
@Actor
void actor1() {
data = 666;
isReady = true;
}
@Actor
void actor2(I_Result r){
while (!isReady) {};
r.r1 = data + 222;
}
}
05
利用volatile解决有序性问题
06
总结
06
总结
参考阅读:
本文由高可用架构翻译。技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。