https://github.com/forax/how_to_stop_a_thread
Different ways in Java to stop a thread
https://github.com/forax/how_to_stop_a_thread
concurrency java performance thread volatile
Last synced: 6 months ago
JSON representation
Different ways in Java to stop a thread
- Host: GitHub
- URL: https://github.com/forax/how_to_stop_a_thread
- Owner: forax
- License: mit
- Created: 2024-06-14T08:17:58.000Z (about 2 years ago)
- Default Branch: master
- Last Pushed: 2024-07-31T14:35:46.000Z (almost 2 years ago)
- Last Synced: 2024-07-31T18:02:05.372Z (almost 2 years ago)
- Topics: concurrency, java, performance, thread, volatile
- Language: Java
- Homepage:
- Size: 19.5 KB
- Stars: 3
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# how_to_stop_a_thread
A study of the different ways to stop a thread in Java.
For each way to stop a thread, we bench the cost of reading the value `stop` value once
or multiple times in a loop of an array of 100 000 elements.
The following results are on my MacBook Air M2:
```
// ThreadStopBench.stop_synchronized avgt 5 5.055 ± 0.023 ns/op
// ThreadStopBench.stop_reentrant_lock avgt 5 8.406 ± 0.035 ns/op
// ThreadStopBench.stop_interrupt avgt 5 0.490 ± 0.002 ns/op
// ThreadStopBench.stop_volatile avgt 5 0.496 ± 0.004 ns/op
// ThreadStopBench.stop_opaque avgt 5 0.409 ± 0.004 ns/op
// ThreadStopBench.stop_callsite avgt 5 0.306 ± 0.002 ns/op
// ThreadStopBench.stop_arena avgt 5 0.613 ± 0.004 ns/op
```
```
// ThreadStopLoopBench.stop_synchronized avgt 5 510.287 ± 24.235 us/op
// ThreadStopLoopBench.stop_reentrant_lock avgt 5 833.998 ± 1.591 us/op
// ThreadStopLoopBench.stop_interrupt avgt 5 51.382 ± 0.423 us/op
// ThreadStopLoopBench.stop_volatile avgt 5 51.867 ± 0.303 us/op
// ThreadStopLoopBench.stop_opaque avgt 5 30.584 ± 0.075 us/op
// ThreadStopLoopBench.stop_callsite avgt 5 30.604 ± 0.071 us/op
// ThreadStopLoopBench.stop_arena avgt 5 30.592 ± 0.091 us/op
```
The following results are on an x86_64 Intel(R) Xeon(R) Gold 5218 CPU @ 2.30GHz
```
Benchmark Mode Cnt Score Error Units
ThreadStopBench.stop_synchronized avgt 5 16.696 ± 0.745 ns/op
ThreadStopBench.stop_reentrant_lock avgt 5 13.898 ± 0.085 ns/op
ThreadStopBench.stop_interrupt avgt 5 0.524 ± 0.006 ns/op
ThreadStopBench.stop_volatile avgt 5 0.522 ± 0.023 ns/op
ThreadStopBench.stop_opaque avgt 5 0.522 ± 0.023 ns/op
ThreadStopBench.stop_callsite avgt 5 0.393 ± 0.001 ns/op
ThreadStopBench.stop_arena avgt 5 0.785 ± 0.021 ns/op
```
```
ThreadStopLoopBench.stop_synchronized avgt 5 1678.665 ± 38.996 us/op
ThreadStopLoopBench.stop_reentrant_lock avgt 5 1409.330 ± 47.300 us/op
ThreadStopLoopBench.stop_interrupt avgt 5 65.532 ± 0.358 us/op
ThreadStopLoopBench.stop_volatile avgt 5 65.549 ± 0.832 us/op
ThreadStopLoopBench.stop_opaque avgt 5 27.700 ± 1.323 us/op
ThreadStopLoopBench.stop_callsite avgt 5 27.353 ± 0.050 us/op
ThreadStopLoopBench.stop_arena avgt 5 27.844 ± 0.344 us/op
```
1. Using synchronized
Here, we are using the keyword synchronized to protect the access to the field `stop`.
```java
boolean stop;
final Object lock = new Object();
void loop() {
while(true) {
synchronized (lock) {
if (stop) {
break;
}
}
// ...
}
System.out.println("end !");
}
void main() throws InterruptedException {
var thread = new Thread(this::loop);
thread.start();
Thread.sleep(1_000);
synchronized (lock) {
stop = true;
}
}
```
2. Using ReentrantLock
Same code but using a reentrant lock instead of a synchronized block.
```java
boolean stop;
final ReentrantLock lock = new ReentrantLock();
void loop() {
while(true) {
lock.lock();
try {
if (stop) {
break;
}
} finally {
lock.unlock();
}
// ...
}
System.out.println("end !");
}
void main() throws InterruptedException {
var thread = new Thread(this::loop);
thread.start();
Thread.sleep(1_000);
lock.lock();
try {
stop = true;
} finally {
lock.unlock();
}
}
```
3. Using interrupted
Java has its own mechanism to interrupt a thread.
```java
void loop() {
while(true) {
if (Thread.interrupted()) {
break;
}
// ...
}
System.out.println("end !");
}
void main() throws InterruptedException {
var thread = new Thread(this::loop);
thread.start();
Thread.sleep(1_000);
thread.interrupt();
}
```
4. Using volatile
Thread.interrupted()/interrupt are using internally a volatile field.
```java
volatile boolean stop;
void loop() {
while(true) {
if (stop) {
break;
}
// ...
}
System.out.println("end !");
}
void main() throws InterruptedException {
var thread = new Thread(this::loop);
thread.start();
Thread.sleep(1_000);
stop = true;
}
```
5. Using opaque (VarHandle)
Instead of using the volatile semantics, we can use the `opaque` semantics.
This is usually faster that using the keyword `volatile` but sadly, this is usually
not the semantics we want, `opaque` does not guarantee that the preview writes will be seen
by the thread that reads the values when stop becomes true
```java
static final VarHandle STOP = createVH();
private static VarHandle createVH() {
var lookup = MethodHandles.lookup();
try {
return lookup.findVarHandle(lookup.lookupClass(), "stop", boolean.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
boolean stop;
void loop() {
while(true) {
var stop = (boolean) STOP.getOpaque(this);
if (stop) {
break;
}
// ...
}
System.out.println("end !");
}
void main() throws InterruptedException {
var thread = new Thread(this::loop);
thread.start();
Thread.sleep(1_000);
STOP.setOpaque(this, true);
}
```
6. using a MutableCallSite
We can cheat and say that because there only one thread we can use a global state
and then uses a MutableCallSite to first always return `false` and then always return `true`.
Internally the VM will first generate a code that skip the branch of the `if` because `code`
is always `true`and then when de-optimize the code when the code is changed to return `true`.
```java
static final class Stop extends MutableCallSite {
public Stop() {
super(MethodType.methodType(boolean.class));
setTarget(MethodHandles.constant(boolean.class, false));
}
}
static final Stop STOP = new Stop();
static final MethodHandle STOP_MH = STOP.dynamicInvoker();
void loop() {
while(true) {
boolean stop;
try {
stop = (boolean) STOP_MH.invokeExact();
} catch (RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new AssertionError(e);
}
if (stop) {
break;
}
// ...
}
System.out.println("end !");
}
void main() throws InterruptedException {
var thread = new Thread(this::loop);
thread.start();
Thread.sleep(1_000);
STOP.setTarget(MethodHandles.constant(boolean.class, true));
MutableCallSite.syncAll(new MutableCallSite[] { STOP });
}
```
7. Using foreign memory Arena
When the `Arena` created using `ofAshared()` is closed, it forces all the other threads to go to a GC safepoint,
so the cost of calling `scope.isAlive()` is a simple read.
```java
final Arena arena = Arena.ofShared();
final MemorySegment.Scope scope = arena.scope();
void loop() {
while(true) {
if (!scope.isAlive()) {
break;
}
// ...
}
System.out.println("end !");
}
void main() throws InterruptedException {
var thread = new Thread(this::loop);
thread.start();
Thread.sleep(1_000);
arena.close();
}
```