https://github.com/stawirej/threads-collider
https://github.com/stawirej/threads-collider
Last synced: 5 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/stawirej/threads-collider
- Owner: stawirej
- License: apache-2.0
- Created: 2023-10-12T20:30:58.000Z (over 1 year ago)
- Default Branch: master
- Last Pushed: 2024-04-21T17:52:14.000Z (about 1 year ago)
- Last Synced: 2024-04-21T23:27:31.459Z (about 1 year ago)
- Language: Java
- Size: 163 KB
- Stars: 12
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-java - Threads Collider
README
## About
`Threads Collider` attempts to execute a desired action on multiple threads at the "exactly" same moment to increase the
chances of manifesting issues caused by a race condition or a deadlock.## Overview
```java
@RepeatedTest(10)
void Thread_safe_adding_to_list() {
// Given
List list = new ArrayList<>(); // <-- NOT thread safe// When
try (ThreadsCollider threadsCollider =
threadsCollider()
.withAction(() -> list.add("bar"))
.times(Processors.ALL) // run action on all available processors
.build()) {threadsCollider.collide();
}// Then
then(list).hasSize(Processors.ALL).containsOnly("bar");
}
```#### Output

#### Example failure
```bash
java.lang.AssertionError:
Expecting ArrayList:
[null, null, null, null, null, null, "bar", "bar"]
to contain only:
["bar"]
but the following element(s) were unexpected:
[null, null, null, null, null, null]
```#### Fixed version
```java
@RepeatedTest(10)
void Thread_safe_adding_to_list() {
// Given
List list = Collections.synchronizedList(new ArrayList<>()); // <-- thread safe// When
try (ThreadsCollider threadsCollider =
threadsCollider()
.withAction(() -> list.add("bar"))
.times(Processors.ALL) // run action on all available processors
.build()) {threadsCollider.collide();
}// Then
then(list).hasSize(Processors.ALL).containsOnly("bar");
}
```## Usage
### Single action
- You can create `ThreadsCollider` using `ThreadsColliderBuilder`.
- Use `junit5` `@RepeatedTest` annotation to run test multiple times to increase chance of manifesting concurrency issues.```java
@RepeatedTest(10)
// run test multiple times to increase chance of manifesting concurrency issues
void Thread_safe_adding_to_set() {
// Given
Set set = Collections.synchronizedSet(new HashSet<>());
List exceptions = Collections.synchronizedList(new ArrayList<>());// When
try (ThreadsCollider threadsCollider = // use try-with-resources to automatically shutdown threads collider
threadsCollider()
.withAction(() -> set.add("foo")) // action to be executed simultaneously
.times(Processors.ALL) // run action on all available processors
.withThreadsExceptionsConsumer(exceptions::add) // save threads exceptions, default consumer do nothing
.withAwaitTerminationTimeout(100) // threads collider will wait 100 milliseconds for threads to finish, default 60 seconds
.asMilliseconds()
.build()) { // build threads colliderthreadsCollider.collide(); // code to be executed simultaneously at "exactly" same moment
}// Then
then(set).hasSize(1).containsExactly("foo");
}
```### Multiple actions
- We have thread safe `Counter` class:
```java
public class Counter {private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}public void decrement() {
counter.decrementAndGet();
}public int value() {
return counter.get();
}
}
```> **_NOTE:_** Change `counter` field to `int` to manifest concurrency issues.
```java
@RepeatedTest(10)
// run test multiple times to increase chance of manifesting concurrency issues
void Thread_safe_counter() {
// Given
Counter counter = new Counter();
List exceptions = new ArrayList<>();// When
try (ThreadsCollider threadsCollider =
threadsCollider()
.withAction(counter::increment) // first action to be executed simultaneously
.times(Processors.HALF) // run on half of available processors
.withAction(counter::decrement) // second action to be executed simultaneously
.times(Processors.HALF) // run on half of available processors
.withThreadsExceptionsConsumer(exceptions::add) // save threads exceptions
.build()) {threadsCollider.collide();
}// Then
then(counter.value()).isZero(); // counter should be zero after all threads finish
then(exceptions).isEmpty(); // no exceptions should be thrown during threads execution
}
```> **_NOTE:_** It is recommended to set threads exceptions consumer using `withThreadsExceptionsConsumer()` method to be sure
> that no exceptions were thrown during threads execution.### Deadlocks
- We have two methods that are updating two lists:
```java
void update1(List list1, List list2) {synchronized (list1) {
list1.add(1);
synchronized (list2) {
list2.add(1);
}
}
}
``````java
void update2(List list2, List list1) {synchronized (list2) {
list2.add(1);
synchronized (list1) {
list1.add(1);
}
}
}
```- We want to execute both methods simultaneously to manifest a deadlock:
```java
@RepeatedTest(10)
void Detect_deadlock() {
// Given
List exceptions = new ArrayList<>();// When
try (ThreadsCollider collider =
threadsCollider()
.withAction(() -> update1(list1, list2), "update1") // add action name for better logs readability
.times(Processors.HALF)
.withAction(() -> update2(list2, list1), "update2") // add action name for better logs readability
.times(Processors.HALF)
.withThreadsExceptionsConsumer(exceptions::add) // save threads exceptions
.withAwaitTerminationTimeout(100)
.asMilliseconds()
.build()) {collider.collide();
}// Then
then(exceptions).isEmpty();
}
```- Some tests will fail with following message:
```bash
java.lang.AssertionError:
Expecting empty but was: [pl.amazingcode.threadscollider.UnfinishedThreads:
There are threads that have not completed within the specified timeout: 100 MILLISECONDS
Check if there are any deadlocks and fix them.
If there are no deadlocks, increase timeout.
Deadlocked threads: [
"collider-pool-thread-2 [update1]" daemon prio=5 Id=24 BLOCKED on java.util.ArrayList@6c6cb480 owned by "collider-pool-thread-3 [update2]" Id=25"collider-pool-thread-3 [update2]" daemon prio=5 Id=25 BLOCKED on java.util.ArrayList@3eb738bb owned by "collider-pool-thread-2 [update1]" Id=24
]
```### Detailed examples:
- Single action
- [UseCases_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fsingle%2FUseCases_Scenarios.java)
- [ThreadsCollider_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fsingle%2FThreadsCollider_Scenarios.java)- Multiple actions
- [UseCases_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fmulti%2FUseCases_Scenarios.java)
- [ThreadsCollider_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fmulti%2FThreadsCollider_Scenarios.java)- Deadlocks
- [Deadlock_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fmulti%2FDeadlock_Scenarios.java)## Requirements
- Java 8+
## Dependencies
---
### Maven
```xml
pl.amazingcode
threads-collider
1.0.3
test```
### Gradle
```groovy
testImplementation group: 'pl.amazingcode', name: 'threads-collider', version: "1.0.3"
```