{"id":22315644,"url":"https://github.com/stawirej/threads-collider","last_synced_at":"2025-06-22T17:08:37.047Z","repository":{"id":200028966,"uuid":"704233802","full_name":"stawirej/threads-collider","owner":"stawirej","description":null,"archived":false,"fork":false,"pushed_at":"2024-04-21T17:52:14.000Z","size":167,"stargazers_count":16,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-22T17:08:31.138Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stawirej.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-12T20:30:58.000Z","updated_at":"2025-04-09T10:27:14.000Z","dependencies_parsed_at":"2023-10-21T13:28:55.327Z","dependency_job_id":"c256830c-a0ba-49d5-9b24-a50ba3ba03ca","html_url":"https://github.com/stawirej/threads-collider","commit_stats":null,"previous_names":["stawirej/threads-collider"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/stawirej/threads-collider","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stawirej%2Fthreads-collider","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stawirej%2Fthreads-collider/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stawirej%2Fthreads-collider/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stawirej%2Fthreads-collider/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stawirej","download_url":"https://codeload.github.com/stawirej/threads-collider/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stawirej%2Fthreads-collider/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261330122,"owners_count":23142482,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-03T22:22:55.357Z","updated_at":"2025-06-22T17:08:32.034Z","avatar_url":"https://github.com/stawirej.png","language":"Java","funding_links":[],"categories":["测试"],"sub_categories":[],"readme":"## About\n\n`Threads Collider` attempts to execute a desired action on multiple threads at the \"exactly\" same moment to increase the\nchances of manifesting issues caused by a race condition or a deadlock.\n\n## Overview\n\n```java\n\n@RepeatedTest(10)\nvoid Thread_safe_adding_to_list() {\n    // Given\n    List\u003cString\u003e list = new ArrayList\u003c\u003e();    // \u003c-- NOT thread safe\n\n    // When\n    try (ThreadsCollider threadsCollider =\n             threadsCollider()\n                 .withAction(() -\u003e list.add(\"bar\"))\n                 .times(Processors.ALL)   // run action on all available processors\n                 .build()) {\n\n        threadsCollider.collide();\n    }\n\n    // Then\n    then(list).hasSize(Processors.ALL).containsOnly(\"bar\");\n}\n```\n\n#### Output\n\n![img.png](png/img.png)\n\n#### Example failure\n\n```bash\njava.lang.AssertionError: \nExpecting ArrayList:\n  [null, null, null, null, null, null, \"bar\", \"bar\"]\nto contain only:\n  [\"bar\"]\nbut the following element(s) were unexpected:\n  [null, null, null, null, null, null]\n```\n\n#### Fixed version\n\n```java\n\n@RepeatedTest(10)\nvoid Thread_safe_adding_to_list() {\n    // Given\n    List\u003cString\u003e list = Collections.synchronizedList(new ArrayList\u003c\u003e());  // \u003c-- thread safe\n\n    // When\n    try (ThreadsCollider threadsCollider =\n             threadsCollider()\n                 .withAction(() -\u003e list.add(\"bar\"))\n                 .times(Processors.ALL)   // run action on all available processors\n                 .build()) {\n\n        threadsCollider.collide();\n    }\n\n    // Then\n    then(list).hasSize(Processors.ALL).containsOnly(\"bar\");\n}\n```\n\n## Usage\n\n### Single action\n\n- You can create `ThreadsCollider` using `ThreadsColliderBuilder`.\n- Use `junit5` `@RepeatedTest` annotation to run test multiple times to increase chance of manifesting concurrency issues.\n\n```java\n\n@RepeatedTest(10)\n    // run test multiple times to increase chance of manifesting concurrency issues\nvoid Thread_safe_adding_to_set() {\n    // Given\n    Set\u003cString\u003e set = Collections.synchronizedSet(new HashSet\u003c\u003e());\n    List\u003cException\u003e exceptions = Collections.synchronizedList(new ArrayList\u003c\u003e());\n\n    // When\n    try (ThreadsCollider threadsCollider =    // use try-with-resources to automatically shutdown threads collider\n             threadsCollider()\n                 .withAction(() -\u003e set.add(\"foo\"))    // action to be executed simultaneously\n                 .times(Processors.ALL)   // run action on all available processors\n                 .withThreadsExceptionsConsumer(exceptions::add)  // save threads exceptions, default consumer do nothing\n                 .withAwaitTerminationTimeout(100)    // threads collider will wait 100 milliseconds for threads to finish, default 60 seconds\n                 .asMilliseconds()\n                 .build()) {  // build threads collider\n\n        threadsCollider.collide();  // code to be executed simultaneously at \"exactly\" same moment\n    }\n\n    // Then\n    then(set).hasSize(1).containsExactly(\"foo\");\n}\n```\n\n### Multiple actions\n\n- We have thread safe `Counter` class:\n\n```java\npublic class Counter {\n\n    private final AtomicInteger counter = new AtomicInteger(0);\n\n    public void increment() {\n        counter.incrementAndGet();\n    }\n\n    public void decrement() {\n        counter.decrementAndGet();\n    }\n\n    public int value() {\n        return counter.get();\n    }\n}\n```\n\n\u003e **_NOTE:_** Change `counter` field to `int` to manifest concurrency issues.\n\n```java\n\n@RepeatedTest(10)\n    // run test multiple times to increase chance of manifesting concurrency issues\nvoid Thread_safe_counter() {\n    // Given\n    Counter counter = new Counter();\n    List\u003cException\u003e exceptions = new ArrayList\u003c\u003e();\n\n    // When\n    try (ThreadsCollider threadsCollider =\n             threadsCollider()\n                 .withAction(counter::increment)    // first  action to be executed simultaneously\n                 .times(Processors.HALF)            // run on half of available processors\n                 .withAction(counter::decrement)    // second action to be executed simultaneously\n                 .times(Processors.HALF)            // run on half of available processors\n                 .withThreadsExceptionsConsumer(exceptions::add)    // save threads exceptions\n                 .build()) {\n\n        threadsCollider.collide();\n    }\n\n    // Then\n    then(counter.value()).isZero();   // counter should be zero after all threads finish\n    then(exceptions).isEmpty();       // no exceptions should be thrown during threads execution\n}\n```\n\n\u003e **_NOTE:_**  It is recommended to set threads exceptions consumer using `withThreadsExceptionsConsumer()` method to be sure\n\u003e that no exceptions were thrown during threads execution.\n\n### Deadlocks\n\n- We have two methods that are updating two lists:\n\n```java\nvoid update1(List\u003cInteger\u003e list1, List\u003cInteger\u003e list2) {\n\n    synchronized (list1) {\n        list1.add(1);\n        synchronized (list2) {\n            list2.add(1);\n        }\n    }\n}\n```\n\n```java\nvoid update2(List\u003cInteger\u003e list2, List\u003cInteger\u003e list1) {\n\n    synchronized (list2) {\n        list2.add(1);\n        synchronized (list1) {\n            list1.add(1);\n        }\n    }\n}\n```\n\n- We want to execute both methods simultaneously to manifest a deadlock:\n\n```java\n\n@RepeatedTest(10)\nvoid Detect_deadlock() {\n    // Given\n    List\u003cException\u003e exceptions = new ArrayList\u003c\u003e();\n\n    // When\n    try (ThreadsCollider collider =\n             threadsCollider()\n                 .withAction(() -\u003e update1(list1, list2), \"update1\") // add action name for better logs readability\n                 .times(Processors.HALF)\n                 .withAction(() -\u003e update2(list2, list1), \"update2\") // add action name for better logs readability\n                 .times(Processors.HALF)\n                 .withThreadsExceptionsConsumer(exceptions::add)     // save threads exceptions\n                 .withAwaitTerminationTimeout(100)\n                 .asMilliseconds()\n                 .build()) {\n\n        collider.collide();\n    }\n\n    // Then\n    then(exceptions).isEmpty();\n}\n```\n\n- Some tests will fail with following message:\n\n```bash\njava.lang.AssertionError: \nExpecting empty but was: [pl.amazingcode.threadscollider.UnfinishedThreads: \nThere are threads that have not completed within the specified timeout: 100 MILLISECONDS\nCheck if there are any deadlocks and fix them. \nIf there are no deadlocks, increase timeout.\nDeadlocked threads: [\n\"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\n\n\"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\n\n]\n```\n\n### Detailed examples:\n\n- Single action\n    - [UseCases_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fsingle%2FUseCases_Scenarios.java)\n    - [ThreadsCollider_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fsingle%2FThreadsCollider_Scenarios.java)\n\n- Multiple actions\n    - [UseCases_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fmulti%2FUseCases_Scenarios.java)\n    - [ThreadsCollider_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fmulti%2FThreadsCollider_Scenarios.java)\n\n- Deadlocks\n    - [Deadlock_Scenarios.java](src%2Ftest%2Fjava%2Fpl%2Famazingcode%2Fthreadscollider%2Fmulti%2FDeadlock_Scenarios.java)\n\n## Requirements\n\n- Java 8+\n\n## Dependencies\n\n---\n\n### Maven\n\n```xml \n\n\u003cdependency\u003e\n    \u003cgroupId\u003epl.amazingcode\u003c/groupId\u003e\n    \u003cartifactId\u003ethreads-collider\u003c/artifactId\u003e\n    \u003cversion\u003e1.0.3\u003c/version\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n```\n\n### Gradle\n\n```groovy\ntestImplementation group: 'pl.amazingcode', name: 'threads-collider', version: \"1.0.3\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstawirej%2Fthreads-collider","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstawirej%2Fthreads-collider","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstawirej%2Fthreads-collider/lists"}