It was not a fun time. In Java, for example, the concurrency & threading primitives were so low level it was almost impossible for anyone to use them and get it right. The concurrency package introduced in 2004 brought higher level concepts and mostly eliminated the need to risk the footguns present in the thread/runnable/synchronized constructs.

As far back at 1995 people were warning against using threads. See for example John Ousterhout's "Why Threads are a Bad Idea (for most purposes)" <https://blog.acolyer.org/2014/12/09/why-threads-are-a-bad-id...>

> In Java, for example, the concurrency & threading primitives were so low level it was almost impossible for anyone to use them and get it right.

I disagree with this. As long as you had an understanding of critical sections and notify & wait, typical use cases were reasonably straightforward. The issues were largely when you ventured outside of critical sections, or when you didn’t understand the extent of your shared mutable state that needed to be protected by critical sections (which would still be a problem today, for example when you move references to mutable objects between threads — the concurrent package doesn’t really help you there).

The problem with Java pre-1.5 was that the memory model wasn’t very well-defined outside of locks, and that the guarantees that were specified weren’t actually assured by most implementations [0]. That changed with the new memory model in Java 1.5, which also enabled important parts of the new concurrency package.

[0] https://www.cs.tufts.edu/~nr/cs257/archive/bill-pugh/jmm2.pd...

Now I see it written down I realize how lucky I've been to have spent time at the Programming Research Group at Oxford when Tony Hoare was running it and then to have worked for and founded a company with John Ousterhout. And, yeah, when I worked with him he wasn't a fan of threads.