2

Lets say I have a relatively simple object with two properties :

@Data public class MyObject { public Integer a; public Integer b; } 

can I safely mutate a in some thread and b in some other thread safely ? for example, would this code be safe from race conditions ?

public MyObject compute() { MyObject newObj = new MyObject(); List<Runnable> tasks = new ArrayList<>(); Runnable computeATask = () -> { Integer a = computeA(); newObj.setA(a); }; Runnable computeBTask = () -> { Integer b = computeB(); newObj.setB(b); }; tasks.add(computeATask); tasks.add(computeBTask); tasks.stream().parallel().forEach(Runnable::run); return newObj; } 
2
  • 1
    @Kayaman depends on the context. In the example, a parallel stream is used, which makes the decision to use multiple threads (or not) and guarantees correct visibility of the results when forEach returned. And there’s no other access to these variable prior to the completion. Commented Mar 2, 2022 at 9:27
  • @Holger ah I shouldn't have spoken so hastily. So visibility is guaranteed by the parallel forEach() after the operation, but e.g. updating a thread-unsafe collection isn't safe due to the concurrent operation. I very much appreciate your continuing insight on concurrency and other issues on SO. Commented Mar 2, 2022 at 10:43

1 Answer 1

5

This is specified in JLS, §17.6. Word Tearing:

One consideration for implementations of the Java Virtual Machine is that every field and array element is considered distinct; updates to one field or element must not interact with reads or updates of any other field or element.

So the fact that a might written by a different thread than b in your example, does not create any data race.

But it still requires a thread safe mechanism to read the result. In your example, it’s the parallel stream which guarantees that the initiating thread can safely read the two variables after forEach returned.

You example can be simplified to

public MyObject compute() { MyObject newObj = new MyObject(); Stream.<Runnable>of(() -> newObj.setA(computeA()), () -> newObj.setB(computeB())) .parallel().forEach(Runnable::run); return newObj; } 

But the recommended pattern would be to execute the calculation first, followed by constructing the object, which can be designed as immutable object then.

public class MyObject { public final Integer a, b; public MyObject(Integer a, Integer b) { this.a = a; this.b = b; } } 
public MyObject compute() { return CompletableFuture.supplyAsync(() -> computeA()) .thenCombine(CompletableFuture.supplyAsync(() -> computeB()), MyObject::new) .join(); } 

This way, you can be sure that any thread seeing the MyObject will see consistent values for the fields, regardless of what happens in the remaining application.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.