> it's difficult to change while keeping compatibility
Actually, it is trivial to change. Just embed a ReentrantLock into every object and rewrite all calls to "synchronized"/"Object.wait" to use that lock.
Unfortunately, this would result in a bit of performance regression (increasing per-object memory footprint). To solve that would require turning ReentrantLock into a magical intrinsic, fully integrated with lock bytes in the object header. Which is actually not that hard either — other runtimes like Golang or Android VM solve problems like this on daily basis. Oracle, however…