在“ 修复 Java 内存模型,第 1 部分”中,我回顾了同步的基本规则:只要读取可能由其他线程写入的变量,或者写入随后由其他线程读取的变量,就必须进行同步。很容易“忘记”这个规则,特别是在读取时 —— 但是这么做可以造成很多有关程序线程安全的风险。这种 bug 通常是在维护类时引入的:这个类原来是正确同步的,但是维护人员并没有完全理解线程安全需求。
幸运的是,FindBugs 拥有大量的检测器,它们可以帮助识别错误同步的类。 Inconsistent Synchronization 检测器很可能是 FindBugs 所使用的最复杂的检测器;它必须分析整个程序,而不仅仅是单个方法,使用数据流分析来确定什么时候加锁,并使用直观推断来推出一个类想要提供线程安全保证。基本上,对于每个域,它都会查看该域的访问模式,并且如果大多数访问都是同步实现的,那么没有同步的访问将被标记为可能的错误。类似地,如果一个属性的设置函数是同步的,而获取函数不是,那么 Inconsistent Synchronization 检测器将生成一条警告。
除了 inconsistent synchronization 之外,FindBugs 还包含其他很多用于检测常见线程错误的检测器,如在加锁两次的情况下等待监视器(这虽然不一定是 bug,但是可能导致死锁),使用双检测加锁模式,不正确地初始化非易失性的域,对线程调用 run() 而不是启动线程,从构造函数中调用 Thread.start() ,或者没有将 wait() 包装到循环中就调用它。
变化,或不变化
在“ 变还是不变?”(和其他文章中),我赞扬了不可变的优点,不可变对象不能进入不稳定的状态。它们在本质上就是线程安全的(假设它们的不可变性是通过使用 final 关键字保证的),并且您可以随意共享和缓存对不可变对象的引用,而不必复制或者克隆它们。
Java 语言中包括 final 关键字是为了帮助开发人员创建不可变类,并允许编译器和运行时环境以声明的不可变性为基础进行优化。然而,虽然域可以是 final,但是数组元素不可以。通过正确地使用 final 和 private 域,可以使对象成为不可变的,但是如果对象的状态包括数组,那么防止对这些内部数组的引用逃避该类的方法是很重要的。清单 4 展示的类尝试成为不可变的,但是不是,因为在调用 getStates() 之后,调用者可以修改状态数组。(相关的可能 bug 是在可变类可能返回可变数组的引用时,并且在调用者使用这个数组时,它的内容可能已经更改了。)虽然通常将其看作一种“恶意代码”脆弱性(并且很多开发人员并不关心“恶意代码”,因为他们的系统并不加载“不受信任”的类),但是这种习惯仍然可能导致各种与恶意代码无关的问题。返回一个不可修改的 List 或者在返回之前克隆该数组可能更好。FindBugs 可以检测类似 getStates() 中的错误(如清单 4 所示)—— 虽然它不必知道 States 类是假定为不可变的,但是知道这个设置函数返回了可变私有数组的句柄,并且相应地做了标记。
清单 4. 错误地返回可变数组的引用
public class States { private final String[] states = { "AL", "AR", "AZ", ... }; public boolean isState(String stateCandidate) { ... } public String[] getStates() { return states; } }
回页首
bug 都很重要
FindBugs 确实是一种不寻常的工具,它几乎可以在任何时间找出实际的 bug。您可能认为它搜索的一些变量自赋值之类的 bug 模式,它们太微不足道了,以至于不必麻烦地查找,但是您错了 —— FindBugs 的每个检测器都已经在测试、产品、专业的开发代码中发现了 bug。您的代码中是否潜藏着未知的 bug?下载一个 FindBugs,并尝试对您的代码使用它。结果可能会启发(和干扰)您。
参考资料
您可以参阅本文在 developerWorks 全球站点上的 英文原文.下载一个 FindBugs工具,并对您的代码尝试该工具。
其他代码检测工具包括 Checkstyle和 TeamStudio。
请访问 Developer Bookstore,获取技术书籍的完整列表,其中包括数百本 与 Java 相关的书籍。
在 developerWorksJava 技术专区 可以找到数百篇有关 Java 编程每个方面的文章。
文章来源于领测软件测试网 https://www.ltesting.net/