了解什么是可见性错误,为什么会发生,以及如何在并发Java应用程序中查找难以捉摸的可见性错误。这些问题你可能也遇到过,当在优锐课学习了一段时间后,我对这些问题有了一定见解,写下这篇文章和大家分享。

检测可见性错误的机会各不相同。在最佳情况下,可以在所有情况的90%中检测到以下可见性错误。在最坏的情况下,检测错误的机会低于百万分之一。

但是首先,什么是可见性错误?

什么是可见性错误?

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

 public class Termination {
private int v;
public void runTest() throws InterruptedException {
Thread workerThread = new Thread( () -> {
while(v == 0) {
// spin
}
});
workerThread.start();
v = 1;
workerThread.join(); // test might hang up here
}
public static void main(String[] args) throws InterruptedException {
for(int i = 0 ; i < 1000 ; i++) {
new Termination().runTest();
}
}
}

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

读取过时的值的原因之一是CPU内核的缓存。现代CPU的每个内核都有自己的缓存。因此,如果读取和写入线程在不同的内核上运行,则读取线程将看到缓存的值,而不是写入线程写入的值。 下面显示了超级用户答案给出的Intel Pentium 4 CPU内部的内核和缓存:

Intel Pentium 4 CPU的每个核心都有自己的1级和2级缓存。所有内核共享一个大的3级缓存。这些缓存的原因是性能。下列数字显示了访问内存所需的时间,摘自《计算机体系结构,一种定量方法》,JL Hennessy,DA Patterson,第5版,第72页:

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

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

如何重现可见性错误

如果你运行了上面的示例,则很有可能该测试无法挂断。该测试只需要很少的CPU周期,因此两个线程通常都在同一内核上运行,并且当两个线程在同一内核上运行时,它们将读取和写入同一缓存。幸运的是,OpenJDK提供了jcstress工具,可以帮助进行这种类型的测试。jcstress使用多种技巧,以便测试的线程在不同的内核上运行。这里,上面的示例被重写为jcstress测试:

 @JCStressTest(Mode.Termination)
@Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE, desc = "Gracefully finished.")
@Outcome(id = "STALE", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Test hung up.")
@State
public class APISample_03_Termination {
int v;
@Actor
public void actor1() {
while (v == 0) {
// spin
}
}
@Signal
public void signal() {
v = 1;
}
}

此测试来自jcstress示例。通过使用注解@JCStressTest对该类进行注解,我们告诉jcstress此类是jcstress测试。jcstress在单独的线程中运行以@Actor@Signal注释的方法。jcstress首先启动actor线程,然后运行信号线程。如果测试在合理的时间内退出,则jcstress记录"TERMINATED"结果;否则,结果为"STALE."

jcstress使用不同的JVM参数多次运行测试用例。这是在我的开发机器(使用测试模式压力的Intel i5 4核CPU)上进行此测试的结果。

对于JVM参数-XX:-TieredCompilation,在所有情况下90%都挂起线程,但是对于JVM flags -XX:TieredStopAtLevel=1 and -Xint,该线程在所有运行中终止。

在确认我们的示例确实包含一个错误之后,我们如何解决它?

如何避免可见性错误

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

 public class Termination {
volatile int v;
// methods omitted
}

volatile字段修饰符不是给出此类可见性保证的唯一指令。例如,包java.util.concurrent中的synced语句和类提供相同的保证。Brian Goetz等人撰写的《Java Concurrency in Practice》一书很好地了解了避免可见性错误的技术。

在了解了可见性错误发生的原因以及如何重现和避免它们之后,让我们看一下如何查找它们。

如何查找可见性错误

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

“两个动作可以通过在发生之前的关系进行排序。如果一个动作在另一个发生之前,则第一个对第二个可见并且在第二个之前进行排序。”

读取和写入易失性字段会创建这样的事前关联:

“在每次对该字段进行后续读取之前,都会对易失字段(第8.3.1.4节)进行写操作。”

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

“当程序包含两个冲突访问(第17.4.1节)时,它们之间没有按事前发生的关系排序,则该程序被称为包含数据竞争。对同一变量的两次访问(读或写)被称为:如果至少有一个访问是写操作,则冲突。”

在我们的示例中,我们看到对共享变量v的读取和写入之间没有“先发生后”关系,因此该示例包含根据规范的数据竞争。

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

  • ThreadSanitizer使用C ++内存模型的规则来查找C ++应用程序中的可见性错误。C ++内存模型由正式规则组成,用于指定C ++指令的可见性保证,类似于Java语言规范对Java指令所做的保证。有一个Java增强建议的草案,即JEP草案:Java Thread Sanitizer,将ThreadSanitizer包含在OpenJDK JVM中。 应该通过命令行标志启用ThreadSanitizer的使用。
  • vmlens, 是我编写的用于测试并发Java的工具,它使用Java语言规范自动检查Java测试运行是否包含可见性错误。

在并发Java应用程序中检测可见性错误的更多相关文章

  1. 在 Java 应用程序中使用 Elasticsearch

    如果您使用过 Apache Lucene 或 Apache Solr,就会知道它们的使用体验非常有趣.尤其在您需要扩展基于 Lucene 或 Solr 的解决方案时,您就会了解 Elasticsear ...

  2. 在Java Web程序中使用监听器可以通过以下两种方法

    之前学习了很多涉及servlet的内容,本小结我们说一下监听器,说起监听器,编过桌面程序和手机App的都不陌生,常见的套路都是拖一个控件,然后给它绑定一个监听器,即可以对该对象的事件进行监听以便发生响 ...

  3. 在Java Web程序中使用Hibernate

    在Java Web程序中使用Hibernate与普通Java程序一样.本文中将使用Servlet和JSP结合Hibernate实现数据库表的增删改查操作. Web程序中,hibernate.cfg.x ...

  4. 在 Java 应用程序中绑定 Bean 和数据

    本指南介绍了 NetBeans IDE 对 Java 应用程序中 Bean 绑定和数据绑定的支持. 要学完本教程,您需要具备以下软件和资源. 软件或资源 要求的版本 NetBeans IDE 版本 7 ...

  5. Java应用程序中的声音播放

    声音可以创造意境,触发遐想,当与虚拟图像相结合时,更加可以让整个世界充满幻觉,声音是多媒体技术的基础. 播放声音是Java对多媒体的支持一个重要部分,它支持的声音文件类型主要有: AU - (扩展名为 ...

  6. 在 Java 应用程序中加一些 Groovy 进来

    如果您一直在阅读这个系列,那么您应该已经看到有各种各样使用 Groovy 的有趣方式,Groovy 的主要优势之一就是它的生产力.Groovy 代码通常要比 Java 代码更容易编写,而且编写起来也更 ...

  7. “/wechat”应用程序中的服务器错误。

    对路径“C:\inetpub\wwwroot3\wechat\img\qrcode\”的访问被拒绝. “/wechat”应用程序中的服务器错误. 对路径“C:\inetpub\wwwroot3\wec ...

  8. Error-MVC: “/”应用程序中的服务器错误。

    ylbtech-Error-MVC: “/”应用程序中的服务器错误. 1.返回顶部 1. “/”应用程序中的服务器错误. 运行时错误 说明: 服务器上出现应用程序错误.此应用程序的当前自定义错误设置禁 ...

  9. 应用程序中的server错误,没有名称为“ServiceBehavior”的服务行为

    应用程序中的server错误,没有名称为"ServiceBehavior"的服务行为         今天在阅读"创建和使用Web服务"的相关内容,在浏览器中查 ...

随机推荐

  1. java虚拟机规范(se8)——java虚拟机的编译(四)

    3.12 抛出和处理异常 在程序中使用throw关键字来抛出异常.编译结果很简单. void cantBeZero(int i) throws TestExc { if (i == 0) { thro ...

  2. 【目录】sql server 进阶篇系列

    随笔分类 - sql server 进阶篇系列 sql server 下载安装标记 摘要: SQL Server 2017 的各版本和支持的功能 https://docs.microsoft.com/ ...

  3. kubernetes里面有时候centos源用不了

    kubernetes里面有时候centos源用不了,快速配一个阿里云的源. mkdir /etc/yum.repos.d/yangback;mv /etc/yum.repos.d/* /etc/yum ...

  4. SQL数据库— <3>高级查询、常用函数 --摘录网络

    SQL Server T-SQL高级查询 高级查询在数据库中用得是最频繁的,也是应用最广泛的. Ø 基本常用查询 --select select * from student; --all 查询所有 ...

  5. onLaunch与onLoad同步获取用户数据

    前言 在开发项目的时候遇到从全局获取用户信息,逻辑是从app.js中的onLauch获取,page页面的onLoad拿到数据填充到页面.遇到的问题是onLauch与onLoad是异步的,没办法从页面判 ...

  6. 初学Vue

    指令.组件.数据 指令 指令带有前缀,v-表示它们是Vue提供的特殊属性.如: v-bind:绑定元素属性,如:v-bind:title v-if:条件,绑定DOM 的结构 v-for:循环,使用Ar ...

  7. mysql---级联更新和删除操作

    我们通常有这样的需求:删除表Table 1中记录,需要同时删除其它表中与Table 1有关的若干记录. 对于这种,我们有两种解决方法: 一,使用innodb表的外键约束 ALTER TABLE `sc ...

  8. @PathVariable、@RequestParam、@RequestBody注解

    讲解更加详细的参考资料 https://blog.csdn.net/u011410529/article/details/66974974 https://www.cnblogs.com/soul-w ...

  9. Struts2关于命名空间的例子

    佐证了这样一个原则,package存在,但action没找到,就自动去默认空间去找.如果package不存在,则自动向上一级目录找,一级级倒到根目录.  根目录再没找到,再去默认目录找 网上对于命名空 ...

  10. 图解SSH原理_20190613

    SSH仅仅是一协议标准,其具体的实现有很多,既有开源实现的OpenSSH,也有商业实现方案.使用范围最广泛的当然是开源实现OpenSSH. 2. SSH工作原理 在讨论SSH的原理和使用前,我们需要分 ...