Detecting concurrency issues with JCStress
Juan Antonio Breña Moral
"Testing shows the presence of bugs, not their absence"
- Edsger W. Dijkstra"Quality is never an accident. It is always the result of intelligent effort."
- John Ruskin"At the heart of any reasonable definition of thread safety is the concept of correctness. Correctness means that a class conforms to its specification."
- Brian Goetz, Java Concurrency in Practice, 2006
Engineering in Industrial Organization (M.S.) @ ICAI Head of Developer Relations @ Amadeus for developers Associate Professor @ ICAI STEAM Teacher @ Space Math @juanantoniobm | Github |
Imagine development with some business requirements:
Feature: Sum 2 numbers
Scenario: Sum 2 positive numbers
Given the POST endpoint /api/v1/sum2numbers
When the client sends a request
Then the response includes the sum of the 2 parameters
The code has a good Unit Test:
The code has a good Test Coverage:
The code has a good Mutation Coverage:
But..., one day we received an email from a User about unexpected results...
Developer: "Weird, everything is fine on my side..."
Developer: "...and using the data provided by the User, the new test doesn´t reproduce the issue in my local dev environment."
Developer: "...maybe, I didn´t read the JLS (Java Language Specification), section 17.4. about Concurrency and the JSR-133 Java Memory Model and Thread Specification"
Developer: "...but now, we are already in Production..."
And every Java developer needs to know it.
Do you properly define the non-functional requirements for your projects?
ISO 25010, titled “System and software quality models”, is a software quality standard.
Not all programming languages followed the same design criterias to tackle the concurrency.
How concurrency evolves in Java:
Java version | Key features | Release data |
---|---|---|
Java 1.0 | Java OS Threads | 23/01/1996 |
Java 1.5 | JSR 133, java.util.concurrent.* | 30/09/2004 |
Java 1.7 | Fork/join framework | 07/07/2011 |
Java 1.8 | CompletableFuture | 18/03/2014 |
Java 19 | Virtual Threads, Structured Concurrency | 20/09/2022 |
** Records (Java 14)
CDI, Contexts and Dependency Injection allows the developer to manage the lifecycle of stateful components.
Functional programming
public class EulerProblem20 {
Function factorial = limit -> IntStream.iterate(limit.intValue(), i -> i - 1)
.limit(limit)
.mapToObj(BigInteger::valueOf)
.reduce((n1, n2) -> n1.multiply(n2)).get();
Function sumDigits = value -> value.toString().chars()
.mapToObj(c -> String.valueOf((char) c))
.mapToLong(Long::valueOf)
.reduce(0L, Long::sum);
public Long solution(Long limit) {
return factorial
.andThen(sumDigits)
.apply(limit);
}
}
Functional programming
public class EulerProblem01 {
private final int THREE = 3;
private final int FIVE = 5;
BiPredicate< Integer, Integer > isMultiple = (l, i) -> l % i == 0;
Predicate< Integer > isMultiple3 = number -> isMultiple.test(number, THREE);
Predicate< Integer > isMultiple5 = number -> isMultiple.test(number, FIVE);
public Integer solution(Integer limit) {
return IntStream.range(1, limit).boxed()
.filter(isMultiple3.or(isMultiple5))
.reduce(0, Integer::sum);
}
}
Java records are classes that act as transparent carriers for immutable data.
public record Person (String name, String address) {}
Source:
https://openjdk.org/jeps/395
The Java Concurrency Stress (jcstress) is an experimental harness and a suite of tests to aid the research in the correctness of concurrency support in the JVM, class libraries, and hardware.
Project: https://github.com/openjdk/jcstress
@JCStressTest
@Outcome(id = "1, 1", expect = ACCEPTABLE_INTERESTING, desc = "Both actors came up with the same value: atomicity failure.")
@Outcome(id = "1, 2", expect = ACCEPTABLE, desc = "actor1 incremented, then actor2.")
@Outcome(id = "2, 1", expect = ACCEPTABLE, desc = "actor2 incremented, then actor1.")
@State
public class API_01_Simple {
int v;
@Actor
public void actor1(II_Result r) {
r.r1 = ++v; // record result from actor1 to field r1
}
@Actor
public void actor2(II_Result r) {
r.r2 = ++v; // record result from actor2 to field r2
}
}
@JCStressTest
@Outcome(id = "1", expect = ACCEPTABLE_INTERESTING, desc = "One update lost: atomicity failure.")
@Outcome(id = "2", expect = ACCEPTABLE, desc = "Actors updated independently.")
@State
public class API_02_Arbiters {
int v;
@Actor
public void actor1() {
v++;
}
@Actor
public void actor2() {
v++;
}
@Arbiter
public void arbiter(I_Result r) {
r.r1 = v;
}
}
public class API_04_Nesting {
@JCStressTest
@Outcome(id = "1, 1", expect = ACCEPTABLE_INTERESTING, desc = "Both actors came up with the same value: atomicity failure.")
@Outcome(id = "1, 2", expect = ACCEPTABLE, desc = "actor1 incremented, then actor2.")
@Outcome(id = "2, 1", expect = ACCEPTABLE, desc = "actor2 incremented, then actor1.")
@State
public static class PlainTest {
int v;
@Actor
public void actor1(II_Result r) {
r.r1 = ++v;
}
@Actor
public void actor2(II_Result r) {
r.r2 = ++v;
}
}
@JCStressTest
//Another test
}
@Outcome(id = "1, 1", expect = ACCEPTABLE_INTERESTING, desc = "Both actors came up with the same value: atomicity failure.")
@Outcome(id = "1, 2", expect = ACCEPTABLE, desc = "actor1 incremented, then actor2.")
@Outcome(id = "2, 1", expect = ACCEPTABLE, desc = "actor2 incremented, then actor1.")
public class API_05_SharedMetadata {
@JCStressTest
@JCStressMeta(API_05_SharedMetadata.class)
@State
public static class PlainTest {
int v;
@Actor
public void actor1(II_Result r) { r.r1 = ++v; }
@Actor
public void actor2(II_Result r) { r.r2 = ++v; }
}
@JCStressTest
@JCStressMeta(API_05_SharedMetadata.class)
//Another test
}
@JCStressTest
@Description("Sample Hello World test")
@Ref("http://openjdk.java.net/projects/code-tools/jcstress/")
@Outcome(id = "1, 1", expect = ACCEPTABLE_INTERESTING, desc = "Both actors came up with the same value: atomicity failure.")
@Outcome(id = "1, 2", expect = ACCEPTABLE, desc = "actor1 incremented, then actor2.")
@Outcome(id = "2, 1", expect = ACCEPTABLE, desc = "actor2 incremented, then actor1.")
@State
public class API_06_Descriptions {
int v;
@Actor
public void actor1(II_Result r) {
r.r1 = ++v;
}
@Actor
public void actor2(II_Result r) {
r.r2 = ++v;
}
}
As you see, tests using actors or arbiters require the use of results objets, which accept different primitives:
Source: https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.htmls
How to run the examples?
./gradlew clean jcstress --tests "API_01_Simple"
./gradlew clean jcstress --tests "API_02_Arbiters"
./gradlew clean jcstress --tests "API_04_Nesting"
./gradlew clean jcstress --tests "API_05_SharedMetadata"
./gradlew clean jcstress --tests "API_06_Descriptions"
There is a plugin for Gradle, but not for Maven.
plugins {
id 'io.github.reyerizo.gradle.jcstress' version '0.8.13'
}
ext {
jcstressVersion = '0.15'
}
jcstress {
jcstressDependency "org.openjdk.jcstress:jcstress-core:${jcstressVersion}"
verbose = true
timeMillis = "200"
spinStyle = "THREAD_YIELD"
}
Source:
https://github.com/reyerizo/jcstress-gradle-plugin/blob/master/README.md
But using Maven, you could run your JCstress tests:
Source: https://github.com/gortiz/jcstress-mvn-testBut using Maven, you could run your JCstress tests:
In what stage is JCStress?
In that case, you will have to test without this tool. 🤷♂️
Set up a session with your Dev team to review the samples from JCStress repository: