Concurrency in Java
Every chapter so far assumed one thread of execution. This one drops that assumption — and revisits a lesson c3-3 already taught once, in a language that enforces it no more strictly than C did.
Creating Threads — Thread & Runnable
Extending Thread works, but spends the one extends slot java1-5 established every class only gets. Implementing Runnable instead — itself a functional interface per java2-3 — is the preferred approach precisely because it leaves that slot free for something else.
A Real Race Condition
count++ is really three steps — read, increment, write — and two threads can interleave those steps, each reading the same stale value before either writes back. This is the exact same bug class c3-3 demonstrated with pthreads: a genuine, reproducible race condition, not a hypothetical one.
synchronized — Java's Built-in Mutex
Every Java object carries a built-in ("intrinsic") lock. A synchronized method or block acquires the lock on entry and releases it on exit — even if an exception is thrown — with no separate lock object to create, no explicit unlock call, and no risk of forgetting to release it. This is a real, direct point of comparison: c3-3's pthread_mutex_t is a separate object requiring explicit lock/unlock calls, and even cpp3-4's RAII-based std::lock_guard still requires a distinct std::mutex object to guard. Java bakes the lock/unlock pairing directly into a keyword, syntactically impossible to mismatch.
The java.util.concurrent Package
ExecutorService manages a pool of reusable threads instead of hand-creating and tracking raw Thread objects. AtomicInteger and its relatives offer lock-free atomic operations for simple cases like counters — genuinely safe under concurrent access without ever calling synchronized at all.
The Mutex-to-Data Disconnect, Again
c3-3's own central lesson resurfaces here unchanged: synchronized is pure convention. Nothing stops a different method from touching count directly without ever acquiring the lock — the compiler enforces none of it, the same "zero compiler enforcement" pattern C and C++ both share. Rust's Mutex<T> — previewed in c3-3/cpp3-4 — takes a structurally different approach: it actually owns the data it protects, so the compiler makes it impossible to touch that data at all without locking first. Java, like C and C++, offers no such guarantee — only the discipline to use synchronized consistently everywhere the shared state is touched.
| Approach | C pthreads (c3-3) | C++ (cpp3-4) | Java | Rust |
|---|---|---|---|---|
| Lock mechanism | pthread_mutex_t, manual lock/unlock | std::mutex + RAII lock_guard | synchronized — built into the language | Mutex<T> |
| Auto-release on exception | no | yes — RAII | yes — built-in | yes |
| Compiler enforces locking before access | no | no | no | yes — data is owned by the Mutex |
ExecutorService and the Atomic* classes cover the overwhelming majority of real concurrency needs more safely and with less boilerplate than manual thread and lock management — treat raw Thread/synchronized as the low-level building blocks these higher-level tools are built from.
synchronized does nothing to protect a field that another, unsynchronized method also reads or writes — the lock only excludes other synchronized access to that same lock, not all access to the underlying data. Every code path touching shared state must synchronize consistently, or the protection is illusory.
Coding Challenges
Write a Counter class with an unsynchronized increment() method, start two threads each calling it 100,000 times, join both threads, and print the final count showing it's less than 200,000.
📄 View solutionFix Challenge 1's Counter by marking increment() synchronized, run the same two-thread test, and show the final count is now reliably exactly 200,000.
📄 View solutionWrite a class with a synchronized increment() method AND a separate, unsynchronized resetIfNegative() method that also reads and writes the same field. Explain in a comment why marking only increment() synchronized does not fully protect the field.
📄 View solutionChapter 5 Quick Reference
- Prefer implementing Runnable over extending Thread — it leaves java1-5's single extends slot free
- count++ is a read-modify-write sequence, not atomic — the same race-condition class c3-3 demonstrated with pthreads
- synchronized acquires/releases every object's built-in intrinsic lock automatically, even across an exception — no separate lock object needed, unlike pthread_mutex_t or std::mutex
- java.util.concurrent's ExecutorService and Atomic* classes cover most real needs above raw Thread/synchronized
- synchronized is pure convention — the compiler never enforces it, the same zero-enforcement pattern as C/C++; only Rust's Mutex<T> makes unlocked access structurally impossible
- Next chapter: the Java Memory Model and garbage collection — closing the loop rust1-1 opened