Learn Java

Lesson 60Phase 7: Concurrency

Thread creation and synchronization

Why shared data races, what synchronized guarantees, and the lifecycle of a thread.

By Updated 55 min

  • race condition
  • synchronized
  • shared state
  • thread lifecycle
  • join
  • sleep

In the introduction to threads you started several threads at once and watched their output come out in a different order on each run. That was harmless while each thread minded its own business. Trouble starts when two threads change the same data. This lesson shows the quiet bug that follows, and the keyword that prevents it. First, though, you need a way to make one thread wait for another.

By the end of this lesson
  • Make one thread wait for another with join(), and pause a thread with Thread.sleep().
  • Explain what a race condition is, and why two threads sharing a counter lose updates.
  • Break count++ into its three steps and say which orders of those steps lose an update.
  • Use synchronized methods and blocks so that only one thread at a time touches shared data, and say what a lock is.
  • Spot a check-then-act race and fix it.
  • Name the six states a thread can be in, and read them with getState().
From before
From the introduction to threads you know the main thread, how to hand a Runnable (usually a lambda) to a Thread, and why start() makes a new thread while run() does not. Two methods on this page can throw a checked exception, so you also need try, catch and throws from exception handling.
What runs here
The visualizer runs only the main thread, so every program that starts a second thread is read-along, with output copied from real runs of Java. Where that output changes from run to run, the text says so. Code that needs only the main thread runs here as usual, Thread.sleep included, and the race at the heart of this lesson has a stepper of its own that you can drive one step at a time.

Waiting for a thread: join#

Here is a worker built from a lambda. It counts to three, and then main prints that the worker is done. Read the output carefully.

NoJoin.java
read along
1public class Main {
2 public static void main(String[] args) {
3 Thread worker = new Thread(() -> {
4 for (int i = 1; i <= 3; i++) {
5 System.out.println("worker: " + i);
6 }
7 });
8
9 worker.start();
10 System.out.println("main: the worker is done");
11 }
12}
Output
main: the worker is done
worker: 1
worker: 2
worker: 3

main announced that the worker was done before the worker had printed a single line. start() only sets the worker going. main carried straight on to its println, and the worker got its first turn afterwards. The lines came out in this order in each of 70 runs, but nothing promises even that.

To make one thread wait for another, call join() on the thread to wait for. worker.join() pauses the thread that calls it, main here, until worker has finished its run().

Join.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Thread worker = new Thread(() -> {
4 for (int i = 1; i <= 3; i++) {
5 System.out.println("worker: " + i);
6 }
7 });
8
9 worker.start();
10 worker.join(); // wait here until worker has finished
11 System.out.println("main: the worker is done");
12 }
13}
Output
worker: 1
worker: 2
worker: 3
main: the worker is done

Now the order is the same on every run. The worker prints its three lines and finishes, and only then does join() return and let main print. join() can throw a checked exception, InterruptedException, which the next section explains, so main now declares throws InterruptedException.

Key idea

t.join() makes the calling thread wait until thread thas finished. Call it whenever one thread needs another thread's work to be complete.

Pausing a thread: sleep#

Thread.sleep(ms) pauses the thread that calls it for that many milliseconds, then lets it carry on. While it sleeps, the thread uses no processor time. It is how you make a timer tick once a second or wait a little before trying something again, and on this page it stands in for slow work. The visualizer runs this program, narrating each pause instead of waiting.

Countdown.java
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 for (int i = 3; i >= 1; i--) {
4 System.out.println(i + "...");
5 Thread.sleep(1000); // pause for one second
6 }
7 System.out.println("Go!");
8 }
9}
Prints
›3...
›2...
›1...
›Go!

On a real JVM the lines appear one second apart. The text printed is the same.

InterruptedException#

Both sleep and join can throw InterruptedException, a checked exception. It means another thread called interrupt() on this one: a request to stop waiting and get on with something else. No program in this lesson interrupts a thread, but the compiler still makes you handle the exception. In main you can declare it with throws. Inside a job you cannot, because the run() of Runnable declares no checked exceptions, and that rule holds for a lambda too:

SleepyJob.java (does not compile)
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Thread napper = new Thread(() -> {
4 Thread.sleep(500); // error: unreported exception InterruptedException;
5 // must be caught or declared to be thrown
6 System.out.println("awake");
7 });
8 napper.start();
9 napper.join();
10 }
11}

main declares throws InterruptedException, but that covers only the code of main itself. The lambda's body is the body of run(), so the sleep inside it needs its own try and catch. The jobs on the rest of this page have one.

The bug: two threads, one counter#

Now for the trouble. A shared Counter object has a field count and a method increment() that does count++. Two threads run the same job, each calling increment() 100,000 times, and main joins both before it prints. Two threads times 100,000 should make exactly 200,000.

Race.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Counter shared = new Counter();
4 Runnable job = () -> {
5 for (int i = 0; i < 100_000; i++) {
6 shared.increment();
7 }
8 };
9
10 Thread t1 = new Thread(job);
11 Thread t2 = new Thread(job);
12 t1.start();
13 t2.start();
14 t1.join();
15 t2.join();
16
17 System.out.println("Final count: " + shared.count);
18 }
19}
20
21class Counter {
22 int count = 0;
23
24 void increment() {
25 count++;
26 }
27}
Output
Final count: 127868

This run printed 127868, so about 72,000 increments vanished. Other runs of the same program printed 110514, 154935 and 106326. Nothing crashed and no error was printed. The program gave a wrong answer and looked finished.

It does not even fail every time. In one batch of 100 runs, 87 printed a wrong total, no two of them the same, and 13 printed the correct 200000, because one thread happened to finish its work before the other got going. A program like this can pass a quick test and fail later.

Key idea

A race condition is when the result of a program depends on the exact timing of two or more threads, in a way you did not intend. The threads race to touch the same data, and who wins changes the answer.

Where did the updates go?#

The whole problem hides in one short line: count++. It looks like one step, but Java runs it as three: read the current value of count from memory, add one to that value, then write the result back to count. Between any two of those steps, the other thread can get a turn.

While a thread works through the three steps, it keeps the value it read in its own working copy, separate from countin memory. That copy is where updates go missing. Drive the two threads yourself: “Step A” and “Step B” choose which thread moves next, and the buttons at the bottom play three orders for you.

Two threads, one counter
void increment() { count++; }

count++ runs as three steps: read count into the thread's own copy, add one to the copy, write the copy back.

count, in memory0
Thread ARUNNABLE
  1. copy = count
  2. copy = copy + 1
  3. count = copy

A's copy: none yet

Thread BRUNNABLE
  1. copy = count
  2. copy = copy + 1
  3. count = copy

B's copy: none yet

count starts at 0, and each thread will run count++ once, so count should end at 2. Choose which thread moves first.

Play an order:

Play “Both read first”. A and B each read 0, each add one to their own copy, and each write 1. count++ ran twice and countis 1, because the second write landed on top of the first. Now play “A, then B”. B reads after A has written, so B starts from 1 and count ends at 2. The only difference is the order of the steps. In a real program the scheduler chooses that order, a little differently on every run, and 200,000 increments give it plenty of chances to choose a bad one.

An action that happens as one step that no other thread can split is called atomic. count++ is not atomic.

Predict the outputInterleave.java
1public class Main {
2 public static void main(String[] args) {
3 int count = 41;
4
5 int a = count; // thread A reads
6 int b = count; // thread B reads
7 a = a + 1; // thread A adds one
8 b = b + 1; // thread B adds one
9 count = b; // thread B writes back
10 count = a; // thread A writes back
11
12 System.out.println(count);
13 }
14}

This program writes the two threads' steps out as lines of one program, in an order a scheduler might choose. a is thread A's copy and b is thread B's copy. What does it print?

It prints 42, though two increments ran. Both threads read 41 before either one wrote, so both copies became 42. B wrote 42, then A wrote its own 42 over it. Open it in the playground and step through it to watch a, b and count.

Quick check
Two threads each run count++ once on a shared counter that starts at 5, with no lock. Which final values can count have?
Shared, changing data is the whole danger
Two things made this race possible: both threads used the same Counter object, and both changed it. Take away either one and the race is gone. Two threads that each count with their own private Counter never collide, and data that every thread only reads cannot be damaged by reading. So the first question to ask about threaded code is which objects are shared, and which threads change them.

The fix: one thread at a time with synchronized#

The race exists because two threads can be part-way through increment() at the same time. The fix is to forbid exactly that: while one thread is running increment() on an object, no other thread may enter increment() on that same object. It must wait its turn. The keyword that does this is synchronized.

Fixed.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Counter shared = new Counter();
4 Runnable job = () -> {
5 for (int i = 0; i < 100_000; i++) {
6 shared.increment();
7 }
8 };
9
10 Thread t1 = new Thread(job);
11 Thread t2 = new Thread(job);
12 t1.start();
13 t2.start();
14 t1.join();
15 t2.join();
16
17 System.out.println("Final count: " + shared.count);
18 }
19}
20
21class Counter {
22 int count = 0;
23
24 synchronized void increment() {
25 count++;
26 }
27}
Output
Final count: 200000

One word changed: synchronized void increment(). In 50 runs, every one printed 200000. The read, the add and the write still happen as three steps, but no other thread can slip in between them, because only one thread at a time may be inside the method.

What synchronized does: a lock#

Picture a small room with one key hanging by the door. Every Java object has a key like this, called its lock (Java's documentation also calls it the object's monitor). To run a synchronizedmethod on an object, a thread must first take that object's lock. While it holds the lock, it runs the method. Any other thread that wants to run a synchronized method on the same object finds the key gone and waits by the door. When the first thread leaves the method, it hangs the key back up, and one waiting thread may take it and go in.

Key idea

synchronizedmakes a thread take the object's lock before it runs the code. Only one thread can hold a lock at a time, so the three steps of count++ can no longer be split by another thread.

Here is the stepper again, starting in its synchronized mode. Try to make B cut in.

Two threads, one counter
synchronized void increment() { count++; }

count++ runs as three steps: read count into the thread's own copy, add one to the copy, write the copy back.

count, in memory0
the counter's lockfree
Thread ARUNNABLE
  1. take the lock
  2. copy = count
  3. copy = copy + 1
  4. count = copy
  5. give the lock back

A's copy: none yet

Thread BRUNNABLE
  1. take the lock
  2. copy = count
  3. copy = copy + 1
  4. count = copy
  5. give the lock back

B's copy: none yet

increment() is now synchronized, so each thread must take the counter's lock before its three steps. Choose which thread moves first.

Play an order:

Each thread now takes the counter's lock before its read and gives it back after its write. While A holds the lock, pressing “Step B” gets nowhere: B is blocked at the door. Whichever thread takes the lock first has written its result before the other one can read, so the second thread always starts from the first one's result, and count ends at 2 whatever order you press.

Put the lines in orderOneAtATime.java
The lock only lets one kind of order through: all of one thread's steps, then all of the other's. Put the lines in such an order, so that neither update is lost and the program prints 43.

Tap a line, then tap where it should go. Or use the arrows.

The lock belongs to the object, not to the method. Two Counter objects have two locks, so a thread incrementing one counter never waits for a thread incrementing the other. That is what you want: they share no data, so there is nothing to protect. Two more rules complete the picture. A thread that already holds a lock can call other synchronized methods on the same object without waiting for itself. And a static synchronizedmethod takes a lock that belongs to the class as a whole, shared by every call of that class's static synchronized methods.

Quick check
a and b are two separate Counter objects whose increment() is synchronized. Thread 1 calls a.increment() while thread 2 calls b.increment(). What happens?

Synchronizing a block#

Sometimes only part of a method touches shared data. Then you can lock just that part with a synchronized block, which names the object whose lock to take in parentheses. Here a bank account checks the amount without the lock, because the check only reads the parameter, and locks only the line that changes the balance:

Payday.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Account account = new Account();
4 Runnable payday = () -> {
5 for (int i = 0; i < 1000; i++) {
6 account.deposit(10);
7 }
8 };
9
10 Thread t1 = new Thread(payday);
11 Thread t2 = new Thread(payday);
12 t1.start();
13 t2.start();
14 t1.join();
15 t2.join();
16
17 System.out.println("Balance: " + account.getBalance());
18 }
19}
20
21class Account {
22 private int balance = 0;
23
24 void deposit(int amount) {
25 if (amount <= 0) {
26 throw new IllegalArgumentException("amount must be positive");
27 }
28 synchronized (this) {
29 balance = balance + amount;
30 }
31 }
32
33 synchronized int getBalance() {
34 return balance;
35 }
36}
Output
Balance: 20000

Two threads each deposit 10 a thousand times, so the balance must end at 20000, and it does on every run. Inside a method, this is the object the method was called on, so synchronized (this) takes the same lock that a synchronized method would. In fact a synchronized method is shorthand for wrapping its whole body in synchronized (this) { ... }. getBalance() is synchronized too, because every read of shared data needs the lock, not only every write.

The bug that hides until real use
The most common synchronization mistake is leaving it out. The unsynchronized counter ran without complaint, printed a number and looked finished. It was wrong by a different amount on most runs and right on a few, so a race can pass every quick test on your laptop and only show up when real users put the program under load. When threads share data that any of them changes, guard every read and every write of it with the same lock.

Check, then act#

count++ is one shape of race: read a value, change it, write it back. Another shape is just as common. A thread checks something, then acts on what it saw, and between the check and the act another thread changes the facts. Here a ticket booth has one seat left, and Ana and Ben try to buy it at the same moment:

Booth.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Booth booth = new Booth();
4 Thread ana = new Thread(() -> booth.buy("Ana"));
5 Thread ben = new Thread(() -> booth.buy("Ben"));
6
7 ana.start();
8 ben.start();
9 ana.join();
10 ben.join();
11
12 System.out.println("Seats left: " + booth.seatsLeft);
13 }
14}
15
16class Booth {
17 int seatsLeft = 1;
18
19 void buy(String name) {
20 if (seatsLeft > 0) {
21 pause(100); // stands in for taking the payment
22 seatsLeft--;
23 System.out.println(name + " gets a seat");
24 } else {
25 System.out.println("Sorry " + name + ", sold out");
26 }
27 }
28
29 static void pause(int ms) {
30 try {
31 Thread.sleep(ms);
32 } catch (InterruptedException e) {
33 System.out.println("interrupted");
34 }
35 }
36}
Output
Ana gets a seat
Ben gets a seat
Seats left: -1

There was one seat, and both of them got it. Each thread checked seatsLeft > 0, saw 1 and went on to take the payment, which the pause stands in for. By the time either one took the seat, the other had already passed the check. In a batch of 100 runs the seat was sold twice in 99, in either order. Five of those even printed Seats left: 0, because the two seatsLeft--lines are a read, a subtraction and a write, and they raced as well. In the one remaining run, Ben's thread happened to start late enough to find the seat already gone.

The fix is the same keyword. The check and the act have to happen under one lock, so buy becomes synchronized:

BoothFixed.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Booth booth = new Booth();
4 Thread ana = new Thread(() -> booth.buy("Ana"));
5 Thread ben = new Thread(() -> booth.buy("Ben"));
6
7 ana.start();
8 ben.start();
9 ana.join();
10 ben.join();
11
12 System.out.println("Seats left: " + booth.seatsLeft);
13 }
14}
15
16class Booth {
17 int seatsLeft = 1;
18
19 synchronized void buy(String name) {
20 if (seatsLeft > 0) {
21 pause(100); // stands in for taking the payment
22 seatsLeft--;
23 System.out.println(name + " gets a seat");
24 } else {
25 System.out.println("Sorry " + name + ", sold out");
26 }
27 }
28
29 static void pause(int ms) {
30 try {
31 Thread.sleep(ms);
32 } catch (InterruptedException e) {
33 System.out.println("interrupted");
34 }
35 }
36}
Output
Ana gets a seat
Sorry Ben, sold out
Seats left: 0

Whoever takes the lock first now does the check, the payment and the sale before the other thread can even look. Ana won in every run here, because her thread was started first, but if Ben's thread reached the lock first, he would get the seat and Ana would hear “sold out”. Either way the seat is sold once. A sleeping thread keeps any lock it holds, so while Ana pays, Ben waits at the door.

Key idea

When a decision depends on shared data, the check and the action must happen under the same lock.

ExerciseGroupBooking.java
This booth sells group tickets. Finish buy(name, wanted): if at least wanted seats are left, take them and print, for example, Ana gets 2 seats; otherwise take none and print, for example, Sorry Ben, only 3 left. Then make the method safe for two threads, so that the check and the sale happen under one lock. The test can only check what the program prints, so the lock is up to you.

The life of a thread#

A thread is not simply on or off. Over its life it moves through a handful of states, and Java has a name for each one. getState() returns a thread's current state as a value of the enum Thread.State.

StateWhat it meansHow a thread gets there
NEWCreated, but not started yet.new Thread(...)
RUNNABLERunning, or ready to run and waiting for a turn on a core. Java does not tell those two apart.Calling start(), or coming back from any of the waits below.
BLOCKEDWaiting to take a lock that another thread holds.Reaching a synchronized method or block whose lock is taken.
WAITINGWaiting, with no time limit, for another thread to do something.join() with no timeout, and a few tools from later lessons.
TIMED_WAITINGWaiting, but only for a set time.Thread.sleep(ms), or join with a timeout.
TERMINATEDFinished. It cannot run again.run() returns, or throws an exception that nothing catches.
The six values of Thread.State.

This program catches a thread in four of them:

States.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Thread napper = new Thread(() -> {
4 System.out.println("inside run: " + Thread.currentThread().getState());
5 try {
6 Thread.sleep(500);
7 } catch (InterruptedException e) {
8 System.out.println("woken early");
9 }
10 });
11
12 System.out.println("before start: " + napper.getState());
13 napper.start();
14 Thread.sleep(100);
15 System.out.println("while it sleeps: " + napper.getState());
16 napper.join();
17 System.out.println("after join: " + napper.getState());
18 }
19}
Output
before start: NEW
inside run: RUNNABLE
while it sleeps: TIMED_WAITING
after join: TERMINATED

Before start() the thread is NEW. Inside its own run(), the thread asks for its own state and gets RUNNABLE, because it is running at that moment. main then sleeps for 100 milliseconds, so it looks while the thread is in the middle of its 500 millisecond sleep: TIMED_WAITING. After join() the thread has finished: TERMINATED. The sleeps make this timing reliable in practice, and the four lines came out the same in each of 80 runs, but nothing in Java promises it.

BLOCKED shows up at the ticket booth. This version of main starts Ana first, waits a moment, starts Ben, and looks at both of them while Ana is paying:

Blocked.java
read along
1public class Main {
2 public static void main(String[] args) throws InterruptedException {
3 Booth booth = new Booth();
4 Thread ana = new Thread(() -> booth.buy("Ana"));
5 Thread ben = new Thread(() -> booth.buy("Ben"));
6
7 ana.start();
8 Thread.sleep(50); // Ana gets into buy first
9 ben.start();
10 Thread.sleep(50); // Ben reaches the locked door
11
12 System.out.println("Ana is " + ana.getState());
13 System.out.println("Ben is " + ben.getState());
14
15 ana.join();
16 ben.join();
17 }
18}
19
20class Booth {
21 int seatsLeft = 1;
22
23 synchronized void buy(String name) {
24 if (seatsLeft > 0) {
25 pause(500); // stands in for taking the payment
26 seatsLeft--;
27 System.out.println(name + " gets a seat");
28 } else {
29 System.out.println("Sorry " + name + ", sold out");
30 }
31 }
32
33 static void pause(int ms) {
34 try {
35 Thread.sleep(ms);
36 } catch (InterruptedException e) {
37 System.out.println("interrupted");
38 }
39 }
40}
Output
Ana is TIMED_WAITING
Ben is BLOCKED
Ana gets a seat
Sorry Ben, sold out

Ana is inside buy, asleep while she pays, so her state is TIMED_WAITING, and she still holds the booth's lock. Ben has reached buy and is waiting for that lock, so he is BLOCKED. When main then sits in ana.join(), its own state is WAITING. These lines too came out the same in each of 80 runs.

Quick check
Thread t is in the middle of Thread.sleep(2000). What does t.getState() return?
A thread runs once
Once a thread is TERMINATED, it is finished for good. Calling start() on it again does not restart it: Java throws an IllegalThreadStateException, and it does the same for a second start() on a thread that is still running. To do the work again, create a new Thread and start that.
Where concurrency goes next
The next lessons build on this one. Thread pools, executors and futures runs jobs on a pool of threads and gets results back from them. Virtual threads covers the very cheap threads of Java 21. Safe sharing: atomics, locks and concurrent collections covers counters that are safe without synchronized, maps built for many threads, and deadlock, where two threads each wait forever for a lock the other one holds.

Try it

Answer each one in your head or on paper first, then open the answer.

1
In your own words, why is count++ not a single, safe step when two threads share the same counter?
2
The racing program printed 127868 instead of 200000. If you ran the same program again, would you expect 127868 a second time? Why or why not?
3
What does adding synchronized to increment() guarantee about how threads run that method on the same Counter object?
4
Put these states in the order a thread passes through them: TERMINATED, RUNNABLE, NEW. Where can BLOCKED and TIMED_WAITING come in?
5
In the first program on this page, main printed “the worker is done” too early. Which one line fixes that, and what does it do?
6
A shop's sell() method does if (stock > 0) { stock--; } with no lock, and two threads call it when stock is 1. What can go wrong, and what is the fix?

Practice

Work through these on your own. Each one builds on this page.

1
On paper, trace one losing order by hand. Start with count at 7. Two threads each call increment() once, and their steps overlap in the worst way. Write out, step by step, what each thread reads, what its copy holds and what it writes, and show how count ends at 8 instead of 9.
2
Suppose each of the two threads had its own private Counter object instead of sharing one. Would synchronized still be needed? Explain in a short paragraph, using the idea that a lock belongs to an object.
3
On your own computer, write a program where main starts one worker that prints the numbers 1 to 5 with a 200 millisecond sleep between them, then uses join() to wait and prints worker finished only after all five numbers. Run it a few times. Does the order ever change?
4
Take the Payday program, remove the synchronized (this) block (keep the line inside it), and raise each thread's loop to 100,000 deposits. Run it five times on your own computer and write down the balances. What should the balance be, and what does that tell you?
5
In two or three sentences, explain to a friend why a race condition is more dangerous than a program that crashes outright. Use the fact that the racing counter printed a number and looked finished.

Further reading#

Spotted a mistake, or a part that did not make sense? Report it by email