Java Threads & Concurrency API: Part 2
Once two threads share data, correctness stops being obvious — this is the toolkit for making it obvious again.

Part I was about running work on threads. Part II is about the trouble that starts the instant two threads touch the same data — and the machinery Java gives you to keep it under control.
Why shared state is where it gets hard
A single thread is predictable: read the code top to bottom and you know what happens. Add a second thread sharing a variable and that certainty evaporates. The classic example is two threads incrementing the same counter. Increment looks atomic but isn't — it's read, add, write — and if the two interleave, one update silently vanishes. That's a race condition, and its defining nastiness is that it's intermittent: the code passes every test on your laptop and corrupts data once a week in production.
The blunt tool, and the sharper ones
The oldest fix is the synchronized keyword. Mark a block or method synchronized and Java lets only one thread run it at a time by acquiring a lock on an object; everyone else waits their turn. It's simple, it works, and it's a hammer. Lock too broadly and you serialise the program, throwing away the concurrency you wanted; take two locks in different orders across threads and you can deadlock, each waiting forever on what the other holds.
When the hammer costs too much, Java gives you finer tools. ReentrantLock is synchronized with options — try to acquire without blocking forever, or set a fairness policy. ReadWriteLock notices that reads don't conflict with each other, so it admits many readers at once and demands exclusivity only for writes. And atomic classes like AtomicInteger solve the counter problem head-on, wrapping a value with operations that are atomic in hardware, no lock required. One more rule worth burning in: if you catch yourself synchronising by hand around a plain HashMap, stop — ConcurrentHashMap already exists, locks only parts of itself so many threads proceed at once, and will beat your hand-rolled version on both speed and correctness.
Executors: stop managing threads by hand
Above the locking primitives sits the machinery for organising the work itself, and this is where most real code should live. You rarely want to create threads directly — Part I explained why they're expensive. Instead you hand tasks to an ExecutorService, which owns a pool of threads and runs your tasks on them: a fixed pool of N, a cached pool that grows and shrinks on demand, a scheduled one for delayed or repeating work.
The piece people skip is the lifecycle. An executor is active while it accepts and runs tasks. Call shutdown() and it stops accepting new work but finishes what's already queued — it's shutting down, not dead. Only once the running tasks drain is it truly terminated. Skipping that orderly shutdown is a classic leak: the JVM won't exit because a pool of live threads is still sitting there, waiting for work that will never come. Submit your tasks, shut the pool down deliberately, and confirm it terminated.
CompletableFuture: composing the async work
ExecutorService runs tasks; CompletableFuture lets you describe how they fit together. Instead of nesting callbacks by hand, you write a chain — do this, then when it finishes do that, and handle any error along the way — and each stage can run on a pool of your choosing. It's how several concurrent operations become one readable pipeline instead of a tangle of nested handlers.
None of this makes concurrency easy; it makes it manageable. The through-line from Part I holds: the hard part was never starting threads, it's what happens when they share. Reach for the library before the keyword, keep your shared state small, and shut your executors down on the way out.






