了解什么是可见性错误,为何会发生,以及如何在并发Java应用程序中查找难以捉摸的可见性错误。这些问题你可能也遇到过,当在优锐课学习了一段时间后,我对这些问题有了必定看法,写下这篇文章和你们分享。java
检测可见性错误的机会各不相同。在最佳状况下,能够在全部状况的90%中检测到如下可见性错误。在最坏的状况下,检测错误的机会低于百万分之一。git
可是首先,什么是可见性错误?github
当线程读取陈旧值时,会发生可见性错误。在如下示例中,一个线程向另外一个线程发出信号以中止其while循环的处理:缓存
1 public class Termination { 2 private int v; 3 public void runTest() throws InterruptedException { 4 Thread workerThread = new Thread( () -> { 5 while(v == 0) { 6 // spin 7 } 8 }); 9 workerThread.start(); 10 v = 1; 11 workerThread.join(); // test might hang up here 12 } 13 public static void main(String[] args) throws InterruptedException { 14 for(int i = 0 ; i < 1000 ; i++) { 15 new Termination().runTest(); 16 } 17 } 18 }
错误是工做线程可能永远不会看到变量v的更新,所以将永远运行。并发
读取过期的值的缘由之一是CPU内核的缓存。现代CPU的每一个内核都有本身的缓存。所以,若是读取和写入线程在不一样的内核上运行,则读取线程将看到缓存的值,而不是写入线程写入的值。 下面显示了超级用户答案给出的Intel Pentium 4 CPU内部的内核和缓存:工具
Intel Pentium 4 CPU的每一个核心都有本身的1级和2级缓存。全部内核共享一个大的3级缓存。这些缓存的缘由是性能。下列数字显示了访问内存所需的时间,摘自《计算机体系结构,一种定量方法》,JL Hennessy,DA Patterson,第5版,第72页:性能
读取和写入普通字段不会使高速缓存无效,所以,若是不一样内核上的两个线程读取和写入同一变量,则它们将看到陈旧的值。让咱们看看是否能够重现此错误。学习
若是你运行了上面的示例,则颇有可能该测试没法挂断。该测试只须要不多的CPU周期,所以两个线程一般都在同一内核上运行,而且当两个线程在同一内核上运行时,它们将读取和写入同一缓存。幸运的是,OpenJDK提供了jcstress工具,能够帮助进行这种类型的测试。jcstress使用多种技巧,以便测试的线程在不一样的内核上运行。这里,上面的示例被重写为jcstress测试:测试
1 @JCStressTest(Mode.Termination) 2 @Outcome(id = "TERMINATED", expect = Expect.ACCEPTABLE, desc = "Gracefully finished.") 3 @Outcome(id = "STALE", expect = Expect.ACCEPTABLE_INTERESTING, desc = "Test hung up.") 4 @State 5 public class APISample_03_Termination { 6 int v; 7 @Actor 8 public void actor1() { 9 while (v == 0) { 10 // spin 11 } 12 } 13 @Signal 14 public void signal() { 15 v = 1; 16 } 17 }
此测试来自jcstress示例。经过使用注解@JCStressTest
对该类进行注解,咱们告诉jcstress此类是jcstress测试。jcstress在单独的线程中运行以@Actor
和@Signal
注释的方法。jcstress首先启动actor线程,而后运行信号线程。若是测试在合理的时间内退出,则jcstress记录"TERMINATED"结果;不然,结果为"STALE."google
jcstress使用不一样的JVM参数屡次运行测试用例。这是在个人开发机器(使用测试模式压力的Intel i5 4核CPU)上进行此测试的结果。
对于JVM参数-XX:-TieredCompilation,在全部状况下90%都挂起线程,可是对于JVM flags -XX:TieredStopAtLevel=1 and -Xint,该线程在全部运行中终止。
在确认咱们的示例确实包含一个错误以后,咱们如何解决它?
Java有专门的指令,可确保线程始终看到最新的写入值。易失性字段修饰符就是这样的一条指令。读取易失性字段时,能够确保线程看到最后写入的值。该保证不只适用于字段的值,并且适用于在写入volatile
变量以前由写入线程写入的全部值。从以上示例中,将字段修饰符volatile添加到字段v中,能够确保while循环始终终止,即便在使用jcstress的测试中运行也是如此。
1 public class Termination { 2 volatile int v; 3 // methods omitted 4 }
volatile
字段修饰符不是给出此类可见性保证的惟一指令。例如,包java.util.concurrent中的synced语句和类提供相同的保证。Brian Goetz等人撰写的《Java Concurrency in Practice》一书很好地了解了避免可见性错误的技术。
在了解了可见性错误发生的缘由以及如何重现和避免它们以后,让咱们看一下如何查找它们。
Java语言规范第17章。线程和锁正式定义了Java指令的可见性保证。该规范定义了所谓的“先发生”关系来定义可见性保证:
“两个动做能够经过在发生以前的关系进行排序。若是一个动做在另外一个发生以前,则第一个对第二个可见而且在第二个以前进行排序。”
读取和写入易失性字段会建立这样的事前关联:
“在每次对该字段进行后续读取以前,都会对易失字段(第8.3.1.4节)进行写操做。”
使用此规范,咱们能够检查程序是否包含可见性错误,在规范中称为“数据争用”。
“当程序包含两个冲突访问(第17.4.1节)时,它们之间没有按事前发生的关系排序,则该程序被称为包含数据竞争。对同一变量的两次访问(读或写)被称为:若是至少有一个访问是写操做,则冲突。”
在咱们的示例中,咱们看到对共享变量v的读取和写入之间没有“先发生后”关系,所以该示例包含根据规范的数据竞争。
固然,这种推理能够自动化。如下两个工具使用此规则自动检测可见性错误: