I imagine Java's approach to concurrency and parallelism might be quite different if it were designed today.
Probably not, actually. Project Loom's initial goal was to rethink concurrency on the JVM from scratch. What they came up with was:
* Make threads really, really cheap
* Make thread locals work better (as scoped locals)
* Add a few Executor utilities to help you control sub-tasks better (structured concurrency)
It turns out that Java concurrency is pretty damn good already. It provides all the different paradigms you might want to explore, is efficient and well specified. Meanwhile they realised that many of the alternative approaches to concurrency are in reality trying to work around the high cost of kernel threads. When you make threads really cheap, a lot of the motivation for other approaches falls away and the existing set of tools in the JDK come to the fore.
Your characterization of Loom is, I think, pretty accurate.
There are, however, a few things in Java's early concurrency support that make various things harder, including Loom, and we're having to put some extra effort into grappling with them.
Probably the most obvious is the fact that the language and VM requires every object to have a monitor lock that can be synchronized and waited/notified. In 1996 this was viewed as "Ooooh, sophisticated, building locking and concurrency support into the platform!" In recent years this has started to get in the way. Really only a very few objects are used as locks, but the _potential_ for every object to be locked is paid by the JVM.
It also intrudes on Project Valhalla, which is trying to define "identity-less" inline types (formerly, "value types"). Ideally, we'd want all conventional objects and inline objects to be descendants of java.lang.Object. But Object has the locking APIs defined on it, and locking is intertwined with object identity. So, does Object have identity or not? There are some solutions, but they're kind of weird and special-cased.
Another issue is that the locks defined by the language/VM ("synchronized") are implemented differently from locks implemented by the library (in java.util.concurrent.locks). Loom supports virtual threads taking library-based locks, in that when a virtual thread blocks on a lock it will be dismounted from the real thread. This can't be done with language/VM locks, so there's an effort underway to migrate the those locks to delegate to library code for their implementation. This isn't an insurmountable problem, but it's yet more work to be done, and it's a consequence of some of the original designs of Java 1.0's concurrency model.
That's true but if Java had been designed with a "synchronizable" keyword applied to classes, I wouldn't consider that a radically different language. The prevalence of unnecessarily lockable things is unfortunate from a JVM implementors perspective, slightly convenient from a user's perspective, but ultimately not a defining feature of the language or platform even if it may have seemed important in 1995.
When I think about Java concurrency today I tend to think of java.util.concurrent or the JMM. Perhaps that's odd.
Probably not, actually. Project Loom's initial goal was to rethink concurrency on the JVM from scratch. What they came up with was:
* Make threads really, really cheap
* Make thread locals work better (as scoped locals)
* Add a few Executor utilities to help you control sub-tasks better (structured concurrency)
It turns out that Java concurrency is pretty damn good already. It provides all the different paradigms you might want to explore, is efficient and well specified. Meanwhile they realised that many of the alternative approaches to concurrency are in reality trying to work around the high cost of kernel threads. When you make threads really cheap, a lot of the motivation for other approaches falls away and the existing set of tools in the JDK come to the fore.