Concurrency in Java

Course 2 · Ch 5
Concurrency in Java
A third data point alongside c3-3's pthreads and cpp3-4's modern C++ threading

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

class MyThread extends Thread { @Override public void run() { System.out.println("Running"); } } new MyThread().start(); Runnable task = () -> System.out.println("Running"); // java2-3's own lambda syntax new Thread(task).start(); // preferred — Runnable is just an interface, not a class

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

class Counter { private int count = 0; public void increment() { count++; } // read-modify-write — NOT atomic public int getCount() { return count; } } // two threads calling increment() 100,000 times each — final count is often LESS than 200,000

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

public synchronized void increment() { count++; } // acquires this object's intrinsic lock automatically

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 pool = Executors.newFixedThreadPool(4); pool.submit(() -> System.out.println("Task running")); pool.shutdown(); AtomicInteger atomicCount = new AtomicInteger(0); atomicCount.incrementAndGet(); // lock-free, still safe under concurrent access

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.

ApproachC pthreads (c3-3)C++ (cpp3-4)JavaRust
Lock mechanismpthread_mutex_t, manual lock/unlockstd::mutex + RAII lock_guardsynchronized — built into the languageMutex<T>
Auto-release on exceptionnoyes — RAIIyes — built-inyes
Compiler enforces locking before accessnononoyes — data is owned by the Mutex
Reach for java.util.concurrent before hand-rolling Thread/synchronized
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 only protects code that actually uses it
Marking one method 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

Challenge 1

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 solution
Challenge 2

Fix 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 solution
Challenge 3

Write 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 solution

Chapter 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