Java has had threads since version 1.0. The language provides locking through
synchronized, and the memory model says which values a read may
see when threads share variables. The model was rewritten for Java 5 (JSR 133) and is
JLS chapter 17. Threads themselves, platform and virtual (Java 21), are created through
the Thread class and the java.util.concurrent library.
synchronized
Every object has a monitor, a lock that one thread at a time can hold. A thread may lock a monitor it already holds again (it is re-entrant); it must unlock it as many times.
| Form | Locks |
|---|---|
synchronized (obj) { … } | obj’s monitor |
synchronized void m() { … } | this |
static synchronized void m() { … } | The class’s Class object, C.class |
The lock is released however the block or method ends, including by an exception.
Interface methods, constructors and initialisers cannot be synchronized,
though they may contain synchronized blocks.
class Counter {
private int count;
synchronized void increment() { count++; } // read, add and write as one step
synchronized int get() { return count; } // also synchronized, to see the latest value
}
wait, notify and interrupts
Every object also has a wait set (JLS 17.2), used through methods of
Object by a thread that holds the object’s monitor (otherwise they throw
IllegalMonitorStateException):
| Method | Effect |
|---|---|
wait(), wait(ms), wait(ms, ns) | Releases the monitor, waits until notified, interrupted or timed out, then locks it again |
notify() | Wakes one waiting thread, if any |
notifyAll() | Wakes every waiting thread |
A waiting thread may also wake for no reason (a spurious wake-up), so the
condition must be tested in a loop. Thread.interrupt() makes a waiting
thread throw InterruptedException.
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // releases the lock while waiting
}
item = queue.remove();
}
Thread.sleep pauses without releasing any monitor, and
Thread.yield hints that other threads may run; neither has any
synchronisation effect, so a loop waiting on a non-volatile field may never see it
change (JLS 17.3).
The memory model
Compilers and processors reorder reads and writes, and cache values, as long as each thread on its own cannot tell. Another thread can, so without synchronisation one thread may see another’s writes late, or in a different order. The memory model defines a partial order, happens-before: if a write happens-before a read of the same variable, the read sees that write or a later one (JLS 17.4.5).
Happens-before rules
- Each action in a thread happens-before every later action in that thread (program order).
- Unlocking a monitor happens-before every later lock of the same monitor.
- A write to a
volatilefield happens-before every later read of that field. Thread.start()happens-before every action in the started thread.- Every action in a thread happens-before another thread returns from
join()on it, or seesisAlive()false. - Interrupting a thread happens-before the interrupted thread detects it.
- The end of a constructor happens-before the start of the object’s finaliser.
- The default values of every variable happen-before the first action of every thread.
- Happens-before is transitive.
The java.util.concurrent classes document their own happens-before
guarantees, built on these.
Data races
Two accesses to the same variable, at least one of them a write, that are not ordered by happens-before form a data race. A program with no data races in any sequentially consistent execution is correctly synchronised, and then every execution appears sequentially consistent: as if all actions ran in one order, each seeing the last write before it. Programs with races still have defined behaviour, unlike in C: the model limits which values a read can return, and rules out values “out of thin air”.
volatile
A volatile field is read from and written to main memory in a single
global order shared by all threads, and its writes happen-before its later reads.
Everything a thread wrote before writing a volatile field is visible to a thread that
then reads that field. Reads and writes of volatile long and
double fields are atomic.
class Worker implements Runnable {
private volatile boolean stopped; // without volatile, run() might never see the change
public void stop() { stopped = true; }
public void run() {
while (!stopped) doWork();
}
}
volatile does not make compound actions atomic: count++ on a
volatile field can still lose updates. Use synchronized or
java.util.concurrent.atomic.AtomicInteger.
final fields
If an object’s constructor does not let this escape, every thread that
obtains a reference to the object after the constructor finishes sees the values its
constructor wrote to its final fields, and everything reachable through
them as of that time, without any synchronisation (JLS 17.5). This is what makes
immutable objects such as String safe to share. Fields that are not final
have no such guarantee when the object is passed through a data race.
Changing a final field afterwards by reflection or other means is
allowed only in special cases (such as deserialisation) and undermines the guarantee.
Since Java 26 the JDK warns about such changes made through deep reflection, as a
step towards forbidding them (JEP 500).
Word tearing and long and double
- Every field and array element is a separate variable: writing one byte of a
byte[]never changes its neighbours, even on hardware that can only write whole words (JLS 17.6). - Writes to non-volatile
longanddoublevariables may be split into two 32-bit halves, so another thread could see half of an old value and half of a new one (JLS 17.7). Reads and writes of references and of every other primitive type are always atomic. Most 64-bit JVMs do write 64-bit values atomically, but the language does not promise it.