Lesson 60Phase 7: Concurrency
Thread creation and synchronization
Why shared data races, what synchronized guarantees, and the lifecycle of a thread.
By BasithUpdated 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.
- Make one thread wait for another with
join(), and pause a thread withThread.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
synchronizedmethods 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().
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.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.
public class Main { public static void main(String[] args) { Thread worker = new Thread(() -> { for (int i = 1; i <= 3; i++) { System.out.println("worker: " + i); } }); worker.start(); System.out.println("main: the worker is done"); }}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().
public class Main { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { for (int i = 1; i <= 3; i++) { System.out.println("worker: " + i); } }); worker.start(); worker.join(); // wait here until worker has finished System.out.println("main: the worker is done"); }}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 threadthas 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.
public class Main { public static void main(String[] args) throws InterruptedException { for (int i = 3; i >= 1; i--) { System.out.println(i + "..."); Thread.sleep(1000); // pause for one second } System.out.println("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:
public class Main { public static void main(String[] args) throws InterruptedException { Thread napper = new Thread(() -> { Thread.sleep(500); // error: unreported exception InterruptedException; // must be caught or declared to be thrown System.out.println("awake"); }); napper.start(); napper.join(); }}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.
public class Main { public static void main(String[] args) throws InterruptedException { Counter shared = new Counter(); Runnable job = () -> { for (int i = 0; i < 100_000; i++) { shared.increment(); } }; Thread t1 = new Thread(job); Thread t2 = new Thread(job); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final count: " + shared.count); }} class Counter { int count = 0; void increment() { count++; }}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 ideaA 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.
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.
- copy = count
- copy = copy + 1
- count = copy
A's copy: none yet
- copy = count
- copy = copy + 1
- 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 “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.
public class Main { public static void main(String[] args) { int count = 41; int a = count; // thread A reads int b = count; // thread B reads a = a + 1; // thread A adds one b = b + 1; // thread B adds one count = b; // thread B writes back count = a; // thread A writes back System.out.println(count); }}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.
count++ once on a shared counter that starts at 5, with no lock. Which final values can count have?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.
public class Main { public static void main(String[] args) throws InterruptedException { Counter shared = new Counter(); Runnable job = () -> { for (int i = 0; i < 100_000; i++) { shared.increment(); } }; Thread t1 = new Thread(job); Thread t2 = new Thread(job); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final count: " + shared.count); }} class Counter { int count = 0; synchronized void increment() { count++; }}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 ofcount++can no longer be split by another thread.
Here is the stepper again, starting in its synchronized mode. Try to make B cut in.
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.
- take the lock
- copy = count
- copy = copy + 1
- count = copy
- give the lock back
A's copy: none yet
- take the lock
- copy = count
- copy = copy + 1
- count = copy
- 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.
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.
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.
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:
public class Main { public static void main(String[] args) throws InterruptedException { Account account = new Account(); Runnable payday = () -> { for (int i = 0; i < 1000; i++) { account.deposit(10); } }; Thread t1 = new Thread(payday); Thread t2 = new Thread(payday); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Balance: " + account.getBalance()); }} class Account { private int balance = 0; void deposit(int amount) { if (amount <= 0) { throw new IllegalArgumentException("amount must be positive"); } synchronized (this) { balance = balance + amount; } } synchronized int getBalance() { return balance; }}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.
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:
public class Main { public static void main(String[] args) throws InterruptedException { Booth booth = new Booth(); Thread ana = new Thread(() -> booth.buy("Ana")); Thread ben = new Thread(() -> booth.buy("Ben")); ana.start(); ben.start(); ana.join(); ben.join(); System.out.println("Seats left: " + booth.seatsLeft); }} class Booth { int seatsLeft = 1; void buy(String name) { if (seatsLeft > 0) { pause(100); // stands in for taking the payment seatsLeft--; System.out.println(name + " gets a seat"); } else { System.out.println("Sorry " + name + ", sold out"); } } static void pause(int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { System.out.println("interrupted"); } }}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:
public class Main { public static void main(String[] args) throws InterruptedException { Booth booth = new Booth(); Thread ana = new Thread(() -> booth.buy("Ana")); Thread ben = new Thread(() -> booth.buy("Ben")); ana.start(); ben.start(); ana.join(); ben.join(); System.out.println("Seats left: " + booth.seatsLeft); }} class Booth { int seatsLeft = 1; synchronized void buy(String name) { if (seatsLeft > 0) { pause(100); // stands in for taking the payment seatsLeft--; System.out.println(name + " gets a seat"); } else { System.out.println("Sorry " + name + ", sold out"); } } static void pause(int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { System.out.println("interrupted"); } }}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 ideaWhen a decision depends on shared data, the check and the action must happen under the same lock.
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.
| State | What it means | How a thread gets there |
|---|---|---|
NEW | Created, but not started yet. | new Thread(...) |
RUNNABLE | Running, 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. |
BLOCKED | Waiting to take a lock that another thread holds. | Reaching a synchronized method or block whose lock is taken. |
WAITING | Waiting, with no time limit, for another thread to do something. | join() with no timeout, and a few tools from later lessons. |
TIMED_WAITING | Waiting, but only for a set time. | Thread.sleep(ms), or join with a timeout. |
TERMINATED | Finished. It cannot run again. | run() returns, or throws an exception that nothing catches. |
This program catches a thread in four of them:
public class Main { public static void main(String[] args) throws InterruptedException { Thread napper = new Thread(() -> { System.out.println("inside run: " + Thread.currentThread().getState()); try { Thread.sleep(500); } catch (InterruptedException e) { System.out.println("woken early"); } }); System.out.println("before start: " + napper.getState()); napper.start(); Thread.sleep(100); System.out.println("while it sleeps: " + napper.getState()); napper.join(); System.out.println("after join: " + napper.getState()); }}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:
public class Main { public static void main(String[] args) throws InterruptedException { Booth booth = new Booth(); Thread ana = new Thread(() -> booth.buy("Ana")); Thread ben = new Thread(() -> booth.buy("Ben")); ana.start(); Thread.sleep(50); // Ana gets into buy first ben.start(); Thread.sleep(50); // Ben reaches the locked door System.out.println("Ana is " + ana.getState()); System.out.println("Ben is " + ben.getState()); ana.join(); ben.join(); }} class Booth { int seatsLeft = 1; synchronized void buy(String name) { if (seatsLeft > 0) { pause(500); // stands in for taking the payment seatsLeft--; System.out.println(name + " gets a seat"); } else { System.out.println("Sorry " + name + ", sold out"); } } static void pause(int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { System.out.println("interrupted"); } }}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.
t is in the middle of Thread.sleep(2000). What does t.getState() return?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.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.
count++ not a single, safe step when two threads share the same counter?127868 instead of 200000. If you ran the same program again, would you expect 127868 a second time? Why or why not?synchronized to increment() guarantee about how threads run that method on the same Counter object?TERMINATED, RUNNABLE, NEW. Where can BLOCKED and TIMED_WAITING come in?main printed “the worker is done” too early. Which one line fixes that, and what does it do?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.
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.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.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?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?Further reading#
- Thread.State in the Java SE 25 API documentation, with the exact meaning of all six states
- The Thread class in the Java SE 25 API documentation, for join, sleep and interrupt
- Synchronization, from Oracle's Java Tutorials: thread interference, synchronized methods and locks (written for Java 8, and these basics have not changed)
- Threads and Locks, chapter 17 of the Java Language Specification, the precise rules behind synchronized and join
Spotted a mistake, or a part that did not make sense? Report it by email