Today, I want to share with you an introduction to multithreading in Java. In order to make the blog post more fun, I’ll use the roles of “Game of Thrones” in my examples: white walkers and the Night King. After reading the blog post, you’ll understand:
- How to create and run a thread
- How a thread notifies others using
wait()
,notify()
, andnotifyAll()
- How a thread notifies others using
join()
Let’s get started.
The Night King and his army | HBO
1. How to Create and Use Threads
First of all, let’s ensure we know how to create and use threads. There’re several ways to create and start a thread.
1.1. Extending Class Thread
You can create your own Java object by extending the class java.lang.Thread
,
and override the Thread#run()
method.
Once everything is done, you must start the thread by calling method
Thread#start()
. When a new thread of execution stats, it will execute the code
defined in the thread instance’s method run()
. Method start()
will trigger
creation of a new thread of execution, allocating resources to it.
public class WhiteWalker extends Thread {
@Override
public void run() {
System.out.println("From inheritance");
}
}
/* Somewhere else */
WhiteWalker whiteWalker = new WhiteWalker();
whiteWalker.start();
However, when you create a thread class by extending class Thread
, you lose
the flexibility of inheriting any other class. To get around this, instead of
extending class Thread
, you can implement the java.lang.Runnable
interface,
which will be discussed later.
1.2. Using Anonymous Class
If you want to create a Thread
without being bothered by the inheritance, you
can use the anonymous class, which overrides the Thread#run()
method in-place:
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("From anonymous");
}
}
thread.start();
1.3. Implementing Interface Runnable
If a class implements java.lang.Runnable
, its instances can be executed by
threads of execution via method #run()
. For example:
public class WhiteWalker implements Runnable {
@Override
public void run() {
System.out.println("From Runnable");
}
}
This class can be executed by Thread
:
Thread thread = new Thread(new WhiteWalker());
thread.start();
1.4. Using Lambda
In Java 8, you can forget all the ceremonies and use lambda expression :)
Thread thread = new Thread(() -> System.out.println("From lambda"));
thread.start();
Now I would like to explain how threads wait, and notify others using different approaches: wait-notify, or join. There’re many other models, but only these two will be discussed in the blog post. These actions are related to thread lifecycle.
Let’s ues white walkers as our context. Assume that there’s a Night King and many white walkers. The Night King and each white walker uses its own thread, but the white walkers cannot move before the Night King is ready. Once ready, the white walkers can prepare and follow the king after that. How can we implement this scene?
2. Wait, Notify and NotifyAll
The Night King notifies his army. | HBO
$ java io.mincong.ocpjp.threads.ThreadWait
Night King is preparing...
A is waiting...
B is waiting...
C is waiting...
Night King is ready.
C follows.
B follows.
A follows.
2.1. White Walker
Firstly, we need to write a class for the army—white walkers. White walker
extends the class Thread
, so that they can be started easily. A Night King
instance is assigned to the white walker, so the white walker can understand
who to follow:
public class WhiteWalker extends Thread {
WhiteWalker(String name, NightKing king) {
this.name = name;
this.king = king;
}
...
}
Now, let’s take a look in the method run()
. Inside the run()
method, white
walker waits until the king
is ready. When digging deeper, you can notice that
a synchronized
statement is used before calling method wait()
. This is
because a thread must acquire a lock on an object monitor, before it can
execute the synchronized statements. The state of thread WhiteWalker
is
RUNNABLE before the waiting the king.
When thread WhiteWalker
calls king.wait(1000)
, it is placed in the waiting
set of threads for the object king
, gives up king
‘s monitor lock, and waits
utils:
- Another thread invokes
notify()
ornotifyAll()
on the same objectking
. - Some other threads interrupt
WhiteWalker
, e.g. Jon Snow :) - The timeout 1000 ms is reached, in which case the white walker will die.
Immediately after calling Thread#wait(long)
, the current thread WhiteWalker
changes its thread state, from RUNNABLE to TIMED_WAITING. TIMED_WAITING
means a thread is waiting for another thread to perform an action for up to a
specified time is in this state.
Here’s the implementation:
@Override
public void run() {
System.out.println(name + " is waiting...");
synchronized (king) {
try {
king.wait(1000);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
if (king.isReady.get()) {
System.out.println(name + " follows.");
} else {
System.out.println(name + " is dead (king is not ready).");
}
}
2.2. King
Let’s take a look on class NightKing
. Night king will be ready 100 ms after
having started its preparation. Once ready, he will notify all the white
walkers, the entire army that is waiting for him:
public static class NightKing extends Thread {
AtomicBoolean isReady = new AtomicBoolean(false);
@Override
public void run() {
System.out.println("Night King is preparing...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
ready();
}
...
}
In method ready()
, king uses method Thread#notifyAll()
to notify his army.
It means that king will wake up all threads that are waiting on this object’s
monitor:
private synchronized void ready() {
System.out.println("Night King is ready.");
isReady.set(true);
notifyAll();
}
Notice that in the method above, the keyword synchronized
is necessary to
acquire a monitor of the Night King instance. The threads waiting on this
object’s monitor, they will compete in the usual manner to acquire a lock on the
object’s monitor and resume their execution.
2.3 Source code
package io.mincong.ocpjp.threads;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author Mincong Huang
*/
public class ThreadWait {
/**
* Milliseconds required to let the king be ready.
* <p>
* You can change this value and / or the value of {@link
* #MS_TO_WAIT} to modify the behaviors of white walkers.
*
* @see #MS_TO_WAIT
*/
private static final int MS_TO_READY = 100;
/**
* Milliseconds required to wait the king.
* <p>
* You can change this value and / or the value of {@link
* #MS_TO_READY} to modify the behaviors of white walkers.
*
* @see #MS_TO_READY
*/
private static final int MS_TO_WAIT = 1_000;
public static void main(String... args) {
NightKing king = new NightKing();
List<WhiteWalker> walkers = Arrays.asList(
new WhiteWalker("A", king),
new WhiteWalker("B", king),
new WhiteWalker("C", king)
);
king.start();
walkers.forEach(Thread::start);
}
public static class WhiteWalker extends Thread {
private String name;
private final NightKing king;
WhiteWalker(String name, NightKing king) {
this.name = name;
this.king = king;
}
@Override
public void run() {
System.out.println(name + " is waiting...");
synchronized (king) {
try {
king.wait(MS_TO_WAIT);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
if (king.isReady.get()) {
System.out.println(name + " follows.");
} else {
System.out.println(name + " is dead (king is not ready).");
}
}
@Override
public String toString() {
return name;
}
}
public static class NightKing extends Thread {
AtomicBoolean isReady = new AtomicBoolean(false);
@Override
public void run() {
System.out.println("Night King is preparing...");
try {
Thread.sleep(MS_TO_READY);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
ready();
}
private synchronized void ready() {
System.out.println("Night King is ready.");
isReady.set(true);
notifyAll();
}
}
}
3. Method join()
A thread might need to pause its own execution when it is waiting for another
thread to complete its task. If thread A calls join()
on a thread instance B,
A will wait for B to complete its execution before A can proceed to its own
completion. Now let’s see how “join” can change the white walker army’s
behaviors.
3.1 White Walker
Comparing to the previous approach in section 2, where we need to acquire a monitor of the Night King, now using “join”, we can simply describe the workflow as: “Any white walker following the Night King will join him when he’s ready.”
public void run() {
try {
System.out.println(name + " is waiting...");
king.join(1000);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
if (king.isReady.get()) {
System.out.println(name + " follows.");
} else {
System.out.println(name + " is dead (king is not ready).");
}
}
When a white walker calls king.join()
, the thread of white walker waits for
the king to complete its execution before executing the rest of its code. Method
join()
guarantees that the calling thread won’t execute its remaining code
until the thread on which it calls join()
completes. Behind the scenes,
join()
is implemented using methods wait()
, isAlive()
, and notifyAll()
.
3.2 Night King
This approach also simplifies the logic for the Night King, who does not need to notify all the threads anymore:
public void run() {
System.out.println("Night King is preparing...");
try {
Thread.sleep(MS_TO_READY);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("Night King is ready.");
isReady.set(true);
}
However, we must understand that this approach is not the same as the notify-all
approach. Using method join()
requires the execution of target thread (king)
to be complete, before the army can resume their execution. While using
notify-all, you can let king to notify all in middle of his execution, then
resume after that.
3.3 Source Code
package io.mincong.ocpjp.threads;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author Mincong Huang
*/
public class ThreadJoin {
/**
* Milliseconds required to let the king be ready.
* <p>
* You can change this value and / or the value of {@link
* #MS_TO_WAIT} to modify the behaviors of white walkers.
*
* @see #MS_TO_WAIT
*/
private static final int MS_TO_READY = 100;
/**
* Milliseconds required to wait the king.
* <p>
* You can change this value and / or the value of {@link
* #MS_TO_READY} to modify the behaviors of white walkers.
*
* @see #MS_TO_READY
*/
private static final int MS_TO_WAIT = 1_000;
public static void main(String... args) {
NightKing king = new NightKing();
List<WhiteWalker> walkers = Arrays.asList(
new WhiteWalker("A", king),
new WhiteWalker("B", king),
new WhiteWalker("C", king)
);
king.start();
walkers.forEach(Thread::start);
}
public static class WhiteWalker extends Thread {
private String name;
private final NightKing king;
WhiteWalker(String name, NightKing king) {
this.name = name;
this.king = king;
}
@Override
public void run() {
try {
System.out.println(name + " is waiting...");
king.join(MS_TO_WAIT);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
if (king.isReady.get()) {
System.out.println(name + " follows.");
} else {
System.out.println(name + " is dead (king is not ready).");
}
}
@Override
public String toString() {
return name;
}
}
public static class NightKing extends Thread {
AtomicBoolean isReady = new AtomicBoolean(false);
@Override
public void run() {
System.out.println("Night King is preparing...");
try {
Thread.sleep(MS_TO_READY);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
System.out.println("Night King is ready.");
isReady.set(true);
}
}
}
I’m not 100% about these two approaches. If you found some wrong in this blog post, please don’t hesitate to leave me a comment. Hope you enjoy this one, see you the next time!