Start Learning
Javaneer
Back to stage
Stage 4·Advanced Java

Concurrency Principles

Race conditions, deadlock, volatile, atomics, concurrent collections, and the Java Memory Model.

22 min readAdvanced
On this page

Threads sharing memory is where concurrency gets dangerous. Two threads touching the same data at the same time can corrupt it in ways that are maddening to debug. This lesson covers the classic hazards - and how to write safe concurrent code.

The race condition

A race condition happens when the result depends on the unpredictable timing of threads. The classic example: two threads incrementing the same counter.

class Counter {
    int count = 0;
    void increment() { count++; }   // NOT atomic!
}

count++ looks like one step, but it's actually three: read count, add one, write it back. If two threads interleave those steps, updates get lost:

Two people editing the same document

Two editors open the same shared doc showing "10 sales". Both read 10, both add 1, both save 11. Two sales happened, but the count only went up by one - one edit was silently lost. That's a race condition: correct-looking steps, wrong result, because of timing.

Fix 1 - synchronized

The synchronized keyword ensures only one thread runs a block at a time, by acquiring a lock:

class Counter {
    private int count = 0;
    synchronized void increment() { count++; }  // one thread at a time
    synchronized int get() { return count; }
}
A lock is like a single-key bathroom

A one-key bathroom guarantees only one person is inside at a time; others wait for the key. A lock does the same for a block of code - only the thread holding the lock may enter, so shared data can't be corrupted mid-update.

Fix 2 - atomic classes

For simple counters, java.util.concurrent.atomic offers lock-free, thread-safe types:

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();   // atomic - safe without explicit locks

Fix 3 - concurrent collections

Regular collections aren't thread-safe. Use purpose-built concurrent versions:

Map<String, Integer> safe = new ConcurrentHashMap<>();
Queue<Task> queue = new ConcurrentLinkedQueue<>();

Best of all: avoid sharing mutable state

The safest concurrent code shares as little mutable data as possible. Prefer immutable objects (which can be shared freely), pass data via thread-safe queues, and confine mutable state to a single thread. Less shared state means fewer ways to go wrong.

Deadlock

A deadlock occurs when threads wait on each other forever. Thread A holds lock 1 and wants lock 2; thread B holds lock 2 and wants lock 1. Neither can proceed.

The dining philosophers

Five philosophers sit around a table, each needing two forks to eat. If every philosopher grabs the fork on their left and waits for the one on their right - nobody ever eats. Everyone holds something the next person needs. That's deadlock.

Avoid it by always acquiring locks in the same order, holding locks briefly, and using timeouts.

The Java Memory Model and volatile

Threads may cache variables locally, so one thread's change might not be visible to another. The volatile keyword guarantees that reads and writes go straight to main memory, so all threads see the latest value:

private volatile boolean running = true;   // changes are visible across threads

void stop() { running = false; }            // another thread will see this

volatile ensures visibility, not atomicity

volatile guarantees other threads see the latest value, but it does not make compound operations like count++ atomic. For counters, use synchronized or an AtomicInteger. Use volatile for simple flags.

Quick check

Two threads both run count++ on a shared, unsynchronized counter. What can go wrong?

Key takeaways

  • A race condition arises when correctness depends on thread timing (e.g. unsynchronized count++).
  • synchronized locks a block so only one thread runs it at a time, protecting shared data.
  • Atomic classes (AtomicInteger) and concurrent collections (ConcurrentHashMap) are thread-safe by design.
  • Deadlock is threads waiting on each other forever; avoid it by acquiring locks in a consistent order.
  • volatile guarantees visibility of changes across threads but not atomicity of compound operations.
  • Safest of all: minimize shared mutable state and prefer immutable objects.

Now we shift gears from concurrency to a different superpower: treating code as data with lambdas and functional interfaces.