性能文章>【译】Java并发编程之可见性错误详解>

【译】Java并发编程之可见性错误详解转载

2年前
384557

检测可见性错误的概率很随机。

运气好,90%的案例中可以检测到可见性错误。运气差,检测错误的机会低于百万分之一。

那么,什么是可见性错误?

可见性错误

当线程读取陈旧的值时,就会发生可见性错误。在以下示例中,线程向另一个线程发出信号,以停止处理其while循环:

 

问题是,工作线程可能永远不会看到变量v的更新,因此永远运行。

读取陈旧值的一个原因是CPU内核的缓存。现代CPU的每个核心都有自己的缓存。因此,如果读取和写入线程在不同的内核上运行,则读取线程会看到缓存值,而不是写入线程写入的值。以下显示了英特尔奔腾4 CPU内部的核心和缓存:

图片标题

英特尔奔腾4 CPU的每个核心都有自己的1级和2级缓存。所有内核共享一个大型的3级缓存。这些缓存的原因是性能。

以下数字显示了访问内存所需的时间,来自计算机架构、定量方法、JL Hennessy、DA Patterson,第5版,第72页:

  • CPU寄存器~300皮秒
  • 1级缓存~1纳秒
  • 主内存~50-100纳秒

读取和写入普通字段不会使缓存无效,因此,如果不同核心上的两个线程读取和写入同一变量,它们会看到陈旧的值。让我们看看是否可以重现这个错误。

如何重现?

如果你运行了上述示例,测试不会挂断的可能性很大。该测试需要如此少的CPU周期,以至于两个线程通常在同一内核上运行,当两个线程在同一内核上运行时,它们会读取和写入到相同的缓存。

幸运的是,OpenJDK提供了一个工具jcstress,它有助于进行此类测试。jcstress使用多个技巧,以便测试的线程在不同的核心上运行。在这里,上述示例被重写为jcstress测试:

 

这个测试来自jcstress示例。通过用注释@JCStressTest注释该类,我们告诉jcstress,该类是一个jcstress测试。jcstress在一个单独的线程中运行带有@Actor@Signal注释的方法。jcstress首先启动演员线程,然后运行信号线程。如果测试在合理的时间内退出,jcstress会记录“终止”结果;否则,结果“陈旧”。

jcstress使用不同的JVM参数多次运行测试用例。以下是我开发机器上这次测试的结果,这是一个使用测试模式应力的英特尔i5 4核CPU。

图片标题

对于JVM参数-XX:-TieredCompilation,线程在90%的情况下挂断,但对于JVM标志-XX:TieredStopAtLevel=1和-Xint,线程在所有运行中都终止。

在确认我们的示例确实包含错误后,我们如何修复它?

如何避免?

Java有专门的指令,可以保证线程始终看到最新的写入值。其中一条指令是易失性字段修饰符。当读取易失性字段时,线程保证看到最后一个写入值。该保证不仅适用于字段的值,也适用于写入线程在写入易失变量之前写入的所有值。从上述示例中将字段修饰符volatile添加到字段v中,确保while循环始终终止,即使使用jcstress的测试运行。

 

volatile场修饰符并不是提供这种可见性保证的唯一指令。例如,软件包java.util.concurrent中的同步语句和类提供了相同的保证。Brian Goetz等人的《实践中的Java并发》一书是一本关于避免可见性错误的技术的好读物。

在了解了为什么会发生可见性错误以及如何重现和避免它们后,让我们看看如何找到它们。

如何查找?

Java语言规范第17章。线程和锁正式定义了Java指令的可见性保证。该规范定义了所谓的“发生前”关系,以定义可见性保证:

“两个操作可以由发生前关系排序。如果一个动作发生在另一个动作之前,那么第一个动作在第二个动作之前是可见的,并有序的。”

从波动场读取和写入会产生这样的事前关系:

在每次读取该字段之前,都会发生写入到不稳定字段(§8.3.1.4)。

使用此规范,我们可以检查程序是否包含可见性错误,在规范中称为“数据竞赛”。

“当程序包含两个不按先发生关系排序的冲突访问(§17.4.1)时,它被称为包含数据竞赛。如果至少有一个访问是写入,则对同一变量的两个访问(读取或写入)称为冲突。”

看看我们的示例,我们发现读取和写入共享变量v之间没有发生前关系,因此此示例包含根据规范的数据竞赛。

当然,这种推理可以自动化。以下两个工具使用此规则自动检测可见性错误:

  • ThreadSanitizer使用C++内存模型的规则来查找C++应用程序中的可见性错误。C++内存模型由形式规则组成,用于指定类似于Java语言规范对Java指令的C++指令的可见性保证。Java增强提案草案JEP草案:Java Thread Sanitizer,将ThreadSanitizer纳入OpenJDK JVM。ThreadSanitizer的使用应通过命令行标志启用。
  • vmlens是我为测试并发Java而编写的工具,它使用Java语言规范自动检查Java测试运行是否包含可见性错误。
点赞收藏
willberthos

keep foolish!

请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

7
5