The most difficult requirement here is:
The submitted Runnable
is guaranteed to be executed exactly once.
Using a non-volatile
(Plain) field for transfering the work task from the submitter to the executor would not create a happens-before relationship, but would also not guarantee that the executor sees the task at all or in a finite amount of time. The compiler would be able to optimize away assignments to that field, or during runtime the executor thread might only read the value from its cache instead of from main memory.
So for code using Java 8 or lower, I would say the answer is "No, such an invokeAndWait
method is not possible" (except maybe using native code).
However, Java 9 added the memory mode Opaque. The page "Using JDK 9 Memory Order Modes" by Doug Lea, the author of JEP 193 (which added this functionality), describes this in great detail. Most importantly Opaque mode is weaker than volatile
but provides still the following guarantee:
- Progress. Writes are eventually visible.
[...]
For example in constructions in which the only modification of some variable x is for one thread to write in Opaque (or stronger) mode, X.setOpaque(this, 1)
, any other thread spinning in while(X.getOpaque(this)!=1){}
will eventually terminate.
[...]
Note that this guarantee does NOT hold in Plain mode, in which spin loops may (and usually do) infinitely loop [...]
When designing such an invokeAndWait
method without happens-before relationship you also have to consider that an action before starting a thread happens-before the first action in that thread (JLS §17.4.4). So the worker thread must be started before the action is constructed.
Additionally the "final
field semantics" (JLS §17.15.1) have to be considered. When the caller of invokeAndWait
creates the Runnable
in the form of a lambda expression, then the capturing of variables by that lambda has (to my understanding) implicit final
field semantics.
If so, please include an example implementation, which proves this.
Proving or disproving thread-safety or happens-before relationships using examples is difficult, if not impossible, due to being hardware and timing dependent. However, tools like jcstress can help with this.
Below is a (simplified) potential implementation for an invokeAndWait
without happens-before relationship. Note that I am not completely familiar with the Java Memory Model so there might be errors in the code.
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
class OpaqueExecutor {
// For simplicity assume there will every only be a single work task
// So pending task being replaced by other task is not an issue
private final AtomicReference<Runnable> nextTask = new AtomicReference<>();
public OpaqueExecutor() {
Thread worker = new Thread(() -> {
while (true) {
// Use getOpaque() to no create happens-before relationship
Runnable task = nextTask.getOpaque();
if (task == null) {
// For efficiency indicate to the JVM that this is busy-waiting
Thread.onSpinWait();
} else {
// Clear pending task; memory mode here does not matter because we only want
// to guarantee that this thread does not see task again
nextTask.setPlain(null);
task.run();
}
}
}, "Worker thread");
worker.setDaemon(true);
worker.start();
}
public void invokeLater(Runnable runnable) {
// For simplicity assume that there is no existing pending task which could be
// replaced by this
// Use setOpaque(...) to not create happens-before relationship
nextTask.setOpaque(runnable);
}
private static class Task implements Runnable {
private final AtomicBoolean isFinished = new AtomicBoolean(false);
// Must NOT be final to prevent happens-before relationship from
// final field semantics
private Runnable runnable;
public Task(Runnable runnable) {
this.runnable = runnable;
}
public void run() {
try {
runnable.run();
} finally {
// Use setOpaque(...) to not create happens-before relationship
isFinished.setOpaque(true);
}
}
public void join() {
// Use getOpaque() to no create happens-before relationship
while (!isFinished.getOpaque()) {
// For efficiency indicate to the JVM that this is busy-waiting
Thread.onSpinWait();
}
}
}
public void invokeAndWait(Runnable runnable) {
Task task = new Task(runnable);
invokeLater(task);
task.join();
}
public static void main(String... args) {
// Create executor as first step to not create happens-before relationship
// for Thread.start()
OpaqueExecutor executor = new OpaqueExecutor();
final int expectedValue = 123;
final int expectedNewValue = 456;
class MyTask implements Runnable {
// Must NOT be final to prevent happens-before relationship from
// final field semantics
int value;
public MyTask(int value) {
this.value = value;
}
public void run() {
int valueL = value;
if (valueL == expectedValue) {
System.out.println("Found expected value");
} else {
System.out.println("Unexpected value: " + valueL);
}
value = expectedNewValue;
}
}
MyTask task = new MyTask(expectedValue);
executor.invokeAndWait(task);
int newValue = task.value;
if (newValue == expectedNewValue) {
System.out.println("Found expected new value");
} else {
System.out.println("Unexpected new value: " + newValue);
}
}
}