{"record":{"id":"64f7df2aa5cb71e0","repo":"SonarSource/sonarqube","slug":"virtual-thread-already-started","errorCode":null,"errorMessage":"Virtual thread already started","messagePattern":"Virtual thread already started","errorType":"exception","errorClass":"IllegalThreadStateException","httpStatus":null,"severity":"error","filePath":"server/sonar-process/src/main/java/org/sonar/process/VirtualThreadTask.java","lineNumber":40,"sourceCode":"import com.google.common.annotations.VisibleForTesting;\nimport java.util.concurrent.atomic.AtomicReference;\n\n/**\n * Base class for tasks that run on Java 21 virtual threads.\n * Provides common functionality for managing virtual thread lifecycle.\n */\npublic abstract class VirtualThreadTask implements Runnable {\n  private final AtomicReference<Thread> virtualThread = new AtomicReference<>();\n\n  protected void startVirtualThread(String threadName) {\n    Thread newThread = Thread.ofVirtual()\n      .name(threadName)\n      .start(this);\n\n    if (!virtualThread.compareAndSet(null, newThread)) {\n      // Another thread already started, interrupt the thread we just created\n      newThread.interrupt();\n      throw new IllegalThreadStateException(\"Virtual thread already started\");\n    }\n  }\n\n  public void interrupt() {\n    Thread thread = virtualThread.get();\n    if (thread != null) {\n      thread.interrupt();\n    }\n  }\n\n  @VisibleForTesting\n  public boolean isAlive() {\n    Thread thread = virtualThread.get();\n    return thread != null && thread.isAlive();\n  }\n\n  @VisibleForTesting\n  public String getThreadName() {","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/SonarSource/sonarqube/blob/184c821202192afc1c599fc912d0889b69fffa53/server/sonar-process/src/main/java/org/sonar/process/VirtualThreadTask.java#L22-L58","documentation":"VirtualThreadTask.startVirtualThread starts the task on a new virtual thread and uses compareAndSet on the virtualThread reference to guarantee single execution. If another thread concurrently started the task, the newly created thread is interrupted and an IllegalThreadStateException is thrown.","triggerScenarios":"Calling start() (or startVirtualThread) twice on the same VirtualThreadTask instance, including from two threads racing to start it, before the previous virtual thread reference was cleared.","commonSituations":"Supervisor/monitor code retrying a start after a hiccup; lifecycle callbacks firing start both from an initializer and a scheduler; tests calling start in setup and again in the test body.","solutions":["Guard the call site with an isStarted()/state check before calling start() again.","Treat the second start as a no-op: catch IllegalThreadStateException and ignore if idempotent restart is not needed.","If a restart is intended, stop/interrupt and await termination of the current virtual thread before starting a new one."],"exampleFix":"// before\ntask.start();\ntask.start(); // throws\n// after\nif (!task.isStarted()) {\n  task.start();\n}","handlingStrategy":"try-catch","validationCode":"if (task.isStarted()) {\n  return; // already running, skip duplicate start\n}","typeGuard":null,"tryCatchPattern":"try {\n  task.start();\n} catch (IllegalThreadStateException e) {\n  logger.debug(\"Task already started; ignoring duplicate start\", e);\n}","preventionTips":["Centralize start/stop lifecycle in one supervisor component per task.","Make start idempotent at the call site with a started flag.","In tests, reset or recreate the task between test methods."],"tags":["threading","illegal-state","lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"184c821202192afc1c599fc912d0889b69fffa53","analyzedAt":"2026-09-09T12:23:51.573Z","contentChangedAt":"2026-09-09T12:23:51.573Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}