Thread creation and synchronization
Why shared data races, what synchronized guarantees, and the lifecycle of a thread.
Finished reading?
Mark this session so you can track where you are.
Why shared data races, what synchronized guarantees, and the lifecycle of a thread.
Finished reading?
Mark this session so you can track where you are.
Last session you got two threads running at once and watched their output interleave. That was fun while each thread minded its own business. The trouble starts the moment two threads reach for the same piece of data at the same time. This session is about that collision, the quiet bug it causes, and the one keyword that puts it right.
Runnable and start it, from memory.synchronized to let only one thread at a time touch shared data, and say what a lock is.join(), and pause a thread with Thread.sleep().Thread class, the Runnable job, and the all-important difference between start() (run me alongside) and run() (just call the method). You also know try and catch, which we lean on once, near the end, for a checked exception that sleep and join can throw.Before we break anything, let us rebuild a working thread so the shape is fresh. A thread needs a job to do. The cleanest way to hand it one is to write a class that implements Runnable, which is a promise to provide a run() method holding the work. Then we wrap that job in a Thread and call start() on it.
public class Main { public static void main(String[] args) { Runnable job = new Counter(); Thread worker = new Thread(job); worker.start(); System.out.println("main: I started a worker"); }} class Counter implements Runnable { public void run() { System.out.println("worker: counting to three"); for (int i = 1; i <= 3; i = i + 1) { System.out.println("worker: " + i); } }}main: I started a worker worker: counting to three worker: 1 worker: 2 worker: 3
Two threads are alive here: the main thread that runs main, and the worker thread that runs Counter.run(). The output above is one plausible ordering. Because the two threads run side by side, the worker's first line might land before or after the main thread's line. That small uncertainty is harmless when each thread writes its own numbers. It stops being harmless the instant they share a number.
worker.run() would just execute the method on the main thread, like any ordinary method call, and no second thread would ever exist. Only worker.start() hands the job to a brand new thread. This was the trap from last session, and it is worth carrying forward.Here is a small, believable program. We have a shared Counter object with a field count and a method increment() that does count++. We start two threads, and each one calls increment() a hundred thousand times. Two threads, a hundred thousand each, so the final count should be exactly two hundred thousand. Read it, then look at what it actually prints.
public class Main { public static void main(String[] args) { Counter shared = new Counter(); Thread t1 = new Thread(new Worker(shared)); Thread t2 = new Thread(new Worker(shared)); t1.start(); t2.start(); // (we will learn join() below; for now imagine both threads have finished) System.out.println("Final count: " + shared.count); System.out.println("(expected 200000)"); }} class Counter { int count = 0; void increment() { count++; }} class Worker implements Runnable { Counter shared; Worker(Counter shared) { this.shared = shared; } public void run() { for (int i = 0; i < 100000; i = i + 1) { shared.increment(); } }}Final count: 134812 (expected 200000)
The count came out as 134812, not 200000. Roughly sixty-five thousand increments simply vanished. And if you ran it again you would get a different wrong number: maybe 151903, maybe 187264. Almost never the right answer, and almost never the same wrong answer twice. Nothing crashed. No error printed. The program lied quietly. That is the most dangerous kind of bug there is.
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 are racing to touch the same data, and who wins changes the answer.
The whole problem hides inside one tiny, innocent line: count++. It looks like a single step. It is not. To add one to count, the computer does three separate things, in order: read the current value of count out of memory, add one to that value, then write the new value back into memory. Three steps, and a second thread can squeeze in between any two of them.
Counter object, and both were changing it. Shared, and mutable. Remove either property and the race is gone. Two threads each counting their own private Counter never collide. A value that is read but never written cannot be corrupted by reading. The danger lives precisely where threads share data that someone modifies.The race exists because two threads can be partway through increment() at the same time. So the fix is to forbid exactly that: make it so 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 grants this is synchronized.
public class Main { public static void main(String[] args) { Counter shared = new Counter(); Thread t1 = new Thread(new Worker(shared)); Thread t2 = new Thread(new Worker(shared)); t1.start(); t2.start(); // both threads finish (see join() below) System.out.println("Final count: " + shared.count); System.out.println("(expected 200000)"); }} class Counter { int count = 0; synchronized void increment() { count++; }} class Worker implements Runnable { Counter shared; Worker(Counter shared) { this.shared = shared; } public void run() { for (int i = 0; i < 100000; i = i + 1) { shared.increment(); } }}Final count: 200000 (expected 200000)
One word changed: increment() is now synchronized void increment(). And now the count is 200000 every single time, no matter how many times you run it. The vanished updates are back. The read, the add, and the write are no longer interruptible by another thread partway through, because only one thread is allowed inside the method at a time.
Picture a small room with a single key hanging by the door. To run a synchronizedmethod on an object, a thread must first pick up that object's key, called its lock or monitor. While it holds the key, it runs the method. Any other thread that wants in finds the key gone and must wait by the door. When the first thread leaves the method, it hangs the key back up, and one waiting thread may now take it and go in. One key, so one thread inside at a time.
synchronizedattaches a lock to an object. Only the thread holding that object's lock may run the object's synchronized code. Everyone else waits. That waiting is what makes the three steps ofcount++behave as one unbreakable step.
The lock belongs to the object, not the method. Two different Counter objects have two different keys, so a thread working on one does not block a thread working on the other. That is right and good: they share no data, so there is nothing to protect. The lock only matters when threads are reaching for the same object, which is exactly the case we were trying to fix.
Sometimes only a few lines need protecting and you do not want to lock for the entire method. You can wrap just those lines in a synchronizedblock, naming which object's lock to take. Inside a method, the object itself is this, so a synchronized method is really shorthand for locking on this around the whole body.
public class Main { public static void main(String[] args) { Account acc = new Account(); // imagine two threads each calling deposit on the same account // (read-along; the protected block keeps the total honest) System.out.println("balance after deposits: " + acc.balance); }} class Account { int balance = 300; void deposit(int amount) { System.out.println("preparing deposit of " + amount); synchronized (this) { // only one thread at a time runs this line balance = balance + amount; } System.out.println("done"); }}balance after deposits: 300
We have spoken of threads waiting by the door. That hints at something useful: a thread is not simply on or off. Over its life it moves through a handful of states, and knowing their names makes thread behavior far easier to reason about. In plain terms, here is the journey.
Thread object has been created with new, but you have not called start() yet. It exists, but it is not running and never has.start(). The thread is ready to run and eligible for the processor. It may be running right now, or waiting its turn behind other threads. Java lumps “ready” and “running” together under this one name.run() method has finished, or the thread was stopped. The thread is done. It cannot be restarted. Call start() on it again and Java throws an error.A normal, happy thread walks this path: new when you build it, runnable the moment you start it, dipping in and out of waiting whenever it needs a lock or pauses, and finally terminated when its run() returns. The situations you saw earlier fit right in: a thread waiting by the door for a synchronized lock is in the blocked state, and a thread that called sleep is in a waiting state.
start() on a thread a second time does not restart it. Java throws an IllegalThreadStateException. If you need the work done again, build a fresh Thread object and start that.Look back at the racing example. The comment admitted a white lie: we printed the final count “after both threads finish”, but nothing actually made the main thread wait for them. The main thread fired off start() twice and rushed straight to the println. In real life it could easily print the count while the workers are barely getting going. We need a way to say: main thread, stop here until those workers are done.
Calling t1.join()means “pause the thread that is calling this, the main thread here, until t1has finished running”. The caller goes into a waiting state and only continues once t1 reaches its terminated state. Now the count is read at the right moment.
public class Main { public static void main(String[] args) { Counter shared = new Counter(); Thread t1 = new Thread(new Worker(shared)); Thread t2 = new Thread(new Worker(shared)); t1.start(); t2.start(); System.out.println("main: workers started, now waiting"); try { t1.join(); // wait for t1 to finish t2.join(); // then wait for t2 to finish } catch (InterruptedException e) { System.out.println("waiting was interrupted"); } System.out.println("main: both done, count = " + shared.count); }} class Counter { int count = 0; synchronized void increment() { count++; }} class Worker implements Runnable { Counter shared; Worker(Counter shared) { this.shared = shared; } public void run() { for (int i = 0; i < 100000; i = i + 1) { shared.increment(); } }}main: workers started, now waiting main: both done, count = 200000
Now the order is guaranteed. The first line always prints first, because the main thread reaches it before it waits. Then the main thread blocks at t1.join() until t1 is done, then at t2.join() until t2 is done, and only then reads count. Because increment() is synchronized and we waited properly, the answer is 200000, reliably.
Thread.sleep(ms) tells the current thread to stop running for at least the given number of milliseconds, then carry on. It is how you make a thread pause: a one second tick, a short delay between attempts, a deliberate slow-down so a person can watch output scroll. The thread is in a waiting state for that whole time and uses no processor.
public class Main { public static void main(String[] args) { for (int i = 1; i <= 3; i = i + 1) { System.out.println("tick " + i); try { Thread.sleep(1000); // pause this thread for 1000 ms } catch (InterruptedException e) { System.out.println("sleep was interrupted"); } } }}tick 1 tick 2 tick 3
The three lines print one second apart instead of all at once. The output looks the same on the page; the difference is the pause you cannot see in text. Notice both sleep and join sit inside a try with a catch (InterruptedException e). That is required: both can throw the checked InterruptedException if another thread asks this one to wake up early, and a checked exception must be handled. You met that rule in exception handling.
Try each one yourself first, then open the answer.
count++ not a single, safe step when two threads share the same counter?134812 instead of 200000. If you ran the exact same program again, would you expect 134812 a second time? Why or why not?synchronized to increment() guarantee about how threads run that method on the same Counter object?start() calls. Which method should it have called first, and what would that method do?Take these away. They continue exactly what we just did.
count at 7. Two threads each call increment() once, but they overlap in the worst way. Write out, step by step, what each thread reads and writes, and show how the count ends at 8 instead of 9.Counter object rather than sharing one, would synchronized still be needed? Explain using the idea that a lock belongs to an object.join() to wait, then prints “worker finished” only after all five numbers. Make sure your join() sits inside a try that catches InterruptedException.