A replacement for Thread#stop() on JDK 20+, using JVMTI.
ThreadKiller.kill(thread) reproduces the same functionality.
It asynchronously throws a ThreadDeath (or a Throwable of your choosing)
into the target thread at its next safepoint; exactly what
Thread#stop() did before JDK 20 removed the native plumbing behind it.
ThreadKiller.kill(someThread); // throws ThreadDeath, like the old stop()
ThreadKiller.kill(someThread, new MyException()); // or throw something customThe removal of Thread#stop() took away the language-level API, but the
underlying JVM capability to forcibly inject an exception into another thread
is still very possible. It's just locked behind JVMTI's StopThread function and
the can_signal_thread capability.
This means that a native JVMTI agent is required, not just a JNI library. Preferably, without JVM args being added. The process:
- On first use,
ThreadKillerextracts its bundled native agent to a temp file. - It attempts to self-attach this agent via the JDK's Attach API. This operation
will fail unless the JVM was launched with
-Djdk.attach.allowAttachSelf=true, which we don't want to require. - If direct self-attach fails, a second, disposable JVM subprocess is spawned with that flag explicitly set. That subprocess performs an external attach against the original process - a different PID attaching to another PID isn't restricted at all.
- That attach loads the native library as a real JVMTI agent inside the original process,
triggering
Agent_OnAttach, where the library requests thecan_signal_threadcapability. - From then on,
ThreadKiller.kill()callsStopThreaddirectly through the capturedjvmtiEnv.
The native side
is written in Rust, with JVMTI bindings generated via bindgen
against real JDK header files.
- JDK 20+ at runtime.
Thread#stop()still functions normally on JDK < 20, so if your runtime is below JDK 20, you should use it rather than attaching an agent to your runtime.ThreadKillerwill not stop JDK < 20 from using it, but it will give a warning.
| OS | Architecture |
|---|---|
| Windows | x86_64 |
| Linux | x86_64, aarch64 |
| macOS | x86_64, aarch64 (Apple Silicon) |
A single jar ships with five native binaries; the correct one is selected
and extracted automatically at runtime based on os.name and os.arch.
Maven
<repositories>
<repository>
<id>zenith-artifactory</id>
<url>https://repo.zenithstudios.dev/artifactory/java-thread-killer</url>
</repository>
</repositories>
<dependency>
<groupId>me.redot</groupId>
<artifactId>killer</artifactId>
<version>1.0.1</version>
</dependency>Gradle
repositories {
maven { url = uri 'https://repo.zenithstudios.dev/artifactory/java-thread-killer' }
}
dependencies {
implementation 'me.redot:killer:1.0.1'
}import me.redot.killer.ThreadKiller;
Thread worker = new Thread(() -> {
while (true) {
// ...
}
});
worker.start();
// Somewhere else, when you arbitrarily need it to stop:
ThreadKiller.kill(worker);- A thread stuck in a tight, allocation-free native loop with no safepoints will not die until it happens to reach one.
- The disposable-JVM attach fallback adds a one-time startup cost
the first time
ThreadKilleris touched in a process. - This is not a forward-compatible API. It relies on JVMTI agent behavior that,
like
Thread#stop()itself, could change or be restricted in a future JDK release.
Personally, I wanted to better-support a library of mine (TweakSuite), so I built this out. Aside from that, I'd argue that not all programs must be "safe". Sometimes it's fun, or makes perfect sense, for a program to be able to arbitrarily kill its subprocesses. Sometimes, Thread#interrupt or other intermittent-exception-throwing mechanics are not available.
In the case of TweakSuite, a user could run System.exit(0); and end the runtime.
Comparatively, I'm not afraid of Thread#stop(). That said, TweakSuite has a safe implementation
for stopping threads, which it calls before ever referencing ThreadKiller.
The idea there, is to never have to reference ThreadKiller or load its agent.
But it is still there if the user needs it.