javamultithreadingthread-safetycompletable-futurejava-memory-model

Does CompletableFuture ensure field update visibility after join() in Java?


I’m working with CompletableFuture in Java and want to understand how field updates made inside a CompletableFuture task are visible to the main thread after calling join(). Specifically, if I pass an object (x) to a CompletableFuture task, and the task updates a field inside that object (e.g., x.y), will the main thread always see the updated value after join()?

class Y {
    private int value;
    private List<String> names;

    public Y() {
        this.names = new ArrayList<>();
    }

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void addName(String name) {
        this.names.add(name);
    }

    public List<String> getNames() {
        return names;
    }
}

class X {
    private Y y;

    public void setY(Y y) {
        this.y = y;
    }

    public Y getY() {
        return y;
    }
}

public class Main {
    public static void main(String[] args) {
        X x = new X();

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            Y newY = new Y();
            newY.setValue(42);
            newY.addName("Alice");
            newY.addName("Bob");
            x.setY(newY); // Update the y field of x
        });

        future.join(); // Wait for the CompletableFuture to complete

        // Will the main thread always see the updated y field?
        System.out.println("Value: " + x.getY().getValue()); // Should print 42
        System.out.println("Names: " + x.getY().getNames()); // Should print [Alice, Bob]
    }
}

Are the updated fields (value and names) of x.y always guaranteed to be visible to the main thread after join()? Or I have to use AtomicInteger, volatile or Collections.synchronizedList to achive that?

In my humble opinion the join() method ensures that all actions performed by the CompletableFuture task are completed before the main thread proceeds. This means that any updates to fields or objects made by the CompletableFuture task will be visible to the main thread after join() returns. But I'm not sure about memory visibility of these fields. They might not be visible always.

Note: This example is a simplified reflection of a more complex scenario. In practice, I understand that I could directly set the x object after CompletableFuture completes (e.g., x = future.join()). However, my actual use case involves nested objects and shared state across multiple threads, which is why I’m focusing on field visibility and thread safety.


Solution

  • Docs of java.util.concurrent define happen-before relation:

    The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation.

    So if actions, that are made by the CompletableFuture task, happen-before return from join, then the field (x.y) updates are visible to your main thread.

    I have not found in the doc that CompletableFuture.join shares the same guarantees with Future.get.
    But there is a comment in https://bugs.openjdk.org/browse/JDK-8292365 with suggestion to document CompletableFuture guarantees:

    Actions taken by the asynchronous computation represented by a {@code Future} happen-before actions subsequent to the retrieval of the result via {@code Future.get()}, {@code Future.resultNow()} , or {@code Future.exceptionNow()}, in another thread.
    Similar for the computation represented by a {@code CompletabeFuture} and retrieval of the result via {@code CompletabeFuture.getNow} or {@code CompletabeFuture.join}.

    This inclines me to think that CompletableFuture.join shares the same guarantees with Future.get but it is not documented yet.
    And it is stated in the doc that actions made by Future.get happen-before actions after it is called:

    Actions taken by the asynchronous computation represented by a Future happen-before actions subsequent to the retrieval of the result via Future.get() in another thread.