A Java thread executes an independent sequence of instructions, and shared mutable state needs ordering and visibility rules between those sequences.
Java threads: visibility, atomicity, and shared counters
Increment is more than one action
counter++ reads a value, computes another, and writes it back. Two threads can read the same old value and lose one increment. Declaring the field volatile makes visibility rules stronger but does not combine those three steps into an atomic increment.
The program protects an entire increment with the same object monitor and reads under that monitor. The monitor establishes both mutual exclusion for the operation and visibility through the corresponding synchronisation actions.
Thread.start begins another execution. Calling run directly does not create a thread. join waits for completion and provides the ordering needed before the main thread inspects the completed work.
Protect the invariant, not just a field
For a single counter, AtomicInteger is another option. A transfer between two balances needs a larger invariant: changing one balance and the other must be coordinated as one valid operation. Separate atomic fields do not automatically supply that transaction.
The loop is a teaching workload, not a performance benchmark. It intentionally makes many tiny contended operations so the ownership rule is visible. In a real batch, accumulating per-thread totals and combining afterward may reduce contention.
Lock ordering also matters. Acquiring two locks in opposite orders across threads can deadlock. Use one documented order, minimise work under locks, and avoid hidden I/O while holding a shared application monitor.
Working program
public class ShipmentCounterThreads {
static final class Counter {
private int completed;
synchronized void increment() { completed++; }
synchronized int value() { return completed; }
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable batch = () -> {
for (int shipment = 0; shipment < 1000; shipment++) counter.increment();
};
Thread first = new Thread(batch, "shipment-first");
Thread second = new Thread(batch, "shipment-second");
first.start();
second.start();
first.join();
second.join();
System.out.println(counter.value());
}
}Output
2000Cost and design choices
For n total increments the program performs O(n) counter work, with a constant-sized counter and two threads. Contention and scheduling affect elapsed time; an asymptotic count does not prove parallel speedup.
Threads have runtime resources including stacks. Creating a thread for every unbounded request can exhaust resources. A managed executor centralises admission and lifecycle decisions.
InterruptedException communicates a cancellation signal to code waiting in join. A larger application should propagate it or restore interrupt status when it cannot propagate, rather than quietly treating interruption as successful completion.
Common Mistakes
- Do not use volatile as a substitute for atomic compound updates.
- Do not call run and expect another thread.
- Do not ignore interruption without a cancellation policy.
- Do not hold locks during avoidable external I/O.
