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
- 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.
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
- 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.
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
- No cache entry found for key:
- Dump is already published
- Already started
- Fail to create classloader for plugin [%s]
- Failed to read SARIF report at '%s': %s
AI-assisted analysis of SonarSource/sonarqube@184c821202 (2026-09-09).
Data as JSON: /api/errors/64f7df2aa5cb71e0.
Report an issue: GitHub.