Well, at the most basic level, every single field of an object or array[1] can be accessed from multiple threads without data race or tearing. For example, this program is sound:
class Example {
Example ref;
public static void main(String[] args) {
Example parent = new Example();
Example child1 = new Example();
Example child2 = new Example();
new Thread(() -> {
while (true) {
parent.ref = child1;
}
}).start();
while (true) {
parent.ref = child2;
}
}
}
To write fully equivalent lock-free code in Rust, you’d need to bring in arc-swap
or a similar data structure which allows the ref
field to be written atomically (and manages the reference counts of the Arc
s you’d need to use for child1
and child2
). In Java, this applies implicitly to every field regardless of the type of the field.
This means that in Java, locks are used for correctness, not soundness — the consequences of forgetting to use a lock are that data changes in a way that you didn’t want but is still consistent with the rules of the language; there is no undefined behavior. So if you want arc-swap
style “read often, write rarely and with atomic replacement”, in Java that’s just an ordinary field.
There’s a lot more that can be said about the Java memory model and what kinds of lock-free data structures it enables, but I am not an expert on that and cannot write up a neat example. Back when I was writing concurrent Java for profit, we were lucky to have a person on our team who did understand it and could tell us exactly what we could and couldn’t get away with — but without that kind of wizardry a lot of Java programs still incidentally take advantage of being able to not add explicit locks around things which Rust would require. Thus, rewriting in Rust may often require taking different approaches to data organization — even in single-threaded code since Rust’s model applies to reentrant mutation (A calls B calls A) just as much as multi-threaded mutation.
Of course, all this is specific to Java and other languages have other approaches. But many “OOP and GC” languages have some solution such that modifying objects from multiple threads isn’t easily-hit instant UB — for example, Python has the global interpreter lock, so every such read or write is in fact sequential and can’t be a race.[2]