SonarSource/sonarqube · error · IllegalThreadStateException

Virtual thread already started

Error message

Virtual thread already started

What it means

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.

Source

Thrown at server/sonar-process/src/main/java/org/sonar/process/VirtualThreadTask.java:40

import com.google.common.annotations.VisibleForTesting;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Base class for tasks that run on Java 21 virtual threads.
 * Provides common functionality for managing virtual thread lifecycle.
 */
public abstract class VirtualThreadTask implements Runnable {
  private final AtomicReference<Thread> virtualThread = new AtomicReference<>();

  protected void startVirtualThread(String threadName) {
    Thread newThread = Thread.ofVirtual()
      .name(threadName)
      .start(this);

    if (!virtualThread.compareAndSet(null, newThread)) {
      // Another thread already started, interrupt the thread we just created
      newThread.interrupt();
      throw new IllegalThreadStateException("Virtual thread already started");
    }
  }

  public void interrupt() {
    Thread thread = virtualThread.get();
    if (thread != null) {
      thread.interrupt();
    }
  }

  @VisibleForTesting
  public boolean isAlive() {
    Thread thread = virtualThread.get();
    return thread != null && thread.isAlive();
  }

  @VisibleForTesting
  public String getThreadName() {

View on GitHub (pinned to 184c821202)

Solutions

  1. Guard the call site with an isStarted()/state check before calling start() again.
  2. Treat the second start as a no-op: catch IllegalThreadStateException and ignore if idempotent restart is not needed.
  3. If a restart is intended, stop/interrupt and await termination of the current virtual thread before starting a new one.

Example fix

// before
task.start();
task.start(); // throws
// after
if (!task.isStarted()) {
  task.start();
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (task.isStarted()) {
  return; // already running, skip duplicate start
}

Try / catch

try {
  task.start();
} catch (IllegalThreadStateException e) {
  logger.debug("Task already started; ignoring duplicate start", e);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of SonarSource/sonarqube@184c821202 (2026-09-09). Data as JSON: /api/errors/64f7df2aa5cb71e0. Report an issue: GitHub.