原文地址:
https://www.sciencedirect.com/science/article/abs/pii/S0164121215000849?via%3Dihubhttps://www.sciencedirect.com/science/article/abs/pii/S0164121215000849?via%3Dihub
Gustavo Pinto、Wesley Torres、Benito Fernandes、Fernando Castor、Roberto S.M.Barros
{ghlp,wst,jbfan,castor,roberto}@cin.ufpe.br
伯南布哥联邦大学信息学中心。Jornalita Anibal Fernandes,S/N,Recife PE50.740-560,巴西。
学术界和工业界都坚信,多核技术将从根本上改变软件的构建方式。
然而,对并发编程构造的当前使用状态知之甚少。在这项工作中,我们提出了一个经验
旨在研究2227个真实世界、稳定和成熟的Java项目并发编程结构的使用SourceForge。我们已经研究了这些应用程序的最新版本中并发技术的使用,以及如何用法随着时间的推移而演变。我们研究的主要结果是:(一)超过75%的最新版本的项目显式创建线程或使用某种并发控制机制;(二) 超过一半的项目至少有47个每100KLoC同步方法和3个Runnable接口实现,这意味着编程构造经常被使用,但它们也被大量使用;(三) java.util.concurrent的采用图书馆只是中等规模的(大约23%的并发项目使用它);(四) 高效且线程安全的数据结构,诸如ConcurrentHashMap之类的方法尚未被广泛使用,尽管它们具有许多优点。
关键词:
Java、并发、软件进化、OSS
多核系统提供潜在的廉价、可扩展、高性能计算并且在能源消耗上有显著减少。为了实现这一潜力,有必要利用包括多个处理元素的集合的新的异构体系结构。利用多核的技术,应用程序必须并发,构这成了挑战,因为众所周知,并发编程的困难(Sutter,2005)。许多编程语言提供并发编程的构造。这些解决方案在抽象,error-proneness和性能方面有很大区别。当涉及到并发编程结构时Java编程语言尤其丰富。例如,它包括监控的概念,一个支持要互斥和同步的低级的机制,以及高层图书馆(Lea,2005),java.util.concurrent。同时,也称为j.u.c,在版本1.5语言中引入的。
在学术界和工业,有一种强烈的信念,多核技术将从根本上改变构建软件的方式。然而,据我们所知,就开发人员使用的结构来说,关于并发软件发展实践的当前状态是缺乏可靠的信息的。在这项工作中,我们的目标是部分填补这个空白。
具体来说,我们提供了一个实证研究,旨在在Java应用程序中建立并发编程构造的实际使用的当前状态。我们分析了2227个稳定和成熟的Java项目包括超过来自SourceForge(LoC-没有空行和注释)的6亿行代码,其中一个是最流行的开放源代码存储库。我们的分析包括了这些应用程序几个版本和基于50多个自动收集的源代码指标。我们也研究这些指标之间的相关性,试图找到并发编程结构的使用趋势。我们选择了Java,因为它是一个广泛使用的面向对象的编程语言。此外,正如我们之前所说,它包括对有低级和高级机制的多线程的支持。此外,它是在SourceForge数量最高的项目的语言。
如何编写并行程序的证据可以提高开发人员对于可用机制的意识。它还可以显示如何让广为接受的一些机制投入实践。此外,它可以告诉设计新的机制的研究人员关于开发人员可能更愿意使用的种类结构。工具厂商也可以通过支持使用鲜为人知的和更有效的机制的开发人员受益,例如,通过实现重构(Dig,Marrero,Ernst,2009, Ishizaki,Daijavad,Nakatani,2011 and Schaifer,Sridharan,Dolby, Tip, 2011a)。此外,本研究结果发现学生进入教师的并发编程的重要性更令人信服,这不仅是对软件开发的未来,而且还对当下。
基于获得的数据,我们建议回答的研究问题(RQ)。
RQ1:Java应用程序使用并发编程结构吗?
我们发现超过75%的最新版木的检查项目包括某种形式的并发编程,如,至少有一个同步关键字的出现。在媒介项目中(20001 - 100000 LoC),对于大型项目,这个百分比的增长超过90%,达到100%(超过100000 LoC)。此外,平均数字(每100000 LoC)的同步方法,类扩展线和类实现运行,分别66.75,13,13.85。这些结果表明,项目经常使用并发编程构造并且数量相当密集¹ 。另一方面也许相反,尽管多核机器普遍,这些年来并发项目的总百分比未见显著改变。
¹ 在整个论文中,我们经常使用“频繁”和“密集”两个术语。我们使用第一个来指代使用给定构造。我们使用术语“经常”作为“经常”的同义词。我们使用术语“密集”来指单个项目中的给定构造。例如,同步方法使用频繁且密集,因为的项目使用这种构造,并且大多数项目多次使用这种构造。
RQ2:开发人员有转移到基于library 的并发性吗?
我们的数据表明,只有23.21%的分析并发采用java.util.concurrent并发库里的项目类。另一方面,这个库在采用上有增长。然而,这种增长似乎并不是一般有关Java的传统的并发编程结构的使用减少,有一些例外。此外,最近在积极开发更多的项目,例如,自2009年以来至少有一个版本采用java.util.concurrent并发库发布。因此,使用library的成熟的项目有活跃的百分比,高于23.21%。
RQ3:开发人员如何从并发线程中保护共享变量?
大多数的项目使用同步块和方法。挥发性修饰符,显式锁(包括读写锁等变化)和原子变量是不太常见的,尽管它们中的一些似乎越来越受欢迎。我们还注意到同步块使用的一个趋势的增长。
RQ4:开发人员仍然使用java.lang 线程类来创建和管理线程?
我们发现实现Runnable接口是定义新线程的最常见的方法。此外,相当数量的项目采用执行人管理线程执行(11.14%的并发项目)。可以观察到的项目采用执行人展览疲软趋势来减少显式扩展的线程类的数量。
RQ5:开发人员使用线程安全的数据结构?
我们发现开发人员仍然使用Hashtable和 HashMap,尽管前者是线程安全的但效率低下,后者不是线程安全的。尽管如此,在许多项目中,有使用ConcurrentHashMap代替其他关联数据结构的趋势。
RQ6:开发者使用状态同步多长时间呢?
大量的并发项目包括notify 的调用方法,notifyAll或wait方法等。同时,我们注意到一个小数量的项目已经消除了这些方法的许多用法,采用CountDownLatch类,java.util.concurrent并发库的一部分。这个数字对统计分析来说是不足够大的。
然而,它表明有着简单语义的机制像CountDownLatch有潜力,在某些情况下,替代低级,更传统化。
RQ7:开发人员试图捕获可能会导致突然的线程失败的异常?
我们的数据表明,只有不到3%的并发项目执行线程。没有获取异常的处理界面,这意味着,在97%的并发项目中,源于一个编程错误的异常可能导致线程静静地死去,潜在的影响与它们进行交互线程的行为。此外,分析这些实现,我们发现即使他们实现一个处理程序时,开发人员常常不知道如何处理未捕获的异常的线程。这提供了一些迹象,表明新的异常处理机制是明确解决并发应用程序需要的。
提供一个基本的直觉,开发者相信是真实的并发编程结构的用法,我们还进行了一项调查,包含了160多名软件开发人员。这些开发人员都是对源代码进行分析的项目的提交者。这个调查中受访者提出各种问题,比如“你认为哪个是最经常使用并发/并行编程构造Java语言的?”。在本文,我们通过分析Java源代码对比调查结果与数据。
这项工作做出了以下贡献:
它是在Java语言中,并发编程结构的用法上第一个大规模研究,包括如何使用这些构造的分析以已经在随着时间演变。
它提供了大量的数据与当前实践状态的真正的并行项目,这些项目随着时间演变。
它展示了提交者做出的调查结果的一些项目分析。本调查概述了开发人员关于使用并发编程结构的感知。
剩下的文章组织如下:第二节介绍一些在Java方面的并发编程的背景知识。第三节描述了我们的调查设置和一些初步结果。接下来,在第四节中,我们描述了我们用来下载并提取分析数据的基础设施。在第五节就研究问题来说,我们提出了研究结果。然后我们在第六节里提出了线程对工作的有效性,在第七节提出了一些影响。第8节致力于相关工作。最后,在9节,我们提出我们的结论和讨论未来的发展方向。
之前我们的研究中,我们提供了一个并发编程的简短的背景。关于并发编程概念的详细介绍可用其他地方(Tanenbaum,2008)。
一般来说,进程和线程是并发编程的主要的抽象。一个过程是一个容器,持续运行一个程序需要的所有信息,例如,进程的内存位置可以读取和写入数据。一个线程,另一方面,可以被看作是一个轻量级的过程。有不同的实现,即使线程线程和进程不同于彼此,多个线程可以存在在同一个进程和共享自己的数据,在不同的进程不共享资源。此外,线程可以共享源代码信息。这个特性是把双刃剑,因为它有著名并发bug 的成本例如竞争条件。
然而,使用线程的一个主要原因是,因为线程没有相关资源,他们比进程更容易和更快。例如 ,创建一个线程比创建一个进程可以快100倍(Tanenbaum,2008)。
在一个处理器,多线程通常发生在时分多路复用。换句话说,处理器在不同线程之间切换。这个上下文切换通常发生快速和线程同时运行的最终用户感知。在多处理器或在多核系统中,或任务的线程会同时运行,每个处理器或核心运行一个特定的线程。同时运行的线程数量是有限的可用的处理器数量。
在过去的十年,并发编程一直是一个令人兴奋的研究领域。尽管没有共识出并发性的一个单个模型,随着各种竞争模型的发展,许多进步一直在出现(Burckhardt, Baldassin, Leijen, 2010和 Yi, Sadowski, Flanagan, 2011)。除此之外,无论并发性的模型,许多研究人员(Dig, Marrero, Ernst, 2009,Goetz, Peierls, Bloch,Bowbeer,Holmes, Lea, 2006, Ishizaki, Daijavad, Nakatani,2011和 Okur, Dig,2012)认为,高并发库可以提高软件质量。
java.util.concurrent并发库旨在简化并发应用程序在Java语言的发展。使用这个框架,甚至是一个缺乏经验的程序员也可以编写并发应用程序工作。java.util.concurrent并发库提供了一些特性来简化并发编程的任务。此外,library是优化性能。下面我们讨论一些最知名的结构。我们假设读者熟悉Java编程语言和并发编程的基本概念,如锁,相互排斥和同步。java.util.concurrent并发库包含一些构造,如信号量和交换机,我们不讨论本文,因为他们很少使用。例如,我们发现信号量类从未用于分析项目。
锁: 锁接口的实现,如 ReentrantLock,比可以使用同步的方法和代码块执行支持更灵活的锁。他们促进更多的通用结构,可能有不同的取决于线程访问数据的属性,并可能支持多个相关条件(一个接口定义与锁相关的条件变量)对象。锁是一种工具,用于控制由多个线程访问共享资源。一般来说,一个锁提供了独家访问共享资源:一次只有一个线程可以获得锁,每个访问共享资源首先要求锁被收购。然而,一些锁允许并发访问共享资源,如 ReadWriteLock 的读锁。锁的实现在使用同步方法和通过支持阻塞的块尝试获取锁(tryLock())上提供额外的功能块并尝试获取被打断的锁。
原子数据类型: 这些数据类型被一个支持无锁的小工具箱的类提供,在单变量上线程安全编程。从本质上讲,这类java . util .concurrent原子包扩展了波动值的概念,字段,和数组元素,提供一个原子有条件的使用compareAndSet()方法更新操作。如果其当前值等于方法的第一个参数,该方法自动设置一个变量,成功返回true值。在这个包中包含的类方法无条件地获取和设置值,与递增和递减变量的值。该包中的类的例子AtomicBoolean,AtomicInteger AtomicIntegerArray。
并发集合: 一组集合决定在多线程环境中使用。这组包括ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteArraySet和ArrayBlockingQueue。在这个包中并行前缀用于某些类是一个速记,指示一些区别类似同步的类,它使用一个锁的整个集合。例如类Hashtable和 collections.synchronizedmap(...)是同步的,但是ConcurrentHashMap“并发”。并发集合是线程安全的,但并不是由一个单一的锁治理。特别是ConcurrentHashMap,安全地允许任意数量的并发读取以及可调并发写道。
同步:java.util.concurrent同时提供了一些类,可以取代 wait()和notify()方法。CountDownLatch是一个同步的援助,允许一个或多个线程等待直到其他线程正在执行的一组操作都已完成。CountDownLatch等待多个线程结束之前让他们继续下去。CyclicBarrier是另一个同步的援助。它允许一组线程等待以对方达到一个共同的障碍点。
执行人: 执行人,被sub-interfaces Executor 接口及其实现体现出来,支持多个方法来管理线程执行。他们提供了一个异步任务执行框架。ExecutorService管理队列和任务的调度,并允许控制关闭。ExecutorService接口及其实现提供方法来异步地执行一个表示为可调用的任何函数,可运行的result-bearing模拟。ScheduledExecutorService子接口添加支持延迟和执行周期性任务。未来的一个函数,返回结果将会允许决定是否执行完成,并提供取消执行的手段。它的实现提供了可调用,灵活的线程池。执行类提供了工厂方法最常见的类型和配置,以及一些实用的方法来使用它们 ²。
我们进行了一项与程序员有关的调查,为了收集开发人员感知的信息,用Java的并发编程结构的使用。使用这些信息我们可以检查这些开发人员的直觉是否反映真实系统的源代码。调查问卷设计由Kitchenham和 Pfleeger(2008)建议,后阶段规定的作者:规划、创建调查问卷,定义目标受众,评估,进行调查,并分析结果。首先,我们定义的主题问题。主题:受访者和并发编程的经验,他们是多么熟悉,最后,我们问直接质疑使用并发编程技术的状态。调查问卷有9个问题而且由于多项选择,结构被限制,李克特量表(反应了从О到10的规模,其中0表示没有知识和10意味着超级专家),也是自由的。它包括一个问题(#9)在那儿受访者可以使用自由文本回答。
在问卷中定义所有的问题后,我们得到的是澄清和描述一些问题的解释的迭代反馈。这种反馈是从一群专家,也从一个试点调查中的分析和讨论获得的。结合问卷调查的指令,我们包含一些简单的例子,是为了澄清我们的意图。表1介绍了调查问卷的问题。问题的完整列表以及所有调查的反应在网站是可用的 ³。
² 在本文中,我们经常使用“执行者构造”一词。我们使用它来引用与Executor框架相关的类,例如作为执行人、ExecutorService、ScheduledExecutorServices、Executors等。
³ http://www.cin.ufpe.br/˜groundhog
我们的目标人群包括执行至少一个承诺一个分析了工作的开源软件的程序员。这项工作分析是很重要的项目,在 SourceForge 上,它使用Subversion作为默认版本控制系统。尽管如此,Subversion不一定跟踪提交作者的电子邮件地址。例如,可以使用一个匿名提交作者id或一个假名。事实上,后者是更常用的电子邮件地址。SourceForge的另一个问题是,以前的存储库对SourceForge来说是外部的,这使得它很难跟踪他们大量的项目。然后,为了收集这些程序员的电子邮件地址,我们调查了搬到Github的项目,因为它使它更容易找到提交者的电子邮件地址。我们发现搬到Github 的72个项目。
在这些项目中,我们发现2353个独特的电子邮件地址,但是只有1953人有效。调查这些程序员时,当发送273个邮件被未知领域通知的服务器拒绝而且和另一个18自动回
复消息。在一段20天的时间里,我们获得164个响应,9.75%的反应率。这反应率几乎是高于在调查发现在软件工程领域(Kitchenham Pfleeger,2008)反应率的两倍。表2综合调查数据。
我们可以看到在上面的表中,26%的受访者有超过12年的软件开发经验,平均而言,受访者认为自己是适度并发编程经验(价值6范围从0到10),0表示没有知识,和10意味着一个专家)。在他们的经验中,前五名最常用的并发编程构造是相同的第一个版本的Java语言。此外,平均而言,他们认为一半的开源项目使用至少有一个基本的并发构造和30%的项目采用java.uti.concurrent并发库。此外,53%的受访者表示,他们已经使用并发编程技术来提高性能和/或应用程序的可伸缩性。匿名受访者之一具体描述写正确的并发程序和实现了性能改进有多难:
并发性在很多层面上是很艰难的——实际上并行代码,避免潜在的死锁等等。如果不是所有的开发人员在项目上是守纪律的,也容易在实践上打滑,例如正确使用管理锁等等和创建脆弱的并发代码。Java构造在细节上有所帮助,但是主要的负担仍然落在程序员彻底理解并发性及其影响上。也有许多缺陷的语言(如在所有环境中长时间不能保证一直为原子)java.util并发实用程序可以帮助,但只有当程序员理解问题并知道方法和工具使用时,才可以避免它。新的JLS版本已经平息了一些问题(如:如果我没记错的话,现在你可以指望所有语句在一个构造函数返回之前完成,之前不是这样的);但是语言的特性仍然给开发人员一个很大的负担以知道所有的陷阱或开发全力防守。
在本文的其余部分,我们将讨论第1节中提到的基于七个研究问题的调查的主要发现。
本节介绍我们研究的配置:我们的基本假设,我们的矿业基础设施和我们采用的度量套件。我们已经建立了一套从SourceForge下载的项目的工具,分析源代码,并且从这些项目中收集度量。它包括履带,一个度量标准收集工具,及一些辅助外壳脚本。我们称之为基础设施。图1描绘我们所用的基础设施。最初,履带式填充项目是来自SourceForge的Java项目,包括其各种版本的库。
我们由HTTP请求手段获得项目,而不直接访问项目的源代码库。我们用这种做法,因为我们只关心分析提供给广大市民的项目发布,稳定版本。资源代码库往往不明确标识版本而且他们采用方法不一致。而另一方面,SourceForge通过HTTP请求使得它比较容易通过以下方式获得发行版本。
当项目已全部下载,所有的压缩文件解压缩到我们的本地存储库。我们目前能够解压zip,rar,tar,gz,tgz,bz2,tbz,tbz2,bzip2以及7Z文件压缩。在此之后,度量收集工具解析源代码,收集指标,并将结果存储在度量标准库。最后,它产生的输入为CSV文件,被R进行统计分析( Ihaka andGentleman,1996)。
爬虫是Crawler4的扩展,一个开源的网络爬虫框架。这个框架是多线程的,用Java编写。我们还实现了其他脚本,以根据日期组织项目版本并检查目标项目是否准备好进行分析,必要时修复其结构。为了收集并发度量,我们使用了javaCompiler5类来解析源代码并构建解析树。遍历树,提取度量并存储在文本文件中。该延长的指标包括计数类控制线Thread类,实现Runnable接口的类,并且一些Java关键字的用途,如同步,易失性,以及类型实例化属于JUC的数库,如AtomicInteger,ConcurrentHashMap,ReentrantLock和很多其他的。表3列出了我们已经测量的元素的出现次数。
4.http://code.google.com/p/crawler4j/
5.http://docs.oracle.com/javase/6/docs/api/javax/tools/JavaCompiler.html
我们的分析完全专注于成熟稳定的项目,然后确定项目开发。此外,在2004之后没有至少一个释放的项目是不考虑的,因为java.util.concurrent被释放作为于2004年十二月发布的JDK的一部分,而且为了避免琐碎的系统,我们只检查有着至少1000
控制线的项目。我们已经分析同时考虑他们的最新版本及其沿时间演变的项目。在后一种情况下,我们已经研究了多个版本的项目。为了更好地了解他们的发展,我们也计算在一些考虑到最近和以前的系统的版本的指标值的差异。然后,我们计算的Pearson相关(Pearson,1936年),在这些差异之间。这帮助我们确定,例如,若干项目呈现一个倾向,从直接继承Thread类来使用执行切换到管理线程执行。
本文中给出的所有结果被归一化,以避免造成非常大的绝对值的扭曲和使它们更直接比较。例如,在2011.08.22计算结果是在 Dr.Java项目的版本中,度量实现 Runnable释放,我们分好几次实现 Runnable,这是6,通过代码行的数目,这是112703,导致0.000053238。这个结果之后是100000倍而且最后的结果是5.3238。所有收集的指标以这种方式正常化,我们在本文的其余部分使用这些标准值。关于整个文章的绝对值的引用被明确提出。两者的绝对和为所有度量的归一化值以提供本文的配套网站。最后,根据调查结果,我们提出了一些假设,代表关于开发者的期望,尽管国家使用了一些并发编程技术。我们假设有以下几种:
图1。在(a)中,爬虫使用Sourceforge中的Java项目填充基础设施存储库。在(b)中,shell脚本提取所有压缩的文件到我们的本地存储库中。在(c)中,度量收集工具解析源代码,收集度量,并将结果存储在度量中存储库。在(d)中,度量收集工具生成输入CSV文件,供R进行统计分析。