flint: fixing linearizability violations more parallel, and so the demand for efficient yet...

18
Flint: Fixing Linearizability Violations Peng Liu Purdue University [email protected] Omer Tripp IBM Research, NY [email protected] Xiangyu Zhang Purdue University [email protected] Abstract Writing concurrent software while achieving both cor- rectness and efficiency is a grand challenge. To facilitate this task, concurrent data structures have been introduced into the standard library of popular languages like Java and C#. Unfortunately, while the operations exposed by concurrent data structures are atomic (or linearizable), compositions of these operations are not necessarily atomic. Recent studies have found many erroneous implementations of composed concurrent operations. We address the problem of fixing nonlinearizable com- posed operations such that they behave atomically. We in- troduce Flint, an automated fixing algorithm for composed Map operations. Flint accepts as input a composed operation suffering from atomicity violations. Its output, if fixing suc- ceeds, is a composed operation that behaves equivalently to the original operation in sequential runs and is guaranteed to be atomic. To our knowledge, Flint is the first general algo- rithm for fixing incorrect concurrent compositions. We have evaluated Flint on 48 incorrect compositions from 27 popular applications, including Tomcat and My- Faces. The results are highly encouraging: Flint is able to correct 96% of the methods, and the fixed version is often the same as the fix by an expert programmer and as efficient as the original code. 1. Introduction Writing concurrent programs is a challenging task. The main challenge is to enable a high level of concurrency (i.e., thread interleavings) while at the same time ensuring that interleaved execution does not lead to unexpected program behaviors, such as data races or deadlocks. Most develop- ers do not have special expertise in parallel programming. Hardware architectures are, however, becomingly increas- ingly more parallel, and so the demand for efficient yet cor- rect parallel software is on the rise [29]. To address this ten- sion, popular programming languages like Java and C# have incorporated concurrent data types into their libraries. The System.Collections.Concurrent namespace in .NET offers concurrent implementations of abstract data types (ADTs) frequently in use, like Dictionary and Stack. In Java, the java.util.concurrent package pro- vides similar support with concurrent data structures such as ConcurrentHashMap and BlockingQueue. These con- 1 ConcurrentMap put(sender) { 2 ConcurrentMap dests = queues.get(sender); 3 if ( dests == null) { 4 dests=new ConcurrentHashMap(); 5 if (queues.putIfAbsent(sender , dests ) != null ) { 6 dests=queues.get(sender); } 7 return dests ; } Figure 1. Composed operation from Adobe BlazeDS tainer classes, implemented by expert programmers, encap- sulate all the complexities entailed by correct yet efficient synchronization, permitting the developer to work with con- venient interfaces like putIfAbsent in ConcurrentMap or AddOrUpdate in ConcurrentDictionary. These guaran- tee atomic, or linearizable, behavior. That is, the operation is guaranteed to take effect instantaneously at some point between its invocation and return [3, 11]. Composed Concurrent Operations While library support obviates many potential errors and inefficiencies, a funda- mental problem still remains: Custom composition of atomic operations does not necessarily render the composed op- eration atomic. As an illustrative example, we refer to the real-world composed operation in Figure 1. The first step of this operation is to check whether the shared queues Map has a mapping for key sender. If so, then that value is returned. Otherwise, a fresh value is created and mapped under sender assuming another thread hasn’t already cre- ated a mapping for sender. If the mapping succeeds, i.e., the putIfAbsent operation returns null, the returned value dests is the fresh value just mapped by the current thread; If the mapping fails, i.e., the putIfAbsent operation returns a non-null value, the returned value dests, obtained via a second get call, is the one already mapped under sender by another thread. Even though the individual operations comprising put are all atomic, the behavior of put is not atomic. The follow- ing concurrent execution scenario, which assumes that in the initial state there is no mapping for sender under queues, demonstrates this: T1 T2 get(sender) / sender 7null putIfAbsent(sender,dests) / succeeds remove(sender) get(sender) / sender 7null

Upload: others

Post on 30-May-2020

1 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

Flint: Fixing Linearizability ViolationsPeng Liu

Purdue [email protected]

Omer TrippIBM Research, [email protected]

Xiangyu ZhangPurdue University

[email protected]

AbstractWriting concurrent software while achieving both cor-

rectness and efficiency is a grand challenge. To facilitate thistask, concurrent data structures have been introduced intothe standard library of popular languages like Java and C#.Unfortunately, while the operations exposed by concurrentdata structures are atomic (or linearizable), compositions ofthese operations are not necessarily atomic. Recent studieshave found many erroneous implementations of composedconcurrent operations.

We address the problem of fixing nonlinearizable com-posed operations such that they behave atomically. We in-troduce Flint, an automated fixing algorithm for composedMap operations. Flint accepts as input a composed operationsuffering from atomicity violations. Its output, if fixing suc-ceeds, is a composed operation that behaves equivalently tothe original operation in sequential runs and is guaranteed tobe atomic. To our knowledge, Flint is the first general algo-rithm for fixing incorrect concurrent compositions.

We have evaluated Flint on 48 incorrect compositionsfrom 27 popular applications, including Tomcat and My-Faces. The results are highly encouraging: Flint is able tocorrect 96% of the methods, and the fixed version is oftenthe same as the fix by an expert programmer and as efficientas the original code.

1. IntroductionWriting concurrent programs is a challenging task. The

main challenge is to enable a high level of concurrency (i.e.,thread interleavings) while at the same time ensuring thatinterleaved execution does not lead to unexpected programbehaviors, such as data races or deadlocks. Most develop-ers do not have special expertise in parallel programming.Hardware architectures are, however, becomingly increas-ingly more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming languages like Java and C# haveincorporated concurrent data types into their libraries.

The System.Collections.Concurrent namespace in.NET offers concurrent implementations of abstract datatypes (ADTs) frequently in use, like Dictionary andStack. In Java, the java.util.concurrent package pro-vides similar support with concurrent data structures suchas ConcurrentHashMap and BlockingQueue. These con-

1 ConcurrentMap put(sender) {2 ConcurrentMap dests = queues.get(sender);3 if (dests == null) {4 dests=new ConcurrentHashMap();5 if (queues.putIfAbsent(sender , dests) != null ) {6 dests=queues.get(sender); }7 return dests ; }

Figure 1. Composed operation from Adobe BlazeDS

tainer classes, implemented by expert programmers, encap-sulate all the complexities entailed by correct yet efficientsynchronization, permitting the developer to work with con-venient interfaces like putIfAbsent in ConcurrentMap orAddOrUpdate in ConcurrentDictionary. These guaran-tee atomic, or linearizable, behavior. That is, the operationis guaranteed to take effect instantaneously at some pointbetween its invocation and return [3, 11].

Composed Concurrent Operations While library supportobviates many potential errors and inefficiencies, a funda-mental problem still remains: Custom composition of atomicoperations does not necessarily render the composed op-eration atomic. As an illustrative example, we refer to thereal-world composed operation in Figure 1. The first stepof this operation is to check whether the shared queues

Map has a mapping for key sender. If so, then that valueis returned. Otherwise, a fresh value is created and mappedunder sender assuming another thread hasn’t already cre-ated a mapping for sender. If the mapping succeeds, i.e.,the putIfAbsent operation returns null, the returned valuedests is the fresh value just mapped by the current thread;If the mapping fails, i.e., the putIfAbsent operation returnsa non-null value, the returned value dests, obtained via asecond get call, is the one already mapped under senderby another thread.

Even though the individual operations comprising put

are all atomic, the behavior of put is not atomic. The follow-ing concurrent execution scenario, which assumes that in theinitial state there is no mapping for sender under queues,demonstrates this:

T1 T2get(sender) / sender 7→ nullputIfAbsent(sender,dests) / succeeds

remove(sender)

get(sender) / sender 7→ null

Page 2: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

Following a linearization of the put operation and theremove operation wherein put is linearized before remove,the put operation should either return the value alreadymapped to key sender or create a fresh mapping and re-turn the respective value. Following the other linearization,wherein remove is linearized before put, the put opera-tion should create a fresh mapping and return the respectivevalue. In either linearization, put should not return null,though this occurs in the concurrent run above. This showsthat the operation is not linearizable.

Oversight of the lack of composability of concurrent op-erations is a common source of errors. In a recent study,Shacham et al. [27] have found 56 distinct atomicity vi-olations in a large set of real-world concurrent Java pro-grams, including Apache Tomcat, Trinidad and MyFaces.All of these violations, without exception, are due to incor-rect composition of atomic Map operations. Other studies re-port similarly alarming statistics [18, 19].

Scope and Approach We present a general approach forfixing incorrect (i.e., nonlinearizable) compositions. Simi-larly to recent studies [18, 26–28], we instantiate our ap-proach over composed Map operations [26]. Map is the mostpopular Java ADT. Map compositions are prevalent yet of-ten buggy. We stress that the underlying principles and tech-niques are of general applicability and can be extended toother data structures implementing ADTs [9].

Our approach breaks into three main steps:1. Bottom-up specification extraction: The first step is to lift

the concrete concurrent operation to the level of a seman-tic description of its intended behavior. This is done byapplying a data-flow analysis over the semantic effects ofindividual operations assuming sequential execution ofthe composed operation.

2. Top-down implementation: Next, based on the specifi-cation of how the operation should behave, we reimple-ment it via a synthesis algorithm geared toward minimiz-ing the number of individual Map operations. The ratio-nale underlying minimization is to reduce interleavingsbetween Map operations as much as possible. At the ex-treme, reimplementation reduces the composed operationto a single Map operation, which naturally guarantees lin-earizability. A common composition pattern of this kindis “check-then-act” [18]: The composed operation firstruns a test and then conditionally performs an update op-eration (e.g., checking whether the Map contains a certainkey/value pair and only then removing it). Such compo-sitions often lead to buggy behaviors when the check andthe update are not atomic. Minimization is effective inrestoring correctness via the built-in check-then-act op-erations like putIfAbsent.

3. Verification and optimization: For the resulting mini-mized version, we verify whether it is linearizable byapplying an off-the-shelf linearizability checker [26, 28].In certain failure scenarios, correctness is recovered by

introducing a limited measure of speculation. On theother hand, the verified operations undergo correctness-preserving performance optimizations to avoid unneces-sary computations when possible.We have implemented a prototype of our approach,

dubbed Flint. Flint was designed to serve as an IDE refac-toring tool, where responsiveness is key. To meet this re-quirement, Flint leverages a recent result in linearizabilitychecking [26, 28], whereby full verification is replaced bylightweight checking under a criterion (or restriction) knownas data independence. Intuitively, data independence assertsthat the behavior of the composed operation is independentof the actual values of the arguments it operates on, e.g., theoperation decides whether to insert a key/value pair basedonly on membership checking, and not the actual value ofthe key. The linearizability checker verifies data indepen-dence as a precondition, thereby simplifying the ensuingverification process. (See Section 2.5.)

In our practical experience, composed Map operations areoften data independent. Indeed, in an empirical study weconducted over 48 incorrect compositions from 27 popularapplications, we discovered that 38 compositions, which fallinto 12 different buggy patterns (see Figure 14), are eitherdata independent or can become data independent followinga simple transformation. (See Section 3.2.) Flint is also ableto address certain forms of data-dependent composition byinserting abort/retry logic into the transformed code. (SeeSection 5.2.) Overall, Flint is able to fix 96% of the cases,fixing time is negligible (under 100 milliseconds), and theresulting implementation is mostly comparable to the orig-inal buggy code in performance. These suggest that Flintcan serve as an effective refactoring tool within IDEs likeEclipse. We point out limitations of Flint in Section 6.

Contributions This paper makes the following principalcontributions:• General fixing approach: We present a general technique

for fixing atomicity violations (or linearizability viola-tions) in composed concurrent operations, which com-bines bottom-up abstraction of the operation (Section 3)with top-down reimplementation of its behavior (Sec-tion 4).• Effective fixing strategy: We demonstrate, as part of our

fixing algorithm, that reimplementing the buggy oper-ation using a minimal number of atomic operations isan efficient and robust fixing strategy (Section 4.1). Weformalize this strategy as a variant of the well-known set-cover problem, which we solve as a linear-programmingproblem. We describe two enhancements of the corestrategy: (1) the performance optimization and (2) thecorrectness recovery via rollbacks (Section 5).• Experimental evaluation: We have evaluated our ap-

proach on a set of 48 real-world concurrent operationsthat suffer from atomicity violations (Section 7). We re-port on highly encouraging results. We include concrete

Page 3: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

1 compute(k) {2 v = m.get(k);3 if (v != null ) {4 return 2∗v;5 }6 else {7 newV = computeV();8 m.putIfAbsent(k,newV);9 return m.get(k); } }

Figure 2. Nonlinearizable composed operation from AdobeBlazeDS

examples of fixes, which illustrate the general ability byour approach to arrive at a transformed version that isboth efficient and natural. We have also organized thebugs we encountered into patterns, which may benefitfuture research.

2. Technical OverviewTo illustrate and motivate our technical approach, we re-

fer to the compute composed operation in Figure 2, takenfrom the Adobe BlazeDS application. In a sequential set-ting, this method first checks if the shared Map m contains amapping for the argument key k. If so, then the respectiveinteger value is first doubled and then returned. Otherwise,compute attempts to establish a mapping for k, which wouldsucceed in a sequential run. The returned value in this case isthe new value, newV, defined via a call to computeV, whichis obtained by invoking get. For our discussion, we assumethat computeV is pure and returns a non-null result.

As we soon demonstrate (Section 2.1), compute suffersfrom atomicity violations. Flint addresses these violationsby abstracting compute into a semantic description of itsbehavior (Section 2.3) and then concretizing the semanticbehavior into another implementation (Section 2.4) that iseither guaranteed to be linearizable by construction or veri-fied as linearizable using an off-the-shelf checker. Therefore,Flint either returns an alternative implementation of the orig-inal method, such that (i) the new implementation is equiv-alent to the original method in the sequential setting and (ii)the new implementation is linearizable, or it reports a failure.

2.1 Correctness Criterion: LinearizabilityIn the context of linearizability, an operation starts with

invocation and ends with response. An operation may over-lap with another operation, e.g., its response is after the invo-cation of the other operation while its invocation is earlier.Linearizability [3, 11] (or atomicity) is a correctness crite-rion for concurrent objects that, stated intuitively, providesthe illusion that an operation applied to the object takes ef-fect instantaneously at some point between its invocationand its response, known as the linearization point. A con-current object is considered linearizable if any execution ofits operations is equivalent to a “legal” sequential execu-tion that preserves the order between non-overlapping op-erations. Here the sequential execution linearizes the opera-tions so that none of them overlap. Formal definitions appear

in Appendix A. Researchers also refer to linearizability asatomicity in general [27].

We illustrate the meaning of linearizability via our run-ning example. The method compute manipulates a sharedConcurrentHashMap object via its atomic APIs get andputIfAbsent. However, compute is not atomic. As one ex-ample, consider a history where initially there is no mappingfor key k, so the thread t executing compute transitions intothe else branch. Another thread then interleaves an invoca-tion of remove(k) after the call to putIfAbsent (line 8)and before the call to get (line 9). The return value is thennull, which is impossible in any legal sequential executionthat linearizes the invocations of the composed method andthe remove method.

2.2 FixesA first attempt to address this atomicity violation, illus-

trated in Figure 3, is to remove the second get call. This ishowever insufficient, because another thread may insert pair(k, v′), such that v′ 6= newV , into m in between the calls toget and putIfAbsent. Then the newV result of computewould not even be mapped to k under m. The second at-tempt, appearing in Figure 4, which ensures that the value xthat k is actually mapped to is returned, is also wrong, be-cause the sequential specification dictates that 2 · x shouldbe returned if x is already mapped to k (line 4).

Given this observation, we arrive at the fix in Figure 5,which is correct but may cause performance degradation,because the method computeV is called directly, withoutfirst checking the necessity of doing so, i.e., whether k isalready mapped under m. We resolve this issue in the finalfix presented in Figure 6, which is also computed by Flint. Itoptimizes the fix in Figure 5 by first checking if k is alreadyin m. In the following, we give an informal account of howFlint arrives at this fix.

2.3 Abstract BehaviorThe first step is to derive the specification of the semantic

behavior of the operation from its concrete implementation.The semantic behavior of an operation assumes sequentialexecution. It consists of accesses to, and manipulations of,shared data structures at the level of the ADTs they repre-sent. As an ADT, a Map 1 is an associative container mappingkeys to values. As such, its ADT representation is as a (par-tial) function from keys to values. Map operations, like put

and get, manipulate the ADT representation as a function.Flint computes the abstract behavior of composed oper-

ations by (i) translating Map operations into their semanticmeaning (e.g. v=m.get(k) becoming v 7→ m(k)) and then(ii) computing the resulting semantic expressions over localvariables by applying a data-flow analysis, e.g., the analysiscomputes the semantic expression u 7→ 2 · m(k) from u

= 2*v. The full technical details appear in Section 3.

1 In this paper, we focus on composed operations over Maps.

Page 4: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

1 compute(k) {2 v = m.get(k);3 if (v != null ) {4 return 2∗v;5 } else {6 newV = computeV();7 m.putIfAbsent(k,newV);8 return newV; } }

Figure 3. Incorrect fix fol-lowing the pattern in theAnnsor application

1 compute(k) {2 v = m.get(k);3 if (v != null ) {4 return 2∗v;5 } else {6 newV = computeV();7 old = m.putIfAbsent(k,newV);8 return old == null ?9 newV : old; } }

Figure 4. Incorrect fix fol-lowing the pattern in the au-toandroid application

1 compute(k) {2 newV = computeV();3 old = m.putIfAbsent(k,newV);4 if (old != null )5 return 2∗old;6 else return newV; }

Figure 5. Correct yet ineffi-cient fix

1 compute(k) {2 v = m.get(k);3 if (v != null ) {4 return 2∗v;5 } else {6 newV = computeV();7 old = m.putIfAbsent(k,newV);8 if (old != null )9 return 2∗old;

10 else return newV; } }

Figure 6. Correct and effi-cient fix

test: k ∈ myesttjjjj no

))SSSS

return: 2 ·m(k) v = ( )

��m[k 7→ v]

��return: m(k)

Figure 7. Abstract (sequential) flow of the operation in Fig-ure 2

Based on the fixpoint solution of the data-flow analysis,Flint derives a semantic description of the behavior of themethod. For illustrative purposes, we depict the output ofdata-flow analysis over the compute method in graphicalform in Figure 7. As expressed in the graph, the abstractbehavior splits execution into two paths following the k ∈ mmembership test. For the true branch, the graph reflects thatthe returned value is 2 ·m(k). For the false branch, the graphreflects the creation of a fresh value (treating computeV as ablack box denoted as ( )), followed by the insertion of thefresh value into m and the returning of m(k).

2.4 ReimplementationGiven an abstract specification of the (sequential) behav-

ior of the composed operation, the goal of the reimplemen-tation step is to realize the specification as a linearizable im-plementation. Our synthesis algorithm searches for an im-plementation that consists of a minimal number of Map oper-ations. The underlying rationale is that minimizing the num-ber of Map operations reduces the threat of unexpected threadinterleavings. In practice, the minimization often leads to

the implementation that consists of only one Map operation,which is linearizable by definition.

To illustrate this strategy, we refer to the graph in Fig-ure 7. There are two possibilities for implementing the k ∈m test. One is to use containsKey, and the other is via aget call, of which the return value is checked for nullness.Our minimization strategy biases the synthesis algorithm to-ward get, because the call to get achieves both (i) nullnesschecking and (ii) the value mapped to key k, if such a map-ping exists, to implement the “return: m(k)” node. UsingcontainsKey instead would require a separate call to get

(or another API).To achieve minimality, we model the challenge of which

operations to select for the (re)implementation as an op-timization problem, which we solve using an off-the-shelflinear-programming solver. Given the abstract specificationof the composed method, our synthesis algorithm first es-tablishes a set of concrete operations that are candidates toimplement different portions of the abstract description, andthen minimizes the overall number of operations requiredto implement the semantic specification. Minimality createsa bias toward correctness. When only one Map operation isused, linearizability is guaranteed. Interestingly, this occursoften. 79% of the buggy real-world compositions we studiedcan be reduced to a single Map operation. According to ourempirical findings, the composed method typically involvesa check and then an action over the Map state, which canlead to buggy behaviors when the check and the action arenot atomic. (See e.g. Figure 3).

2.5 Linearizability CheckingIf the transformed operation consists of multiple Map op-

erations, then we apply Shacham et al.’s verifier [28]. Whileverifying linearizability is undecidable in general [28],Shacham et al. reduce the verification challenge to lightweightdynamic checking by further requiring data independence.Intuitively, data independence is the assumption that the con-trol flow of the composed operation does not depend on spe-cific input values.

Verifying data independence is undecidable in general [28].Shacham et al. provide sufficient restrictions that can bechecked with ease to establish data independence. Specif-ically, a composed operation is data independent if all ofthe following conditions are satisfied: (1) the operation in-volves a single key in all basic operations; (2) the key andvalue are immutable; (3) the operation only makes use ofput(k,val), get(k), remove(k) and putIfAbsent(k,val);(4) path conditions involving Map operations only check thereturn value for nullness (where membership checking viam.containsKey(k)==false is allowed being equivalentto m.get(k)==null); and (5) the result of the composedoperation can only depend on the return value of a Map op-eration.

Page 5: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

2.6 EnhancementsHaving arrived at a minimal (re)implementation that

is verified by the linearizability checker, Flint performscorrectness-preserving post-processing optimizations to re-cover performance, since minimality may degrade perfor-mance although it creates a bias toward correctness. Fol-lowing the common fast-path idiom, the optimization stageattempts to boost the minimal implementation by fusing intoit the tests that guard against redundant computations. Forinstance, given the minimal reimplementation in Figure 5,the optimized version of it, as illustrated in Figure 6, wouldcondition its execution on a check whether m.get(k) isnull. The optimized code is not minimal, but is more effi-cient than the minimal reimplementation, and is also provento be linearizable (Section 5.1).

In the cases where the revised implementation fails lin-earizability checking, to boost the completeness of Flint, wehave developed a fallback strategy that attempts to imposelinearizability on the reimplemented code via lightweightrollback facilities. These are limited to rolling back localthough not global state manipulated by the execution pre-fix. In our evaluation, we demonstrate that with this fallbackstrategy in place, Flint is able to address 96% of the bugsin our input suite compared to 79% otherwise. Note that therollback strategy complements Flint, but cannot replace it.Concretely, 23 out of the 48 bugs that we examined can befixed by Flint but cannot be fixed by rolling back local state.

2.7 DiscussionTo summarize, Flint first extracts a summary of the in-

tended behavior of the buggy operation, derived from itssequential flow; it then reimplements the operation while re-stricting the number of Map invocations to a minimum, laterapplying correctness-preserving performance optimizations;finally, lightweight linearizability checking is performed,and abort/retry logic is installed if needed and possible. Allof the above steps aim for efficiency to enable Flint as anIDE refactoring tool.

A natural alternative to Flint is to obtain a fix via codemutation. While possible in theory, this approach would re-quire the automated tool to traverse a vast space of possiblemutations, hindering performance and thus also IDE com-patibility. Moreover, synthesis is not guaranteed to yield alinearizable fix, as the examples in Figures 3 and 4 illustrate.Flint, instead of attempting arbitrary mutations and transfor-mations, is guided by the intended behavior of the buggymethod and minimizes interference with other threads by re-stricting the number of Map operations to a minimum. Ac-cording to our experimental results, these are highly effec-tive strategies. (See Section 7.3.)

3. AbstractionThe purpose of abstraction is to specify the behavior (i.e.,

flow and functionality) of the method at the semantic level,so as to guide the reimplementation process. Since concur-

Operation Kill Genu=m(k) u 7→ ? u 7→ m(k)u=v u 7→ ? u 7→ [[v]]m[k 7→ v] ? 7→ m(k) v 7→ m(k), k ∈ mm[k 7→ null] ? 7→ m(k) k /∈ m

Table 2. Kill/gen rules for fixpoint computation of avail-able semantic expressions (? stands for any variable or mapvalue.)

1 compute(k) {2 v=m(k);3 // {[k ∈ m,v 7→ m(k)],[k /∈ m, v 7→ m(k)]}4 if (v!=null) {5 // {[k ∈ m,v 7→ m(k)]}6 return 2∗v; }7 else {8 // {[k /∈ m, v 7→ m(k)]}9 newV = computeV();

10 // {[k /∈ m, v 7→ m(k)]}11 if (m(k) == null) // true12 m[k 7→ newV];13 // {[k ∈ m, newV 7→ m(k)]}14 return m(k); } }

Figure 8. First step: abstrac-tion of Map operations

1 compute(k) {2 if (m(k) != null) {3 return 2 · m(k); }4 else {5 m[k 7→ computeV()];6 return m(k); } }

Figure 9. Second step:rewriting of local expres-sions

rent building blocks are often data structures that implementADTs, a useful form of abstraction is in terms of ADT se-mantics. For the Map data structure, a natural abstraction is a(partial) function from keys to values [32], which is manip-ulated via the following three abstract operations:• k ∈ m indicates whether key k is mapped under Map

m. We also interchangeably use m(k) 6= null, which isequivalent according to the semantics of ConcurrentMap.• u = m(k) denotes access to the value v mapped to key k

under Mapm. That value is stored as u.• m[k 7→ v] sets the respective value of key k under m tov.

3.1 Abstracting OperationsThe first abstraction step is to rewrite Map operations into

the abstract operations above. Table 1 specifies the mainrewriting rules. We rewrite an operation by binding the freevariables in the rule. For example, to rewrite the operationx=y.get(z), we bind the free variables u, m and k in thefirst rule to their respective actual arguments x, y and z. Ifthe return value is not assigned, then it is also dropped fromthe abstraction.

As an illustration, consider the running example in Fig-ure 2. Abstracting the operations in this code yields the rep-resentation in Figure 8 (Ignore the annotations for now).Specifically, the get call is abstracted as the expressionv=m(k), and the putIfAbsent invocation is abstracted asthe conditional m[k 7→ newV] operation, i.e., the fragmentof the abstraction in Table 1 that does not involve the assign-ment of the return value.

Page 6: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

Operation Abstractionu=m.get(k) u = m(k);u=m.put(k,v) u = m(k); m[k 7→ v];u=m.putIfAbsent(k,v) if(m(k) == null ) {u = null; m[k → v]; } else u = m(k);u=m.remove(k) if(m(k)! = null ) {u = m(k); m[k → null]; } else u = null;b=m.replace(k,u,v) if(m(k) == u ) {m[k 7→ v]; b = true; } else b = false;b=m.containsKey(k) b = (m(k) 6= null);

Table 1. Abstraction rules for Map operations

3.2 Abstracting Local ExpressionsThe next step, having rewritten the operations, is to

rewrite local expressions into their semantic meaning. Thisis important for two reasons: (1) Rewriting recovers missedinstances of data independence, a precondition for ensuringlinearizability. As data independence requires the method’sresult to depend on the Map state, we rewrite the local vari-able as the Map state it represents. (2) Some local variablesrepresent the Map state in all sequential settings but not in(all) concurrent settings. The programmer, who is not awareof the difference, may use local variables as an optimizationfor getting rid of a get call, leading to incorrect behaviors inconcurrent settings. We need to recover the original seman-tics of the local variable by replacing it with the Map statethat it is expected to represent in both settings.

An illustration of the result of abstracting local expres-sions is given in Figure 9. As an example, return 2*v be-comes return 2 · m(k) according to this rewriting step,which highlights how to compute the return value (along therelevant branch) in the rewritten method.

To assign semantic expressions to local variables, we per-form intraprocedural data-flow analysis that takes its inspira-tion from the available-expressions analysis [1]. Essentially,our analysis forward propagates, using the classic kill/genframework [14], the “must” information about the availabil-ity of semantic expressions. As a simple example, considerthe following code: x = get(k); y = 2*x;. Due to oper-ation rewriting, we conclude that x carries the value of ex-pressionm(k). Therefore, y maps to the expression 2 ·m(k).

The data-flow equation underlying our kill/gen available-expressions analysis first applies kill and only then gen:

avail(n) ={∅ if pred(n) = ∅⋂

p∈pred(n)(avail(p) \ kill(p)) ∪ gen(p) o/w

The data-flow analysis is applied to the Static Single Assign-ment (SSA) [5] representation. We apply this equation to thenodes of the control-flow graph of the subject method until afixpoint is reached. The invocation of the methods (non-Mapmethods) other than Map APIs are assumed to be pure, i.e.,the only effect of them are the return values. In the presenceof the non-Map methods that are not pure, linearizability isrelated not only to the Map structure but also to the shareddata structures modified by the non-Map methods. As we fo-cus on the linearizability of composed Map operations, weassume the purity of the non-Map methods. The fixpoint pro-

cess commences after we have applied operation rewriting.The resulting data-flow annotations reflect the semantic be-havior of the method assuming sequential execution.

Table 2 lists the kill/gen rules defining the analysis. Aread operation sets the assigned variable to point to the readexpression m(k). Assignment between variables kills themapping previously established for the left-hand variable u,and instead points it to the target of the right-hand variable.Finally, a write operation invalidates all old mappings relatedto the key k and instantiates the new mapping for the key.

As an illustration, consider the real-world code in Fig-ure 8, where the operations are already abstracted. After line2, there are two possibilities, as illustrated by the data flowannotations: Either there exists a mapping for k under m(expressed as: [k ∈ m, v 7→ m(k)]) or not (expressed as:[k /∈ m, v 7→ m(k)]). We meet the two possibilities withthe condition v != null. Only the first possibility (or dataflow) can be propagated through the condition check whilethe second is filtered out. The first data flow following the ifbranch leads to a definition of the returned expression 2*v as2 ·m(k). For the other branch (i.e., the else branch), onlythe second possibility can pass through the guarding condi-tion v == null. Therefore, the condition check at line 11can be safely removed as it is always true. By taking thesefactors into account, we derive the abstract specification inFigure 9.

This example highlights an important feature of our anal-ysis, which is path sensitivity. As shown above, the data-flow facts propagated through conditional statements are metwith the asserted condition. Path sensitivity results in precisetracking of data-flow facts, and as a consequence, simplifi-cation of the abstract representation. An example is the re-moval of the condition check at line 11.

Abstracting local variables is important for recovering theexpected semantics, or the original semantics. For the vari-ables that are expected to represent the Map state, it is pos-sible that they represent the Map state in sequential settingsbut do not represent the Map state in concurrent settings. Anexample is shown in the code (Figure 10), taken from theAnnsor application. The local variable c in the return al-ways represents a Map state in sequential runs. However, inthe concurrent settings, if the execution of getKey is inter-leaved just before line 5 by the insertion of pair (a,v) (forsome value v 6= c) by another thread, the local variable c

does not represent a Map state. The programmers, who bearthe sequential reasoning in mind, use the local variable as the

Page 7: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

1 getKey(a) {2 c=m.get(a);3 if (c==null) {4 c=getClass(a);5 m.putIfAbsent(a, c ); }6 return c; }

Figure 10. The AnnsorgetKey method

1 getKey(a) {2 c=m(a);3 // {[a ∈ m,c 7→ m(a)],[a /∈ m,c 7→ m(a)]}4 if (c==null) {5 // { a /∈ m,c 7→ m(a)}6 c=getClass(a);7 // { a /∈ m }8 m[a 7→ c];9 //{ [a ∈ m,c 7→ m(a)]}

10 }11 // { [a ∈ m,c 7→ m(a)]}12 return m(a); }

Figure 11. The getKey methodin abstract form

replacement of the Map state, leading to incorrect behaviorsin concurrent settings.

Our analysis recovers the original semantics of such localvariables by applying the data flow analysis 2 under thesequential reasoning. As illustrated in Figure 11, there aretwo possibilities after line 2: (1) [a ∈ m,c 7→ m(a)] or (2) [a/∈m,c 7→m(a)]. Only the second data flow can be propagatedthrough the if branch, which changes to [a ∈ m,c 7→ m(a)]at the end of the branch. Combining the data flows followingboth the if branch and else branch at line 11, we know thevariable c is equivalent to the map state m(a) and thereforeabstract it as the map state.

In addition, abstracting the local variable in the return,as demonstrated above, also recovers the data independence,which is a prerequisite for a composed operation to be veri-fied as linearizable. Specifically, data independence requiresthe return depends only on the Map state (Section 2.5), whichmay however be violated. For example, the return variable cin Figure 10 may not represent any Map state in concurrentsettings. In contrast, the return in Figure 11 always dependson the Map state m(a).

4. ReimplementationHaving performed the bottom-up mapping from the orig-

inal code to its semantic specification describing the abstractbehavior, our goal in the synthesis step is top-down projec-tion of the semantic specification into a concrete implemen-tation. As an example, we would like to synthesize the codein Figure 5 or Figure 6 based on the semantic descriptionin Figure 7 (or Figure 9). Synthesis essentially replaces theabstract operations in the specification with concrete opera-tions.

If abstract operation a can be implemented by concreteoperation c (i.e., c produces the expression(s) in a), then wesay c covers a. a may be covered by multiple candidate con-crete operations, collectively denoted as cover(a), besides, cmay cover multiple abstract operations, collectively denotedas covered(c).

2 The data flow analysis is applied to the SSA form, but we demonstratewith the non-SSA form for simplicity.

Given the abstract operations (or more precisely, instanti-ations of the abstract operations) from the semantic specifi-cation, a1, . . . , am, we need to first determine the candidateconcrete operations that cover them, c1, . . . , cn, and then se-lect a minimal set of concrete operations out of the candi-dates to cover all the abstract operations. We defer the expla-nation of how we arrive at the candidate concrete operationsto Section 4.2. For now, we assume that we have all the can-didate concrete operations. Our motivation for minimizingthe number of operations is that there are fewer interleav-ings and the linearizability is more likely to be obtained. Seealso Section 5 for additional discussion and formal claims.

4.1 Covering OperationsThe set-cover problem is the following combinatorial

problem: Given the set U of elements (known as the uni-verse) and the family S = {S1, . . . , Sn} of subsets Si of Usuch that

⋃i Si = U , the goal is to find a cover—i.e., a sub-

set {Si1 , . . . , Sik} of S, such that⋃

1≤j≤k Sij = U—whosecardinality is minimal. We strengthen the constraints, furtherrequiring that any given element in U is covered by exactlyone set Si ∈ S, which yields a slight variant of the originalproblem.

In our setting, there are candidate concrete Map opera-tions and the abstract operations. We select the minimal setof concrete operations to cover the abstract operations bysolving the cover problem, in which each concrete operationci is treated as a set of abstract operations that it covers, anal-ogously to Si in the above. For example, given the fragmentk ∈ m ? return m(k) : return null, the concrete opera-tion v=get(k) covers both abstract Map operations, k ∈ mand return m(k), where v can be checked for nullness tocover k ∈ m, and if v is not null, then it stores the expressionm(k).

We then solve the following linear-programming prob-lem:

min∑cj∈C

∨ai∈covered(cj)

xij (1)

∀ai ∈ A,∑

cj∈cover(ai)

xij = 1 (2)

Boolean variable xij denotes whether the expression ai iscovered by the operation cj (xij = 1). Equation 2 specifiesthat every abstract operation is covered by exactly one con-crete operation. Equation 1 enforces that a minimal set ofconcrete operations should be used. Specifically, an invoca-tion cj is used if and only if it covers at least one abstractoperation:

∨ai∈cover(cj) xij = 1. We solve the linear pro-

gram by invoking a standard solver. (See Section 7.)In addition, we must account for control dependencies.

Separating abstract operations that are control dependentinto two (or more) concrete operations typically introducesharmful interleavings [19] . Therefore, we impose an addi-tional constraint that such abstract operations should be cov-ered by a single concrete operation. As an example, if the

Page 8: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

test: k ∈ m44jjjj cc

GGGGGGGGG

return: 2 ·m(k) v = ( )OO

m[k 7→ v]OO

return: m(k)

Figure 12. Dependence graph computed over the (graphical)abstract description in Figure 7

update operation m[k 7→ v] is governed by a conditionalcheck — e.g., m(k) = null (or k /∈ m) — then we selectonly an operation that simultaneously covers bothm[k 7→ v]and the conditional check, such as putIfAbsent. More gen-erally, given abstract operation ai and covering operation cjthat does not account for the conditional check, we introducethe constraint xij = 0. This constraint disables the coveringoperation from being used.

4.2 Candidate Concrete OperationsWe now go back to the important step we glossed over

earlier. That is, we need to find the candidate concrete opera-tions based on the abstract behavior of the method and obtainthe set of abstract operations that each concrete operationcan cover. For this, we first rewrite the abstract descriptioninto SSA representation, so as to simplify the dependencestructure over local variables.

The next step is to compute a data dependence graphover the abstract description. Dependencies are computed atthe abstract level of ADT semantics, as defined by Tripp etal. [32]. To reason about abstract-level dependencies, Trippet al. generalize the standard notion of memory locations toaccommodate abstract memory locations. Following the no-tion, a Map instance then consists of abstract memory loca-tions mapping the Map root to keys, as well as abstract loca-tions mapping keys to values. In this way, the standard no-tion of data dependence, whereby two computations accessa common memory location and at least one of the compu-tations writes that location, is preserved. For our needs, wealso track input (i.e., read/read) dependencies.

To illustrate the resulting representation, we return to ourrunning example in Figure 2, whose abstract description isgiven in Figure 7. The dependence graph for this code isshown in Figure 12. Dependence edges extend from thedepending expression to its dependent expressions. As anexample, node m[k 7→ v] is dependent on test: k ∈ m,because both of these nodes access key k under m.

Next, for each node n in the dependence graph that corre-sponds to a semantic expression, n is mapped to all Map op-erations that are capable of generating its respective expres-sion. As an example, the root k ∈ m node is mapped both tocontainsKey and to get. To maximize the utility of eachconcrete operation (and minimize the number of concreteoperations needed), we heuristically check for each concrete

operation c installed at node n whether the expressions asso-ciated with adjacent nodes are also covered by c. If they arealso covered by c, then we include them into covered(c),i.e., the set of abstract operations covered by c, which is theoutput of this step.

As an illustration, given subgraph

test: k ∈ m ←− return: 2 ·m(k)

of the dependence graph in Figure 12, a v=get(k) operationinstalled at node test: k ∈ m also covers them(k) expressionrequired by the successor return: 2 ·m(k) node. In the actualimplementation, the local variable v that stores the valueof expression m(k) flows into the multiplication operation,yielding the desired return value.

4.3 Complete AlgorithmThe complete synthesis procedure is summarized in pseu-

docode form as Algorithm 1. We first (i) initialize an emptymapping π that we later use to record associations betweenconcrete operations and the abstract operations they cover,and (ii) compute a dependence graph DG over the behav-ior graph G. We then iterate over the nodes of G that rep-resent abstract operations (rather than local expressions), asenforced by the Abs filter. For each node n, we map n tothe operations that are capable of generating n’s respectiveexpression. For each operation c, we compute a maximalregion around n that c covers based on DG. This yields amapping c 7→ {n, n1, . . .} between c and the set of abstractoperations it covers, which we record into π.

Having iterated over all the nodes n ∈ Abs(N) andpopulated π, we submit the abstract operations Abs(N)

as well as the sets of operations maintained as the imageof π (Im(π)) to a solver. This results in a cover, whichis then mapped back to its respective concrete operations.The concrete operations are then composed according to thebehavior graph G and the dependence graph DG to formthe new implementation of the subject method. Composi-tion is achieved by applying standard code transformations— namely, code isolation and extraction [25] — while pre-serving program dependencies. The complete description isgiven in an accompanying technical report 3.

The intuition informing our synthesis algorithm — ofreimplementing the composed operation using the leastamount of shared operations — is characterized in the fol-lowing: Given composed operation m, suppose the trans-formation rewrites some Map operation o in m as a localexpression `, which infers the behavior of o from the returnvalue of an adjacent Map operation o′, which represents theMap state at o′. The transformation guarantees atomicity overo and o′, because they operate on the same Map state as ifthe behavior of o takes effect instantaneously after o′. Basedon the amount of shared operations, we organize the ver-sion m and the transformed version m′ into a partial order,

3 https://sites.google.com/site/flint3141/tr

Page 9: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

ALGORITHM 1: Pseudocode description of the Flintsynthesis algorithm

Input: semantic behavior graph G = (N,E)Output: concrete method m implementing G

1 π←− { }2 DG = (N,EDG)←− compute data dependencies overG

3 foreach node n ∈ Abs(N) do4 {c1, . . .} ←− operations generating expression at n5 foreach c ∈ {c1, . . .} do6 {n, n1, . . .} ←− n neighbors covered by c in

DG

7 π←− π ∪ {c 7→ {n, n1, . . .}}8 end9 end

10 S ⊆ Im(π)←− submit (Abs(N), Im(π)) to solver11 {c1, . . . , ck} ←− map S back to concrete operations12 m←− implement G using {c1, . . . , ck}13 return m

m ≺ m′. Following the partial orders, the greater versionshave more atomicity, and the maximal versions that containonly a single shared operation guarantees atomicity.

5. EnhancementsBeyond the core synthesis algorithm, Flint features facil-

ities to boost the performance of the synthesized method, aswell as handle certain situations where the synthesized im-plementation is still not linearizable. We describe both in thefollowing.

5.1 Performance OptimizationWhile the fix in Figure 5 is correct, it may lead to consid-

erable performance degradation. This is because computeV()is always invoked, even if the respective key is alreadymapped under the Map.

To achieve a fix that has the same level of efficiency asthe original code, we opportunistically attempt to optimizethe core fix. Optimization transformations must, of course,preserve the atomicity of the subject operation. In the follow-ing lemma, we characterize a simple yet common pattern foroptimization, which also guarantees the linearizability. Theproof for this lemma is provided in Appendix C.

Lemma 1. Given a method in the form

if (A) { B } else { C }

and the linearizable fix F produced by Flint,

if (A) { B } else { F }

is also linearizable if,1. The execution following the path A ; B satisfies the data

independence criterion and is linearizable.2. The execution of A does not update the map.

Lemma 1 follows the common fast path idiom, wherethe path A ; B is the fast path. It provides a sound tem-plate for optimizations that preserve linearizability. Follow-ing Lemma 1, we derive the fast path from the original code.We first identify the branch if(A) B that satisfies data inde-pendence and ensures linearizable execution. This is done byinvoking the linearizability checker (Section 2.5). We thencheck A for side effects. Lastly, we combine if(A) B withthe fix F according to Lemma 1. As an illustration, considerthe example in Figure 2. Branch if(A) B corresponds tothe code in lines 2-5. We find that A does not update the Map,and therefore we combine if(A) B with our linearizable butinefficient fix in Figure 5, leading to the linearizable and ef-ficient fix in Figure 6.

5.2 Flint-R: Adding Rollback CapabilitiesBeyond the performance optimization, we have designed

another enhancement to improve the completeness of Flint.If the initial fix produced by Flint fails in the linearizabil-ity checking, e.g., the fix containing multiple invocations,then another transformation is attempted that makes useof lightweight rollback facilities. We dub this enhancementFlint-R.

Flint-R is inspired by the optimistic concurrency-controlframework of Kung et al. [17] (in particular, Sections 3 and4 therein), which assumes that a transaction divides into twophases illustrated in Figure 13: first read and then write. Thewrite phase, before applying any writes, first verifies thatnone of the reads has become invalidated. The write phase isassumed to be atomic. We provide an overview in AppendixB.

Flint-R follows Kung et al.’s framework by classifyingMap operations into two categories: read operations such asget, which do not update the Map state, and write opera-tions such as put and putIfAbsent, which may update theMap state. Flint-R builds a transaction around each write op-eration by treating its execution as the write phase and thepreceding execution as the read phase. In this way, Flint-Rkeeps the write phase short and minimizes the overhead in-curred by the mutual exclusion. Overall, there may be mul-tiple transactions for a composed method.

Importantly, Flint-R requires that the preceding executionhas no global side effects, i.e., it should not write to anyshared program states including the Map and other shareddata structures. Intuitively, this requirement guarantees theexecution may fail in the validation for multiple times with-out changing the program states until it finally passes the val-idation. We check the requirement through the path-sensitivedata-flow analysis, which distinguishes the side effects of theconditional write operations under different path conditions.For example, as illustrated in Pattern (1) of Figure 16, theconditional write operation putIfAbsent has no side ef-fects if its return is not null according to the path condition.The above requirement excludes the case that one write op-

Page 10: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

Read Validate Write Tj

Ti atomic

atomic Figure 13. Illustration of read-then-write transactions

eration follows another successful write operation in a path.Flint-R does not apply if the requirement is not satisfied.

Flint-R is further augmented with the transactional boost-ing [9] according to Map semantics. That is, the valida-tion checks the conflict among transactions at the level ofthe abstract memory locations (Section 4.2), rather thanthe level of concrete memory locations. This eliminates thespurious conflicts. For example, given the transaction withthe write operation map.put(5,5) and the transaction withthe read operation map.get(6), they may conflict at theconcrete memory level 4 because both access the internalfield ConcurrentHashMap$HashEntry.hash [32]. How-ever, they do not conflict with each other at the abstractmemory level as they involve independent mappings of dif-ferent keys. From the implementation perspective, the abovevalidation is implemented following the common practice ofabstract locks [9, 15].

6. Limitations

Limitations shared with verification Data independence(Section 2.5) is a clear limitation of both the verificationwork [28] and our work, which restricts generality. Specifi-cally, the checker can check the linearizability (correctness)only under the data independence conditions. In case thatthese conditions do not hold, the checker conservatively re-ports the composed operation as non-linearizable.

Limitations specific to our work As also specified in Sec-tion 7.1, our work has its own limitations: (1) it focuseson the composed operation involving a single shared Map

object. We do not consider the composed operation involv-ing additional shared collections because the linearizabilitywould be related to all the collections and reasoning about asingle Map object cannot guarantee the overall linearizability.(2) It does not consider the composed operations involvingglobal operations such as size, clear and putAll, whichaccess the global state of the Map object. Such composedoperations require the global synchronization over the entireMap object, e.g., protection of the Map object using the locksor validation of the global Map state using the rollback facil-ities.

Besides, our two enhancements (Section 5) apply only inthe specific conditions. The performance optimization, i.e.,Lemma 1, applies only when (1) the fast path A ; B satisfiesthe data independence and is linearizable, (2) the conditionchecking in the fast path is free of side effects. Intuitively,

4 The put operation may rehash the entries during the resizing while theget operation iterates over the entries to check their hash values.

the requirements specify that the execution may finish fastfollowing the fast path, and if the condition for the fast pathdoes not hold, the execution still proceeds normally startingwith the unchanged program states. The requirements areused in the proof of Lemma 1 for ensuring the correctness.

The rollback recovery, i.e., Flint-R, applies only when theexecution prior to the write phase has no global side effects.Intuitively, this requirement guarantees the execution mayfail for multiple iterations without changing the programstates until it is finally validated.

7. EvaluationOur evaluation addresses three primary questions: (1)

Does our approach have general applicability? (2) Is thefix efficient? (3) Does our fix outperform general solutionsfor composing operations, such as software transactionalmemory (STM)?

7.1 Experimental Design

Prototype Implementation We have implemented a proto-type of the Flint and Flint-R algorithms. Flint is built atopthe Soot framework,5 and extends the available-expressionanalysis provided by Soot. To solve the cover problem, Flintinvokes the Lpsolver tool.6 Since Lpsolver does not sup-port boolean connectives, Flint first replaces occurrencesof the disjunction connective in the input formula with thesum and comparison arithmetic operators. For linearizabil-ity checking, we have implemented our own version of thechecker designed by Shacham [26, 28], which is specializedfor Maps.

Experimental Setting Our experiments and measurementswere all conducted on an x86 64 Thinkpad W530 worksta-tion with eight 2.30GHz Intel Core i7-3610QM processors,16GB of RAM and 6M caches. The workstation runs ver-sion 12.04 of the Ubuntu Linux distribution, and has the Sun64-Bit 1.7 Java virtual machine (JVM) installed.

All the performance test drivers we created, as well as thecollected data, are publicly available. We link to public arti-facts from relevant contexts. In our experiments, we repeatedevery configuration 21 times. The reported statistics reflectthe last 20 runs, excluding the first (cold) run that performsgeneral initialization logic.

Benchmarks Our study covers 48 real-world composedoperations that suffer from linearizability violations. Theseare taken from a diversified set of 27 real-world applications,including Tomcat, Cassandra and MyFaces, as shown inTable 4 in Section A.

These operations are a subset of the full list analyzedby Shacham et al. [26, 27] after eliminating methods thatare (i) linearizable, (ii) nonlinearizable but modify othershared collections beyond the shared Map object, or (iii)

5 http://www.sable.mcgill.ca/soot6 https://code.google.com/p/lpsolver

Page 11: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

contain global operations, such as size, clear and putAll,which involve the entire Map object. The methods in (ii)are excluded because our approach focuses on composedoperations involving a single Map object. The methods in (iii)require global synchronization over the entire Map object.We have made the source code of the extracted operationspublicly available.7

7.2 Aggregate StatisticsWe visualize statistics on successful fixing by both Flint

and Flint-R as a pie chart. Flint is able to successfully fix79% of the buggy methods (38/48), and the combinationof Flint and Flint-R is able to handle 96% of the bugs(46/48) with only 2 bugs being beyond the reach of bothFlint and Flint-R. Beyond the core Flint algorithm, Flint-Ris able to produce a fix in 17% of the cases where Flint fails.In isolation, Flint-R produces fixes only 24 of the buggymethods (16 in common with Flint), which comes to 50%of all benchmarks.

Flint

Flint‐R

Fail

A more de-tailed view ofthe results is pro-vided in Table 3.Each entry in thistable refers to adifferent clusterof buggy meth-ods, whose char-acterization (ex-plained in detail later, in relevant context) is given in theleftmost column and the bugs included in it are given in themiddle column. The rightmost column indicates which ofFlint and Flint-R were able to fix the problem (either one ofthe algorithms or both or none).

In all cases, the Flint algorithm completes in under 100milliseconds. The main reason for this is the modular na-ture of Flint, which refrains from modeling other threadsand thread interleavings. Breakdown of the overall Flint run-ning time into steps reveals that data-flow analysis convergeswithin 50 milliseconds, and solving the cover problem usu-ally requires up to 20 milliseconds. Data-flow analysis con-verges fast, albeit being path sensitive, because the buggymethods mostly feature simple control flow (often withoutloops). The solver returns a solution quickly because thereare typically only few abstract operations (≤ 10) and covers(≤ 5) per buggy method.

These compelling performance statistics for the Flintalgorithm, combined with the high frequency and qualityof successful fixes, suggest Flint as effective IDE-hostedrefactoring tool. In this use case, the developer highlights amethod, which is checked for linearizability, and — if themethod is confirmed to violate linearizability — refactoredby Flint.

7 https://sites.google.com/site/flint3141/

Characterization Bug IDs Fixed ByPattern (a) 1,39,46 FlintPattern (b) 2,3 FlintPattern (c) 5,20,23,27, both

32,34,37,40,41,45,47,48

Pattern (d) 8,17,18,35 FlintPattern (e) 9,28,29, Flint

38,42,43Pattern (f) 24,25 FlintPattern (g) 22,26,30,31 FlintPattern (h) 33 bothPattern (i) 4 bothPattern (j) 14 bothPattern (k) 10 bothPattern (l) 11 FlintIndependent operations 6,7,36 Flint-RDifferent keys 21,44 Flint-RIntervening condition check 15,16,19 Flint-Rtry/catch with side effects 12,13 none

Table 3. Summary of successful bug fixes, where bugs aregrouped according to their characterization

7.3 Detailed Analysis

Bugs Fixed by Flint Beyond the raw statistics, we manu-ally examined the bugs, and classified those fixed by Flintinto a catalog of access patterns. These reflect idiomaticcomposition that leads to linearizability violations. The re-sulting catalog, which consists of 12 categories, is providedin Figure 14. The respective fixes appear in Figure 15. Forconsistency, we normalize variable names: k is a key into theMap, v, v2, . . . and o are values, and the Map reference is m.

For pattern (a), Flint is able to eliminate the get operationafter putIfAbsent. The first get operation is removed dueto minimization, but then reinserted as an optimization. (SeeSection 5.1.) A similar fix is applied for pattern (b), whichhas a slightly more sophisticated behavior. The fix for pattern(c) again utilizes the putIfAbsent interface, though noget calls are dropped. The fix for pattern (d) drops boththe containsKey and get. We cannot optimize pattern (d)further as no path in it ensures linearizable execution.

Patterns (e) and (f) have the same sequential behavior, andthus also the same fix. The get optimization in the buggyversion is featured also in the fix by Flint, and the returnvalue is determined based on the output by putIfAbsent.For pattern (e), the solver (described in Section 4.1) enforcesthe use of putIfAbsent rather than put. Pattern (g) isreminiscent of pattern (d), and thus corresponds to the sameFlint fix. Pattern (h) contains a loop, though this loop alwaysexits after the first iteration in a sequential setting. Hence,Flint eliminates the loop from the fix.

For pattern (i), Flint first eliminates the get call byrelying solely on putIfAbsent. get is then restored asan optimization. The fix for pattern (j) drops the call tocontainsKey and instead uses only get. containsKey

Page 12: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

1 v = m.get(k);2 if (v == null) {3 v = compute(k);4 m.putIfAbsent(k,v);5 v = m.get(k);6 }7 return v;

Pattern (a)

1 v = m.get(k);2 if (v == null) {3 v = compute(k);4 o = m.putIfAbsent(k,v);5 if (o != null ) v = m.get(k);6 }7 return v;

Pattern (b)

1 v = m.get(k);2 if (v == null) {3 v = compute(k);4 m.putIfAbsent(k,v);5 }6 return v;

Pattern (c)

1 if (!m.containsKey(k)) {2 v = compute(k);3 m.putIfAbsent(k,v);4 }5 return m.get(k);

Pattern (d)

1 v = m.get(k);2 if (v == null) {3 v = compute(k);4 m.put(k,v);5 return v;6 }7 return v;

Pattern (e)

1 v = m.get(k);2 if (v == null) {3 v = compute(k);4 m.putIfAbsent(k,v);5 }6 return m.get(k);

Pattern (f)

1 v = compute(k);2 m.putIfAbsent(k,v);3 return m.get(k);

Pattern (g)

1 while (true) {2 v = m.get(k);3 if (v == null) {4 v = compute(k);5 m.putIfAbsent(k,v);6 } else return v;7 }

Pattern (h)

1 v = m.get(k);2 if (v == null) {3 v = compute(k);4 m.putIfAbsent(k,v);5 } else6 update(v);

Pattern (i)

1 if (m.containsKey(k)) {2 return m.get(k);3 }4 return compute(k);

Pattern (j)

1 v = null ;2 if (m.containsKey(k)) {3 v = m.get(k);4 m.remove(k);5 }6 return v;

Pattern (k)

1 r = false ;2 o = m.get(k);3 if (o != null ) r = true;4 m.put(k, compute(k));5 return r ;

Pattern (l)Figure 14. Map access patterns arising in nonlinearizable compositions

1 v = m.get(k);// opt2 if (v == null) {// opt3 v = compute(k);4 o = m.putIfAbsent(k,v);5 v = o == null ? v : o;6 }7 return v;

Pattern (a)

1 v = m.get(k);//opt2 if (v == null) {//opt3 v = compute(k);4 o = m.putIfAbsent(k,v);5 if (o != null ) v = o;6 }7 return v;

Pattern (b)

1 v = m.get(k);//opt2 if (v == null) {//opt3 v = compute(k);4 o = m.putIfAbsent(k,v);5 v = o == null ? v : o;6 }7 return v;

Pattern (c)

1 v = compute(k);2 o = m.putIfAbsent(k,v);3 return o == null ? v : o;

Pattern (d)

1 v = m.get(k);//opt2 if (v == null) {//opt3 v = compute(k);4 o = m.putIfAbsent(k,v);5 return o == null ? v : o;6 }7 return v;

Pattern (e)

1 v = m.get(k);//opt2 if (v == null) {//opt3 v = compute(k);4 o = m.putIfAbsent(k,v);5 return o == null ? v : o;6 }7 return v;

Pattern (f)

1 v = compute(k);2 o = m.putIfAbsent(k,v);3 return o == null ? v : o;

Pattern (g)

1 v = m.get(k);//opt2 if (v == null) {//opt3 v = compute(k);4 o = m.putIfAbsent(k,v);5 v = o == null ? v : o;6 }7 return v;

Pattern (h)

1 v = m.get(k);//opt2 if (v != null ) {//opt3 update(v); return ; }//opt4 v = compute(k);5 o = m.putIfAbsent(k,v);6 if (o != null ) update(o);

Pattern (i)

1 if (!m.containsKey(k))//opt2 return compute(k);//opt3 v = m.get(k);4 if (v != null )5 return v;6 return compute(k);

Pattern (j)

1 if (!m.containsKey(k))//opt2 return null ;//opt3 v = m.remove(k);4 return v;

Pattern (k)

1 r = false ;2 o = m.put(k, compute(k));3 if (o != null ) r = true;4 return r ;

Pattern (l)Figure 15. Natural fixes for nonlinearizable compositions in Figure 14 (// opt denotes optimizations beyond the core fix.)

1 o = m.putIfAbsent(k,v);2 if (o != null )3 { v2=merge(v,o);4 b=replace(k,o,v2); ...}

Pattern (1): independent operations

1 v = m.get(clazz);2 ...3 v = m.get(Map.class);

Pattern (2): access to two keys

1 v = m.get(k);2 if (v == null) {3 if (! enableStatic ) { m.putIfAbsent(k,v); }4 }

Pattern (3): intervening condition checkFigure 16. Bugs that demand Flint-R

Page 13: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

is later restored as an optimization. Similarly, in pattern (k)all operations except remove are dropped by Flint, but thencontainsKey is inserted as an optimization. Finally, in pat-tern (l) get is removed, and the result is instead based on theoutput of put.

Bugs Fixed Only by Flint-R There are 8 bugs that can-not be fixed using Flint, but can be fixed by Flint-R, asshown in Table 3. The coding idioms underlying these casesare listed in Figure 16. In pattern (1), the update operationsputIfAbsent and replace have different semantics, andneither can be used to cover the other. An example of thispattern is bug 6, which cannot be rewritten by Flint. Flint-Rapplies because the preceding execution for either update op-eration is free of side effects according to the path-sensitiveanalysis. Note that the putIfAbsent operation does notwrite to the Map if the execution follows the if branch.

Pattern (2) refers to methods that access more than onekey. Flint cannot use one concrete operation to cover differ-ent keys. An example of this is bug 21. Rollbacks provide amechanism to deal with such situations.

Last, pattern (3) — manifesting e.g. in bug 15 — refers tothe condition check c (line 3) intervening between the Map

operations op1 and op2 (at lines 1 and 3). The dependenciesop1 → c and c → op2 prevent one concrete operation fromcovering both, as they are even not adjacent in the depen-dence graph. Again, rollback facilities provide a solution.

An important constraint on the applicability of Flint-Ris that the execution prior to the write phase should notcarry any global side effects. For example, in pattern (1)in Figure 16, the execution until replace succeeds carriesno side effects. This constraint on Flint-R is because itsspeculative behavior is limited to only rolling back localstate, being less powerful (and thus also more lightweight)than STM systems.

The constraint also explains why Flint-R fails to fix manybugs, including most of those fixed by Flint, where inter-mediate evaluation steps produce side effects. As an exam-ple, pattern (g) — which Flint addresses by minimizing thenumber of used APIs — cannot be fixed by Flint-R becausesuccessful invocation of putIfAbsent creates side effects.

A final important point that we emphasize is that Flint-Ris, in general, more applicable to the composition producedby Flint than to the original composition. That is, even ifFlint proposes a candidate that is not simply linearizable,Flint-R can often recover linearizability for that candidatevia speculation, but not for the original buggy method. Thekey reason for this is that abstracting the sequential behaviorof the method, and then biasing synthesis toward minimality,has the pleasant side effect that there are less update opera-tions and updates in general.

To illustrate this, we refer to Bug 36 in Figure 17. First,the else branch at lines 6-7 is removed by our abstractionbecause the branch is never taken in sequential executions.Second, abstraction merges the operations at lines 4 and 5

1 o=m.putIfAbsent(k,v);2 if (o !=null &&3 !o. registered ()) {4 if (m.remove(k,o))5 return m.putIfAbsent(k,v);6 else7 return o; }8 else9 return o;

Figure 17. Bug 36: unsafecomposition within a loopstructure

1 o=m.putIfAbsent(k,v);2 if (o != null &&3 !o. registered ()) {4 if (m.replace(k,o,v))5 return null ; }6 else7 return o;

Figure 18. Rewritten ver-sion of bug 36, which iscompatible with Flint-R

into a single replace invocation. This step is crucial to en-abling Flint-R. Otherwise, bad interleavings between lines 4and 5 cannot be recovered from due to the side effects causedby remove. Under abstraction, m.remove(k,v) becomesif(m(k) == v)m[k 7→ null], and m.putIfAbsent(k,v)

becomes if(m(k) == null)m[k 7→ v]. The overall behav-ior is then if(m(k) == v)m[k 7→ v], which is covered by thereplace API. The resulting fix is shown in Figure 18. Flint-R now applies because there is a single write operation andits preceding execution is side effect free.

Bugs that are Beyond Flint and Flint-R Bugs 12 and 13cannot be fixed neither by Flint nor by Flint-R. This isbecause there are Map operations op1 and op2 that are placedin the try and catch blocks separately. Denote the catch

block by c. Then the dependencies op1 → c and c → op2prevent one operation from covering the other, as they arenot even adjacent in the dependence graph. As for Flint-R,the code between the operations involves updates to sharedstate outside the Map such as logs, which cannot be rolledback.

7.4 Performance Measurements

We now turn to measure the performance characteristicsof Flint fixes.

Significance of Optimization The first experiment mea-sures the runtime overhead of fixes produced by Flint withand without optimization. We refer to the optimized ver-sion — which is available for most patterns but not for (d),(g) and (l) — as Flint-O. For this experiment, we createda test driver for every composed operation, in which mul-tiple threads running concurrently share a fixed-size work-load (varying slightly between operations to align runningtimes). The workload consists of random inputs to the oper-ation with the exception of patterns (j) and (k), for which thedriver first populates the Map. (Otherwise certain functional-ity, like lines 3–4 in pattern (k), is never executed.)

The results are depicted in Figures 19a–19f. The graphsvisualize running time as a function of the concurrency level,which peaks at 8 threads because this is the number of coreson the machine. In general, Flint-O fixes incur negligibleoverhead (less than 1%), and may even outperform the orig-inal version as certain invocations are dropped (like the get

Page 14: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

Figure 20. Slowdown caused by the unoptimized fix

call in pattern (f)). Flint fixes, on the other hand, may incurup to 50% overhead, highlighting the importance of opti-mization. For pattern (d), an optimized fix cannot be com-puted by our transformation algorithm, and indeed the fix is44%-62% slower than the original code. Figure 20, whichplots running time as a function of workload size for pattern(f), demonstrates that without optimization, slowdown maygrow proportionally to the number of threads. In the case ofpattern (f), this is because compute is invoked even if thekey is in the Map.

Comparison with General Techniques The second experi-ment compares between Flint/Flint-O (where Flint-O is usedwhen different from Flint), Flint-R and two other variants.The first is STM, which is a general synchronization ap-proach that guarantees atomicity optimistically by recordingshared memory accesses and rolling back bad executions.The second is the abstract lock synchronization at the gran-ularity of Map keys [7, 9], which is reminiscent of our data-flow analysis. For STM, we used the Deuce library,8 and inparticular, the default Deuce protocol, called TL2 [6], whichapplies eager speculation. For this experiment, we used high-contention workloads to study the different approaches un-der high stress. This was achieved by (i) increasing the num-ber of operations while narrowing the key pool, as well as(ii) focusing the test drivers on operations that do not com-mute with the operations in the fixed version according tothe heuristics proposed by Shacham et al. [27].

The results are presented in Figure 21. For patterns shar-ing the same fix, we show the comparison for only one repre-sentative. As the graphs indicate, when there are more than 8threads, our fix is generally (i) around 5% faster than the roll-back fix (Figures 21a, 21d and 21f), (ii) 14%-20% faster thanthe locking fix (Figures 21a, 21b and 21d), and (iii) 15%-26% faster than the STM fix (Figures 21a, 21b, 21d, 21eand 21f). These differences in performance cannot be seenwith ≤ 4 threads, which suggests that they are associated

8 https://sites.google.com/site/deucestm

with the concurrency of the fix rather than its overhead (ex-cept STM, which incurs visible overhead at all concurrencylevels due to memory instrumentation and runtime checks).

8. Related Work

Blocking Synchronization Both dynamic approaches [4,23] and static approaches [13, 20, 35] have been proposed tofix atomicity violations. Dynamic methods typically lever-age architecture support for detection of bugs, which in-curs observable runtime overhead. Prevention of atomicityviolations is achieved at runtime by delaying the executionof some thread. Static approaches typically introduce newlocks to fix atomicity violations, which limits concurrencyand may also lead to deadlocks.

Nonblocking Synchronization Nonblocking approaches— most notably STM — have also been proposed for recov-ery from atomicity violations [21, 36]. These require conflictdetection and rollback facilities. Conflict detection at thefield level is overly conservative for Map updates because ir-relevant fields, such as hash or modCount, are written by op-erations accessing different keys [32]. This leads to unneces-sary rollbacks. Zhang et al. [37] propose idempotence-basedrollback, which does not require checkpointing, similarly toFlint-R. Their rollback, designed for general use, requiresthe programmer’s help in identifying the failure sites, whileour approach automatically identifies failures through thestandard Map semantics. Besides, we leverage Map seman-tics and path sensitivity to precisely reason about the sideeffects. As an example, putIfAbsent has no side effectswhen it returns a non-null value.

Map Semantics Synchronization at the level of abstractMap semantics has been utilized to improve STM [9, 16, 30,31]. This helps in reducing the false alarms due to spuriousconflicts. Similarly to our approach, the approach proposedby Lin et al. [18] also leverages Map semantics to fix incor-rect compositions. However, their approach is based on syn-tactic patten matching, and is therefore limited to predefinedtemplates. Besides, their approach guarantees the absence ofruntime exceptions, but may lead to fixes that are function-ally incorrect fixes (like the one in 4).

Privatization Privatization transformations [12, 24, 33]have also been proposed to fix atomicity violations. Pri-vatization turns shared reads to local reads to avoid buggyinterleavings, similarly to the concept of Flint. Flint addi-tionally features inference logic, with which it automaticallyresolves which Map accesses to privatize.

Synthesis Our approach can be used for synthesis, the se-quential code serving as the specification. Vechev et al. [34]propose a semi-automatic approach to synthesize the lin-earizable implementation in general. Their approach, how-ever, interactively involves the programmer. Our approach,instead, is built on the standard semantics of Maps, and with

Page 15: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

(a) Patterns (a), (b), (c), (e), (f) and (h) (b) Patterns (d) and (g) (c) Pattern (i)

(d) Pattern (j) (e) Pattern (k) (f) Pattern (l)Figure 19. Overhead

(a) Patterns a, b, c, e, f and h (Pattern c as therepresentatitve)

(b) Patterns d and g (Pattern d as the representative) (c) Pattern i

(d) Pattern j (e) Pattern k (f) Pattern lFigure 21. Performance comparison

Page 16: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

this focus we do not rely on user interaction. Hawkins etal. [8] automatically synthesize, based on a relational specifi-cation, a concurrent lock-based data structure. Our approachinstead leverages safe compositions of concurrent Map APIs.Besides, at the conceptual level, our technique is similar tothe instruction selection technique [2] (Chapter 9), a classi-cal compiler technique that finds a cover for the primitiveinstructions using the real machine instructions. Each realmachine instruction typically performs several primitive in-structions. However, our technique is different from the in-struction selection technique: (i) we address Map operationsrather than low-level instructions, and (ii) we solve the coverproblem at the abstract rather than concrete level.

9. Conclusion and Future WorkWe have designed and evaluated Flint, the first general

algorithm for automatically transforming nonlinearizablecompositions of Map operations into atomic compositions.Our approach performs semantic modeling of the composedoperation, then reimplements it using a minimal set of MapAPIs, and finally applies optimizations. Evaluation of Flinton 48 incorrect compositions from 27 popular applicationsyields strong evidence in favor of our approach. In the fu-ture, we intend to generalize Flint beyond Maps via a generalspecification format for concurrent data structures with ADTsemantics. We would also like to extend Flint to supportglobal operations (like size).

References[1] A. V. Aho, M. S. Lam, R. Sethi, and J. D. Ullman. Compilers:

Principles, Techniques, and Tools (Second Edition). Addison-Wesley Longman Publishing Co., Inc., 2006.

[2] A. W. Appel and J. Palsberg. Modern Compiler Implementa-tion in Java (Second Edition). Cambridge University Press,2002.

[3] S. Burckhardt, C. Dern, M. Musuvathi, and R. Tan. Line-up:a complete and automatic linearizability checker. In PLDI,2010.

[4] L. Chew and D. Lie. Kivati: Fast detection and prevention ofatomicity violations. In EuroSys, 2010.

[5] R. Cytron, J. Ferrante, B. K. Rosen, M. N. Wegman, and K. F.Zadeck. Efficiently computing static single assignment formand the control dependence graph. TOPLAS, 1991.

[6] D. Dice, O. Shalev, and N. Shavit. Transactional locking ii. InDISC, 2006.

[7] G. Golan-Gueta, G. Ramalingam, M. Sagiv, and E. Yahav.Concurrent libraries with foresight. In PLDI, 2013.

[8] P. Hawkins, A. Aiken, K. Fisher, M. Rinard, and M. Sagiv.Concurrent data representation synthesis. In PLDI, 2012.

[9] M. Herlihy and E. Koskinen. Transactional boosting: amethodology for highly-concurrent transactional objects. InPPoPP, 2008.

[10] M. Herlihy, V. Luchangco, and M. Moir. A flexible frameworkfor implementing software transactional memory. In OOPSLA’06.

[11] M. P. Herlihy and J. M. Wing. Linearizability: A correctnesscondition for concurrent objects. TOPLAS, 1990.

[12] J. Huang and C. Zhang. Execution privatization for scheduler-oblivious concurrent programs. In OOPSLA, 2012.

[13] G. Jin, L. Song, W. Zhang, S. Lu, and B. Liblit. Automatedatomicity-violation fixing. In PLDI, 2011.

[14] G. A. Kildall. A unified approach to global program optimiza-tion. In POPL, 1973.

[15] M. Kulkarni, D. Nguyen, D. Prountzos, X. Sui, and K. Pingali.Exploiting the commutativity lattice. In PLDI, 2011.

[16] M. Kulkarni, D. Nguyen, D. Prountzos, X. Sui, and K. Pingali.Exploiting the commutativity lattice. In PLDI, 2011.

[17] H. T. Kung and J. T. Robinson. On optimistic methods forconcurrency control. TODS, 1981.

[18] Y. Lin and D. Dig. Check-then-act misuse of java concurrentcollections. In ICST, pages 164–173, 2013.

[19] P. Liu, J. Dolby, and C. Zhang. Finding incorrect compositionsof atomicity. In ESEC/FSE, 2013.

[20] P. Liu and C. Zhang. Axis: automatically fixing atomicityviolations through solving control constraints. In ICSE ’12.

[21] B. Lucia, J. Devietti, K. Strauss, and L. Ceze. Atom-aid:Detecting and surviving atomicity violations. In ISCA, 2008.

[22] D. Prountzos, R. Manevich, K. Pingali, and K. S. McKinley.A shape analysis for optimizing parallel graph programs. InPOPL, 2011.

[23] S. Rajamani, G. Ramalingam, V. P. Ranganath, andK. Vaswani. Isolator: dynamically ensuring isolation in con-current programs. In ASPLOS, 2009.

[24] P. Ratanaworabhan, M. Burtscher, D. Kirovski, B. G. Zorn,R. Nagpal, and K. Pattabiraman. Detecting and toleratingasymmetric races. In PPOPP, 2009.

[25] M. Schafer, M. Verbaere, T. Ekman, and O. Moor. Steppingstones over the refactoring rubicon. In ECOOP, 2009.

[26] O. Shacham. Verifying Atomicity of Composed ConcurrentOperations. PhD thesis, Tel Aviv University, 2012.

[27] O. Shacham, N. G. Bronson, A. Aiken, M. Sagiv, M. T.Vechev, and E. Yahav. Testing atomicity of composed con-current operations. 2011.

[28] O. Shacham, E. Yahav, G. Gueta, A. Aiken, N. Bronson,M. Sagiv, and M. Vechev. Verifying atomicity via data in-dependence. In ISSTA, 2014.

[29] N. Shavit. Data structures in the multicore age. CACM, 2011.

[30] O. Tripp, R. Manevich, J. Field, and M. Sagiv. Janus: exploit-ing parallelism via hindsight. In PLDI, 2012.

[31] O. Tripp and N. Rinetzky. Tightfit: Adaptive parallelizationwith foresight. In ESEC/FSE, 2013.

[32] O. Tripp, G. Yorsh, J. Field, and M. Sagiv. Hawkeye: effec-tive discovery of dataflow impediments to parallelization. InOOPSLA, 2011.

[33] P. Cerny, T. A. Henzinger, A. Radhakrishna, L. Ryzhyk, andT. Tarrach. Efficient synthesis for concurrency by semantics-preserving transformations. In CAV, 2013.

Page 17: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

[34] M. T. Vechev and E. Yahav. Deriving linearizable fine-grainedconcurrent objects. In PLDI, 2008.

[35] D. Weeratunge, X. Zhang, and S. Jaganathan. Accentuatingthe positive: Atomicity inference and enforcement using cor-rect executions. In OOPSLA, 2011.

[36] J. Yu and S. Narayanasamy. Tolerating concurrency bugsusing transactions as lifeguards. In MICRO, 2010.

[37] W. Zhang, M. de Kruijf, A. Li, S. Lu, and K. Sankaralingam.Conair: Featherweight concurrency bug recovery via single-threaded idempotent execution. In ASPLOS, 2013.

A. Linearizability

Linearizability Formally, an operation op consists of aninvocation event (t, op,~v), where t denotes the thread iden-tifier, op is the operation identifier, and ~v are the arguments,and a response event (t, op, r), where r is the return value.Given operation op, we denote its invocation (resp. response)by inv(op) (reap. res(op)). We consider finite sequences ofinvocation and response events, known as histories.

A complete invocation of operation op is the two-eventhistory [inv(op), res(op)]. A complete history is a historywhere every invocation event is matched by a response event.A sequential history is the composition of one or more com-plete invocations. A thread subhistory, h|t, is the projectionof history h on the events where t is incident. We assume asequential specification for the concurrent object, and thusthe legality of a sequential history is decided by the spec-ification. We refer to histories h and h′ as equivalent if∀t. h|t ≡ h′|t. Given operations op and op′ in history h,we say that op precedes op′ and write op <h op

′ if res(op)appears before inv(op′) in h.

A history h is linearizable if there exists a legal sequentialhistory h′, such that

h ≡ h′∧∀op, op′. op <h op

′ ⇒ op <h′ op′

That is, h′ is equivalent to h, and further respects the globalordering of non-overlapping operations in h. Finally, a con-current object is linearizable if all the histories over that ob-ject are linearizable.

B. Kung et al.’s FrameworkThe method developed by Kung et al. enforces serial

execution of the write phases using the mutual exclusionsynchronization, i.e., the lock. To achieve atomicity of thetransactions, it further requires, for any two transactions Tiand Tj , either (1) the transaction Ti finishes the write phasebefore the transaction Tj starts the read phase, or (2) Tifinishes the write phase before Tj starts the write phase andthe write set of Ti does not intersect the read set of Tj . Ifthe write set and the read set intersect each other, we say thetransactions Ti and Tj conflict.

From the implementation perspective, Kung et al. havedesigned a counter-based approach to assign unique transac-tion identifiers and identify the transactions that may conflictwith the current transaction, i.e., the transactions that finishthe write phase after the current transaction starts the readphase but before it starts the write phase. The method byKung et al. then validates whether the identified transactionsand the current transaction conflict. An important propertyof the framework is it does not need the heavyweight check-pointing to support rollbacks [10, 22]. If validation fails,there are no special actions to be taken except restarting thetransaction, because the preceding history consisted only ofread operations.

Page 18: Flint: Fixing Linearizability Violations more parallel, and so the demand for efficient yet cor-rect parallel software is on the rise [29]. To address this ten-sion, popular programming

C. ProofsC.1 Proof for Lemma 1Proof. We begin by giving an informal intuition. Intuitively,the path A;B acts as a shortcut which allows the executionto return fast. In case that the execution does not follow theshortcut path and takes the other branch, the code F resumesthe execution, being oblivious to the state information ac-quired at A, as if A is not executed. Executing F alone sufficesto ensure the correctness. Specifically, suppose the threadfollows the path A;F but is interleaved by another operation*, i.e., A;*;F. Because the test performed in A is also doneseparately within F, the behavior of F is independent of theevaluation of A. Hence, A;*;F is equivalent to *;F, which isthe sequential execution of the fix F.

For the full proof, we need to fix the formal setting. Wehave a main thread executing the composed operation C andother (environment) threads executing atomic operations.Let < l,m > denote the program state, where l denotesthe state of the local variables (including program counterpc) in C and m denotes the map state. Three types of statetransitions exist: (1) local transition (Equation 3) and (2)main transition (Equation 4) model the basic execution stepsof the main thread, which correspond to local operationand Map operation, respectively; (3) environment transition(Equation 5) models a Map operation, or specifically Map

update [27], executed by an environment thread.

< l,m >s→< l′,m > (3)

< l,mi >x=m.op(args)→ < l[x→ ret],mi+1 > (4)

< l,mi >m.op(args)→ < l,mi+1 > (5)

The execution may follow two paths, A;B; or A;F;,where the former is required to be linearizable and the lat-ter is to be proven as linearizable. Consider the executionA; ∗;F , where the interleaving operation ∗ happens just af-ter the main thread enters the branch that contains F . Equa-tion 6 shows the state transitions.

< l0,m0 >A→< l1,m0 >

∗→< l1,m1 >F→< l2,m2 > (6)

< l0,m0 >∗→< l0,m1 >

A→< l1,m1 >F→< l2,m2 > (7)

< l0,m0 >∗→< l0,m1 >

A→< l2,m1 >B→< l3,m2 > (8)

The interleaved execution is equivalent to the sequentialexecution ∗;A;F or ∗;A;B. Which branch to take, B orF , depends on the state after ∗. If the sequential executionis ∗;A;F , the state transitions, shown in Equation 7, areequivalent to transitions in Equation 6 because F , as anindependent fix, does not read any locals defined by A,and the input states to F in Equation 6 and Equation 7 areequivalent.

If the sequential execution is ∗;A;B, the state transitionsare shown in Equation 8. The input state to A;B; in Equa-

tion 8 and the input to F in Equation 6 are identical exceptthey have different local states. Recall that F does not readthe locals defined by A (Equation 6), therefore, F and A;B;essentially start with the same local states, i.e., the emptystates. Besides, the executions following both F and A;B;are linearizable, transiting to the same output state as the se-quential execution does.

Table 4. Real World Applications

Application DescriptionAdaptive planning Tool for corporate budgeting

Adobe BlazeDS Server-based remoting tool

Annsor Runtime annotation processing tool

Apache Cassandra Distributed NoSQL database

Apache MyFaces Trinidad JSF framework

Apache Tomcat Java Servlet container

ApacheCXF Services framework

Carbonado Persistence abstraction layer

CBB Concurrent building blocks

dyuproject REST framework

Ektorp Persistence layer on top of CouchDB

EntityFS File system library

fleXive Content repository

GlassFish JavaServer faces

gridkit Data grid library

GWTEventService Event-based communication framework

Hazelcast In-memory data grid

Hudson Continuous integration (CI) tool

JRipples Change impact analysis tool

memcache-client Java memcache client

P-GRADE P-GRADE Grid Portal

Tammi JMX-based development framework

RESTEasy REST framework

Retrotranslator Java compatibility tool

Torque-spring Torque support classes

Vo Urp UML Representation Pipeline

WebMill CMS portal