对象粒度收集的Java不变式检测系统
技术领域
本发明属于程序错误检测技术领域,具体涉及针对Java程序并发错误检测的系统。
背景技术
随着多核技术的发展,Java多线程程序广泛应用在各个领域。多线程技术为应用程序带来了许多优势:首先,多线程可以提高程序的运行效率。不同于单线程程序,需要再上一个任务完成之后才能进行下一个新的任务,多线程程序只需要进行同步就可以同时并发执行多个任务,从而提高运行效率;其次,使用多线程编程,使得用户界面可以在进行其他工作的同时监听用户事件,这样就增强了程序的实时响应性,且提升了用户体验。
但是,多线程技术也给程序员带来了一些新的问题。一方面,由于编写复杂度的大大提升,程序员对多线程程序的开发和调试变得困难,较之传统的单线程程序更容易出现程序错误。另一方面,多线程程序具有运行时的不确定性,导致并发错误的出现带有随机性,这进一步加大了多线程程序调试和修复的难度。目前针对如何高效检测和定位多线程程序中并发错误的问题,虽然已有大量的研究工作,但仍没有十分完善的解决方案。
为了统一地检测程序中多种类型的并发错误,一系列基于不变式的检测工具被提出。这类工具首先制定相应的不变式规则来代表可能产生并发错误的程序行为,然后通过一定数量的训练运行提取满足不变式的所有正确程序行为,最后以收集到的信息指导检测运行时的并发错误检查。
现有的不变式检测工具因只记录和分析单个变量在线程间的交叉存取关系,难以识别程序中变量的相关性,所以无法检测由多变量导致的程序并发错误。相关研究表明,在所有已被确认的非死锁的程序并发错误中,34%的错误是由多变量的读写不一致所导致。这一现象说明多变量并发错误普遍存在于现实应用中,因而如何有效地检测这类错误变得十分重要。考虑到Java程序有更好的封装性,关联变量多声明在同一对象中,所以以对象粒度对程序进行不变式检测能够解决Java程序中的多变量并发错误。
发明内容
本发明的目的在于提供一种能够减少程序中交叉存取次数,降低运行开销,有效地进行多变量并发错误检测的Java不变式检测系统。
多变量并发错误是并发错误中最难检测的一类,由于分析算法难以识别程序中变量的相关性,所以无法在检测过程中将多个相关的变量同时进行考虑。现有的不变式检测工具主要针对于C/C++程序设计,目前还没有高效检测这类程序错误的手段。考虑到Java程序中良好的封装特性,相关变量一般声明在同一对象中,这为多变量并发错误的检测提供了条件。
本发明提供的Java不变式检测工具结构,采用一种以对象为粒度的不变式检测方法,用以检测Java程序中的并发错误,包括多变量并发错误。本发明以收集程序中每个对象的读写行为,代替了原有的记录每个字段的读写行为,并使用对象粒度定义不变式规则,在运行时对多线程程序中对象的交叉存取进行提取和检测,因此能自动地检测多变量并发错误。
本发明提供的Java不变式检测系统,其构架见图1所示。本发明首先在正确的训练运行中以一定规则提取和记录所有正确的程序行为,形成不变式集合,然后在检测运行中检查当前发生的程序行为是否符合不变式集合中所记录的情况,并以此检测程序中的错误。发明具体包括预处理模块、不变式训练模块、不变式文件分析模块、不变式检测模块以及错误排序删减模块。根据不变式检测的工作流程,这里对各个模块所负责的工作分别进行介绍。
1.预处理模块,负责检测和过滤程序中不会引起并发错误的对象。这样的对象有两类,分别是局部对象和单赋值对象,它们在程序中只会被一个线程进行写操作,所以不会造成并发错误。通过静态分析找出这些对象,并在字节码中添加标签进行标示,并分别送入不变式训练模块和不变式检测模块;在训练和检测运行阶段,识别这些标签,并忽略对有标签对象的检查和记录,从而降低检测所带来的开销。
2.不变式训练模块,对于经过预处理模块处理的对象,使用多次训练运行,基于对象粒度提取程序中的不变式信息,并将提取的不变式信息集合以文件形式进行保存,每次正确运行产生一个不变式记录文件。
3.文件分析模块,负责把不变式训练模块产生的所有不变式记录文件中的信息按照不变式规则进行归并,最终合成一个文件。
4.不变式检测模块,利用上述文件分析模块合成的文件指导检测运行。当检测运行中的程序行为与记录中的对象不变式信息出现矛盾时,说明可能存在并发错误,需要进一步分析。
5. 错误排序删减模块,负责对不变式检测模块检测到的程序错误做可信性分析。该模块使用启发式算法,为所有检测到的错误计算相应的可信值。删除可信值较低的错误,其他筛选得到的错误经过排序后反馈给程序员。
本发明涉及不变式训练模块中的对象粒度不变式和错误排序删减模块中排序删减算法的设计,前者确保检测工具能够找到尽可能多的并发错误,后者负责提高检测的准确性。
本发明通过记录程序中对象的交叉存取,提取基于对象粒度的不变式信息。首先定义检测时本发明所关注的两种对象操作,称为对象读和对象写。
定义1:程序对某个对象或其中任意域进行读取,都视为对该对象的一次读操作,简称为对象读,记为r。
定义2:程序对某个对象或其中任意域进行赋值,都视为对该对象的一次写操作,简称为对象写,记为w。
基于文献[1]中对单变量存取关系的研究,本发明针对对象读与对象写间的依赖关系设计了三种以对象粒度为基础的不变式类型,它们分别为本地/远程读不变式,跟随读不变式和写集合不变式。下面依次对这三种不变式进行说明:
(1)本地/远程读不变式,如图2所示,本地/远程读不变式用于判断程序中对象读所对应的最近一次对象写,其来源是本地线程还是远程线程。对于一个对象读r,该不变式有三种情况:当该不变式的值为Local 时,表示r依赖的所有对象写必须来自本地线程;当值为Remote时,表示r依赖的所有对象写必须来自其他线程;如果r可以同时依赖于本地和其他线程中的对象写操作时,r不具有相应的本地/远程读不变式信息。该不变式可用于检测写后读并发错误。
在训练运行时,本发明建立一个哈希表来存储和提取对象读r的不变式信息。对象读r的本地/远程读不变式的值以首次运行时r所依赖的对象写线程作为初始值(Local或Remote),并存入哈希表。当r重新执行时,会判断当前运行所依赖的对象写线程情况和哈希表中的记录是否一致。若不一致,则说明r不具有本地/远程读不变式信息,删除r在哈希表中的记录。在程序运行结束时,哈希表中的所有记录作为不变式信息被导出。由于不变式记录了所有只依赖于本地或远程写的对象读,在检测运行时只需比较所有被记录的对象读r当前的写线程依赖关系和记录的信息是否一致。若结果不一致,则说明在执行r时存在潜在的并发错误。
(2)跟随读不变式,用于判断两个连续的对象读是否依赖于同一个对象写操作,如图3所示。该不变式确定两个读操作间是否可以被其他写操作打断,所以能保证两个读操作间的原子性,用于检测读后读并发错误。
本发明为每个对象维护一个计数器,表示该对象在最近一次对象写后发生对象读的次数。该计数器在每次对象读后增加,每次对象写后被清零。在训练运行时,每当对象读发生,就检查该计数器中的值。若值大于零,表示该对象读使用了与上一次读相同的对象写,满足跟随关系;反之,则不满足。每次运行中都满足跟随关系的所有对象读都被记录下来,并作为不变式信息。在检测运行时,被记录下来的读操作如不满足跟随关系,就是一次潜在错误。
(3)写集合不变式,如图4所示。在正确运行时,对象读一般只能依赖程序中一部分的对象写,当依赖其他写操作时可能会造成并发错误。写集合不变式为所有对象读记录下正确运行时所有依赖的对象写,并形成不变式集合,用于指导并发错误的检测。该不变式用于检测写后读并发错误。
在训练运行阶段,本发明为每个对象读收集在正确运行时所有依赖的对象写,形成写集合并作为不变式信息记录下来。经过一定数量的正确训练运行后,为某一对象读收集的写集合能基本覆盖该对象读在正确运行时所能依赖的对象写。在检测阶段中,检查所有对象读,判断其是否依赖于例外的对象写,若有,则说明存在潜在的并发错误。
此外,在检测阶段后,对生成一个潜在错误列表。但是,在提取的不变式信息不完全的情况下,不变式检测会带来较多的假阳性,并且我们又很难确保经过训练提取的信息是否完全。因此,本发明在检测出潜在错误后,使用错误排序删减模块对潜在错误进一步验证,以减少假阳性。下面将对错误排序删减工作进行说明。
排序删减工作的核心是对可信性的计算,根据不变式的发生频率等信息,为所有潜在错误计算可信值,并以此表示它们是真实错误的可能性。以写集合不变式为例,假设对象读R和对象写W是一对潜在错误,影响其可信性的因素有以下三点:(1)R在训练中的发生频率越高,说明对R的训练越充分,对R的程序行为提取越完整,所以以(R,W)是错误的可信性越高。同理,W在训练中的发生频率越高,可信性越高;(2)训练时R所收集到的对象写集合越小,说明R所依赖的对象写越集中,所以使用其他对象写W会导致错误的可信性越高;(3)如果(R,W)这一读写依赖在检测运行时的出现次数很少,则(R,W)为小概率的事件,说明它是错误的可信性较高。根据上述因素,本发明在训练和检测过程中需要记录一些额外信息,分别是:
R、W在训练运行时发生的次数,分别记为TR和TW。
基于R所收集的对象写集合的大小,记为|DSet(R)|。
读写对(R,W)在检测运行时发生的次数,记为DDSet。
训练运行的次数,记为t。
写集合不变式的启发式可信性计算公式如下:
同理,Jacob类似地对本地/远程读不变式和跟随读不变式所检测到的并发错误进行可信性计算。下面给出它们相应的可信性计算公式:
。
其中,TR为训练运行时发生的次数;DLD,DFollower为相应对象读在检测运行时发生的次数;T为训练运行的次数。
本发明的有益效果是:本发明基于不变式检测原理,根据Java程序的封装特性,以对象粒度为检测基础,设计并实现了一种新型的不变式检测框架。本发明采用对象粒度收集并检测Java程序的行为是否正确,一方面减少了程序中交叉存取出现的次数,降低了运行开销;另一方面,由于Java程序有内在关联的变量一般以同一对象的不同字段的形式存在,以此直接分析该对象的交叉存取关系能够反应出关联变量间的交叉存取关系,能有效地进行多变量并发错误的检测。
附图说明
图1所示为对象粒度不变式检测工具架构概览。
图2所示为本地/远程读不变式示意图。
图3所示为跟随读不变式示意图。
图4所示为写集合不变式示意图。
图5所示为多变量并发错误检测案例分析。
具体实施方式
图1为本发明的总体框架,包括预处理模块、不变式训练模块、不变式文件分析模块,不变式检测模块以及错误排序删减模块。根据不变式检测的工作流程,这里对各个模块所负责的工作分别进行介绍。
1.预处理模块,负责检测和过滤程序中不会引起并发错误的对象。这样的对象有两类,分别是局部对象和单赋值对象,它们在程序中只会被一个线程进行写操作,所以不会造成并发错误。通过静态分析找出这些对象,并在字节码中添加标签进行标示。在训练和检测运行阶段,识别这些标签,并忽略对有标签对象的检查和记录,从而降低检测所带来的开销。
2.不变式训练模块,使用多次训练运行,基于对象粒度提取程序中的不变式信息,并将收集的不变式集合以文件形式进行保存,每次正确运行产生一个不变式记录文件。
3.文件分析模块,负责把所有记录文件中的信息按照不变式规则进行归并,最终合成一个文件。
4.不变式检测模块,利用合成文件指导检测运行。当检测运行中的程序行为与记录中的对象不变式信息出现矛盾时,说明可能存在并发错误,需要进一步分析。
5. 错误排序删减模块,负责对检测模块检测到的程序错误做可信性分析。该模块使用启发式算法,为所有检测到的错误计算相应的可信值。删除可信值较低的错误,其他筛选得到的错误经过排序后反馈给程序员。
本发明的核心是对象粒度不变式和排序删减算法的设计,前者确保检测工具能够找到尽可能多的并发错误,后者负责提高检测的准确性。本发明通过记录程序中对象的交叉存取,提取基于对象粒度的不变式信息。首先定义检测时本发明所关注的两种对象操作,称为对象读和对象写。
定义1:程序对某个对象或其中任意域进行读取,都视为对该对象的一次读操作,简称为对象读,记为r。
定义2:程序对某个对象或其中任意域进行赋值,都视为对该对象的一次写操作,简称为对象写,记为w。
基于文献[1]中对单变量存取关系的研究,本发明针对对象读与对象写间的依赖关系设计了三种以对象粒度为基础的不变式类型,它们分别为本地/远程读不变式,跟随读不变式和写集合不变式。下面依次对这三种不变式进行说明:
(1)本地/远程读不变式。如图2所示,本地/远程读不变式用于判断程序中对象读所对应的最近一次对象写,其来源是本地线程还是远程线程。对于一个对象读r,该不变式有三种情况:当该不变式的值为Local 时,表示r依赖的所有对象写必须来自本地线程;当值为Remote时,表示r依赖的所有对象写必须来自其他线程;如果r可以同时依赖于本地和其他线程中的对象写操作时,r不具有相应的本地/远程读不变式信息。该不变式可用于检测写后读并发错误。
在训练运行时,本发明建立一个哈希表来存储和提取对象读r的不变式信息。对象读r的本地/远程读不变式的值以首次运行时r所依赖的对象写线程作为初始值(Local或Remote),并存入哈希表。当r重新执行时,会判断当前运行所依赖的对象写线程情况和哈希表中的记录是否一致。若不一致,则说明r不具有本地/远程读不变式信息,删除r在哈希表中的记录。在程序运行结束时,哈希表中的所有记录作为不变式信息被导出。由于不变式记录了所有只依赖于本地或远程写的对象读,在检测运行时只需比较所有被记录的对象读r当前的写线程依赖关系和记录的信息是否一致。若结果不一致,则说明在执行r时存在潜在的并发错误。
(2)跟随读不变式用于判断两个连续的对象读是否依赖于同一个对象写操作,如图3所示。该不变式确定两个读操作间是否可以被其他写操作打断,所以能保证两个读操作间的原子性,用于检测读后读并发错误。
本发明为每个对象维护一个计数器,表示该对象在最近一次对象写后发生对象读的次数。该计数器在每次对象读后增加,每次对象写后被清零。在训练运行时,每当对象读发生,就检查该计数器中的值。若值大于零,表示该对象读使用了与上一次读相同的对象写,满足跟随关系;反之,则不满足。每次运行中都满足跟随关系的所有对象读都被记录下来,并作为不变式信息。在检测运行时,被记录下来的读操作如不满足跟随关系,就是一次潜在错误。
(3)在正确运行时,对象读一般只能依赖程序中一部分的对象写,当依赖其他写操作时可能会造成并发错误。如图4所示,写集合不变式为所有对象读记录下正确运行时所有依赖的对象写,并形成不变式集合,用于指导并发错误的检测。该不变式用于检测写后读并发错误。
在训练运行阶段,本发明为每个对象读收集在正确运行时所有依赖的对象写,形成写集合并作为不变式信息记录下来。经过一定数量的正确训练运行后,为某一对象读收集的写集合能基本覆盖该对象读在正确运行时所能依赖的对象写。在检测阶段中,检查所有对象读,判断其是否依赖于例外的对象写,若有,则说明存在潜在的并发错误。
此外,在检测阶段后,对生成一个潜在错误列表。但是,在提取的不变式信息不完全的情况下,不变式检测会带来较多的假阳性,并且我们又很难确保经过训练提取的信息是否完全。因此,本发明在检测出潜在错误后,使用错误排序删减模块对潜在错误进一步验证,以减少假阳性。下面将对错误排序删减工作进行简单说明。
排序删减工作的核心是对可信性的计算,根据不变式的发生频率等信息,为所有潜在错误计算可信值,并以此表示它们是真实错误的可能性。以写集合不变式为例,假设对象读R和对象写W是一对潜在错误,影响其可信性的因素有以下三点:1)R在训练中的发生频率越高,说明对R的训练越充分,对R的程序行为提取越完整,所以以(R,W)是错误的可信性越高。同理,W在训练中的发生频率越高,可信性越高;2)训练时R所收集到的对象写集合越小,说明R所依赖的对象写越集中,所以使用其他对象写W会导致错误的可信性越高;3)如果(R,W)这一读写依赖在检测运行时的出现次数很少,则(R,W)为小概率的事件,说明它是错误的可信性较高。根据上述因素,本发明在训练和检测过程中需要记录一些额外信息,分别是:
R、W在训练运行时发生的次数,分别记为TR和TW。
基于R所收集的对象写集合的大小,记为|DSet(R)|。
读写对(R,W)在检测运行时发生的次数,记为DDSet。
训练运行的次数,记为t。
写集合不变式的启发式可信性计算公式如下:
同理,Jacob类似地对本地/远程读不变式和跟随读不变式所检测到的并发错误进行可信性计算。下面给出它们相应的可信性计算公式:
其中,TR为训练运行时发生的次数;DLD,DFollower为相应对象读在检测运行时发生的次数;T为训练运行的次数。
下面结合现实中的真实案例,说明本发明如何利用对象粒度来检测由多变量引发的并发错误。图5展示了JRuby项目中的一个原子性违反错误。对象entry中的三个域klass、name和method是关联变量,语句1和3实际上需要保证原子性,但在程序中却缺少同步保护。因此,当程序出现图5 (a)中的执行流时,method中的内容和其他两个变量不一致,从而导致并发错误。图5(b)解释了现有检测工具无法检测这个并发错误的原因。由于只针对单变量进行检测,它们所能识别到的交叉存储只有W1→R3,所以无法检测出R2和R3之间是否存在原子性要求。相应地,图5(c)说明本发明通过检测对象粒度,认为本例中四个读写操作是关联操作,从而得到更多有用的交叉存取信息。通过对跟随读不变式的提取,本发明能检测到R3→R2的依赖关系,提取出两个对象读R2和R3使用同一个对象写的不变式信息。因此,本发明能检测出这一多变量并发错误。
参考文件
[1] Y. Shi, S. Park, Z. Yin, et al. DefUse: Definition-Use Invariants for Detecting Concurrency and Sequential Bugs [C]. SPLASH’10, 160-174.