It’s that “threads might complicate things”.
The compiler will try to optimize your code, including by changing the order of steps and short-cutting expressions. The compiler is authorized to make such changes as long as the result is the same on the same thread. But, there is no guarantee that other threads see things happening like they appear in the code. This is also known as the Java Memory Model.
For example, if you set a flag, then change something, then unset the flag, with the expectation that other threads can see that the flag has been set and therefore be alerted that changes are happening, the compiler might conclude that the setting and unsetting of the flag has no effect within the same thread and instead leave the flag unset altogether.
There is one guarantee, though: if you create a new object and the constructor returns the reference to it, then any thread using that reference will see all the changes that happened in the constructor. That means if an object is not further changed after construction - i.e. it is immutable - all threads will see the same object.
The classical method to communicate between threads is to use locks. If one thread locks an object, changes it, and then unlocks it, then any other thread that locks that object will see all the changes.
The problem is that locks don’t scale.
If you have a large data structure and use a lock on it, there may be many threads that want to change it, and then only one thread can change it at a time and the other threads have to wait for the lock.
So will try to have many locks for different parts of the data. But then you will have threads that need more than one lock to to accomplish a task. Then, what happens when a thread obtains one lock but needs a second one that is not available? If the thread decides to hold on to the first lock while waiting for the second, this will block any other threads that also need the first lock. In particular, if a second thread has acquired the second lock and is waiting for the first lock, you have a deadlock.
You may try to impose a policy where no thread is allowed to hold a lock without making immediate progress. So, a thread may acquire one lock, then try to get a second lock, and if the second lock is not available, the thread has to release the first lock and try again later. This can prevent deadlock, but may cause the system to spend most of its time acquiring and releasing locks.
A more scalable approach is to have threads communicate by sending each other immutable objects. Actually, there may still be locks, but only in very limited cases, such as briefly locking a collection to add or remove an object from it.