引言

并发编程无疑是编程领域中的上甘岭,他的“难”主要体现在两个方面,从宏观上来讲,主要是如何确定最优化的模型,例如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测试并发程序

Java程序是依赖JVM解释执行,内部还有复杂的JIT优化,这些优化和JVM参数、 版本、以及CPU架构都有关系,和热点代码也有关系,JIT优化对并发测试的影响往往是颠覆式的;另外并发问题往往需要真实竞争,真实竞争指的是多个线程同一时刻在访问共享变量。上面我们的写的测试程序,并不能很好的解决JIT优化、真实竞争的问题。

好在现在已经有了不少并发测试的工具,jcstress是OpenJDK团队开源的并发测试工具,下面我们用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;@Actorvoid actor1() {data = 666;isReady = true;}@Actorvoid actor2(I_Result r) {if (!isReady) {r.r1 = 0;} else {r.r1 = data + 222;}}
}

上面的测试程序,每个用 @Actor 注解的方法都会在一个独立的线程中执行,如果需要获得线程执行后的结果,可以将结果回写到 I_Result 中, @Outcome 注解用来验证 I_Result 中的结果是否符合预期。

在actor2()中,我们没有使用while()循环来检查isReady,而是用了if()语句,其验证效果都是一样,如果actor1()没有准备好计算结果,r.r1设置为0;反之,如果actor1()准备好了计算结果,则设置r.r1=data+222,此时r.r1的预期结果是888,所以888和0都符合我们的预期,而其他值则属于异常。

在Java11版本的JVM上执行上面的测试程序,最终的结果如下图所示。

我们发现有一个异常结果是222,出现222的唯一可能就是 isReady==true 并且 data==0,这怎么可能呢?这不就意味以下代码中的惊天大谎吗?

data = 666;
isReady = true;if (isReady) {//测试结果说明此处data等于0!!!
}

actor1()中,首先设置的data=666,然后才设置的isReady=true,直觉以及多年程序员的经验都告诉我们:后续代码如果能读到isReady==true,那么一定能读到data==666。

03

指令重排导致直觉失效

我们的直觉以及多年程序员的经验,在单线程场景中是正确的,在多线程场景中是不适用的。出于性能的考虑,Java编译器(含JIT)、CPU并没有忠实地按照我们代码的书写顺序执行代码,而是自以为是改变了执行顺序,我们一般把编译器、CPU的这种行为称为指令重排。

例如 我们书写的顺序是:

data=666; isReady=true;

而真实的执行顺序可能是:

isReady=true; data=666;

调整顺序后,在单线程上执行没有任何后遗症,例如单线程执行:

isReady = true;
data = 666;if (isReady) {//单线程中此处data仍然等于666!!!
}

但是在多线程场景中,就不一定了,例如当actor1()在执行完 isReady=true 后(尚没有执行data=666),actor2()执行以下代码:

if (isReady) {r.r1 = data + 222;
}

由于此时data==0,所以就会得到r.r1 == 222。

04

更匪夷所思的编译器优化

前面我们基于jcstress的测试程序没有使用while()循环来检查isReady,而是用了if()语句,为什么要做这种替换呢?直接用while()循环来验证并发问题不是更直接吗?于是我们得到如下的测试程序:

@JCStressTest
@Outcome(id = "888", expect=Expect.ACCEPTABLE, desc="符合预期.")
@Outcome(expect=Expect.ACCEPTABLE_INTERESTING, desc="异常结果.")
@State
public class IsReadyTestError {int data = 0;boolean isReady = false;@Actorvoid actor1() {data = 666;isReady = true;}@Actorvoid actor2(I_Result r){while (!isReady) {};r.r1 = data + 222;}
}

你可以放心地执行上面的测试程序,放心吧无毒,但是在执行上面这个测试程序的时候,你将渐渐听到轰鸣的风扇的声音,然后一直轰鸣下去,我想你应该猜到发生什么了,死循环发生了。上面的代码在 while (!isReady) {}; 上死循环,再没机会跳出了。

怎么会这样?actor1()可能会慢于actor2()的执行,但也定也慢不过1秒,那为什么会发生死循环呢?这其实也是编译优化惹的祸。我们直觉总是以为每次while()循环都会重新在内存中读取isReady这个变量,但是实际上,编译优化后的代码,仅仅在第一次循环时读了一次,之后所有的循环都没重新再去内存中读取isReady这个变量,从而导致while()循环死在此处,再也无法跳出。

05

利用volatile解决有序性问题

上面提到的问题我们该如何解决呢?方案很简单,只要将isReady声明为volatile变量就可以了。

int data=0;

volatile bool isReady=false;

data=666;

isReady=true;

while(!isReady){};

int r=data+222;

对于volatile变量,JVM会禁用指令重排,例如代码的书写顺序是这样:

data=666; isReady=true;

由于isReady是volatile变量,所以JVM会禁止 data=666 重排到 isReady=true 的后面。

同样的道理,以下面顺序书写的代码:

var1=isReady; var2=data;

由于isReady是volatile变量,JVM会禁止 var2=data 重排到var1=isReady的前面。

同时volatile变量还会禁用CPU缓存,不会因为CPU缓存导致可见性问题。

06

总结

在Java领域,编写线程安全的并发程序并不容易,首先我们需要解决的就是直觉失效的问题。有人把我们的认知分为四个境界:知道自己知道,知道自己不知道,不知道自己知道,不知道自己不知道,而最大悲剧在于不知道自己不知道,却以为自己知道,直觉失效就是属于此类,它会引导我们误入歧途。好在这些直觉失效的问题以及解决方案都有迹可循

并发编程中的大坑:你的直觉有序性问题相关推荐

  1. 并发编程中的原子性,可见性,有序性问题

    前言:大家好,我是小威,24届毕业生,在一家满意的公司实习.本篇文章是关于并发编程中出现的原子性,可见性,有序性问题. 本篇文章记录的基础知识,适合在学Java的小白,也适合复习中,面试中的大佬

  2. Java的并发编程中的多线程问题到底是怎么回事儿?

    转载自   Java的并发编程中的多线程问题到底是怎么回事儿? 在我之前的一篇<再有人问你Java内存模型是什么,就把这篇文章发给他.>文章中,介绍了Java内存模型,通过这篇文章,大家应 ...

  3. cas无法使用_并发编程中cas的这三大问题你知道吗?

    在java中cas真的无处不在,它的全名是compare and swap,即比较和交换.它不只是一种技术更是一种思想,让我们在并发编程中保证数据原子性,除了用锁之外还多了一种选择. 一.cas的思想 ...

  4. java内存栅栏_内存屏障(Memory Barriers/Fences) - 并发编程中最基础的一项技术

    我们经常都听到并发编程,但很多人都被其高大上的感觉迷惑而停留在知道听说这一层面,下面我们就来讨论并发编程中最基础的一项技术:内存屏障或内存栅栏,也就是让一个CPU处理单元中的内存状态对其它处理单元可见 ...

  5. JUC里面的相关分类|| java并发编程中,关于锁的实现方式有两种synchronized ,Lock || Lock——ReentrantLock||AQS(抽象队列同步器)

    JUC分类 java并发编程中,关于锁的实现方式有两种synchronized ,Lock AQS--AbstractQueuedSynchronizer

  6. Java并发编程中的若干核心技术,向高手进阶

    来源:http://www.jianshu.com/p/5f499f8212e7 引言 本文试图从一个更高的视角来总结Java语言中的并发编程内容,希望阅读完本文之后,可以收获一些内容,至少应该知道在 ...

  7. Go并发编程中的那些事[译]

    原文地址:Concurrent programming 原文作者:StefanNilsson 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:kobehah ...

  8. volatile关键字——保证并发编程中的可见性、有序性

    文章目录 一.缓存一致性问题 二.并发编程中的三个概念 三.Java线程内存模型 1.原子性 2.可见性 3.有序性 四.深入剖析volatile关键字 1.volatile关键字的两层语义 2.vo ...

  9. AQS理解之五—并发编程中AQS的理解

    AQS理解之五-并发编程中AQS的理解 首先看下uml类图: AbstractOwnableSynchronizer 这个类定义是提供一个创建锁的基础,设置一个排它线程,帮助控制和监控访问. 先看下A ...

最新文章

  1. lucene4.5近实时搜索
  2. 180326新闻:创客授牌仪式新闻稿
  3. python2.7下面字节数组(ByteArray)和16进制字符串(HexString)转化
  4. mac brew install nginx遇到的坑
  5. 白话说编程之java线程
  6. 生活需要懂点技巧…懂点策略…懂点计谋……【心灵悟语】
  7. method swizzling你应该注意的点
  8. 第三方库之 - SDWebImage
  9. Redis 缓存 + Spring 的集成示例
  10. Silverlight 下载
  11. Windows 使用 Detours 进行 HOOK
  12. 学习Android studio时的报错Binary XML file line #10: Error inflating class fragment
  13. 明源云客微信抢房技巧_明源云客车位线上开盘体验 - 微信抢房_软件抢房_网上选房_手机抢房_代抢房 - 爱抢房...
  14. Docker 学习笔记(八)-- Dockerfile 构建CentOS 实战测试
  15. [转]稳定排序和不稳定排序
  16. 游泳馆会员管理系统功能图
  17. SAP FIに関する専門用語①
  18. ios上1像素的问题
  19. TiKV源码分析(一)RaftKV层
  20. 状态空间表示法----野人与修道士

热门文章

  1. 协议模型的最底层是_CAN通信协议栈(二) 之对ISO11898-1的理解
  2. 如何禁止页面被 jframe 引用_PD1该如何使用?靶向能否转用PD1?
  3. mysql 5.7 收费_MySQL5.7 常用用户操作
  4. android 关闭jack_Android7.0 配置JACK支持多用户同时编译
  5. 电气毕业什么都不会怎么办?电气专业毕业的都去干什么了?
  6. 【Java】面试高频考题---topK问题详解(堆heap求解)
  7. 使用C++基于Socket编程实现文件下载(改进-封装成类)
  8. 关于ValueError: Unknown projection ‘3d‘报错的解决方法
  9. PTA数据结构与算法题目集(中文)7-39
  10. 不若鸿蒙的意思,任正非说鸿蒙媲美iOS不用三年,华为若出鸿蒙手机你会买吗?...