Consider the following scenario.
To make sure our program works correctly, we need to make sure that Thread B waits for Thread A to finish its job.
join()
on Thread A.Busy wait is a technique in which a process repeatedly checks a given condition to proceed with its execution.
public class CharacterData {
protected String[] characters = {};
public synchronized String[] getCharacters() {
return this.characters;
}
public synchronized void setCharacters(String[] characters) {
this.characters = characters;
}
}
import java.util.Random;
public class CharacterGenerator implements Runnable {
CharacterData m;
Random rand = new Random();
@Override
public void run() {
try {
while (true) {
if (m.getCharacters().length == 0) {
int sleepTime = rand.nextInt(10000);
System.out.printf("Generator: I need to create some characters... (%d)%n", sleepTime);
Thread.sleep(sleepTime);
String[] newCharacters = {"Rick", "Morty", "Summer", "Beth"};
System.out.println("Generator: My characters are now available!");
m.setCharacters(newCharacters);
} else {
System.out.printf("Generator: Characters are available. I'll check again later.%n");
}
Thread.sleep(2000);
}
} catch (InterruptedException e1) {
e1.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
import java.util.Arrays;
public class CharacterPrinter implements Runnable {
CharacterData m;
@Override
public void run() {
try {
while (true) {
if (m.getCharacters().length == 0) {
System.out.println("Printer: Nothing for me to print, I'll try again later.");
} else {
System.out.println("Printer: I found some characters: " + Arrays.toString(m.getCharacters()));
String[] emptyArray = {};
m.setCharacters(emptyArray);
}
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
This is not very efficient because it consumes a lot of CPU time.
A better solution would be for the CharacterPrinter
thread to wait until the CharacterGenerator
creates the data for it.
wait()
, notify()
and notifyAll()
Java has a builtin wait mechanism that enables threads to become inactive while waiting for signals.
java.lang.Object
defines three methods.
wait()
notify()
notifyAll()
To implement this mechanism, we also need a shared signaling object. This is the usual flow.
wait()
on the shared signaling object, which blocks thread A.notify()
on the shared signaling object, which unblocks thread A.Note that notify()
wakes up only one thread waiting on the object!
And the choice of which one to wake up is not under our control.
If we know that several threads may be blocked and we want to wake up all of them together,
we can call notifyAll()
instead.
Note that a thread cannot call wait()
, notify()
or notifyAll()
without holding the lock on the object the method is called on.
Let us demonstrate these methods with an example:
class T1 implements Runnable {
Object signal;
@Override
public void run() {
System.out.println("T1: Going to wait");
synchronized (signal) {
try {
signal.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("T1: I am back");
}
}
class T2 implements Runnable {
Object signal;
@Override
public void run() {
System.out.println("T2: I'll sleep for two seconds and then notify the other thread");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (signal) {
signal.notify();
System.out.println("T2: I notified the other thread");
}
}
}
public class Runner {
public static void main(String[] args) {
Object signal = new Object();
T1 t1 = new T1();
t1.signal = signal;
T2 t2 = new T2();
t2.signal = signal;
Thread th1 = new Thread(t1);
Thread th2 = new Thread(t2);
th1.start();
th2.start();
}
}
public class CharacterData {
protected String[] characters = {};
public synchronized String[] getCharacters() {
return this.characters;
}
public synchronized void setCharacters(String[] characters) {
this.characters = characters;
}
}
import java.util.Random;
public class CharacterGenerator implements Runnable {
CharacterData m;
private Random rand = new Random();
private final String[] characters = {"Rick", "Morty", "Summer", "Beth"};
@Override
public void run() {
try {
while (true) {
synchronized (m){
if (m.getCharacters().length == 0) {
int sleepTime = rand.nextInt(10000);
System.out.printf("Generator: I need to create some characters... (%d)%n", sleepTime);
Thread.sleep(sleepTime);
m.setCharacters(characters);
System.out.println("Generator: My characters are ready to be printed!");
m.notify();
} else {
System.out.printf("Generator: Characters already available. I'll wait until someone notifies me.%n");
m.wait();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
import java.util.Arrays;
public class CharacterPrinter implements Runnable {
CharacterData m;
@Override
public void run() {
try {
while (true) {
synchronized (m) {
if (m.getCharacters().length == 0) {
System.out.println("Printer: Nothing for me to print. I'll wait until someone notifies me.");
m.wait();
} else {
System.out.println("Printer: I found some characters: " + Arrays.toString(m.getCharacters()));
String[] emptyArray = {};
m.setCharacters(emptyArray);
m.notify();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class WaitNotifyRunner {
public static void main(String[] args) {
CharacterData m = new CharacterData();
CharacterGenerator producer = new CharacterGenerator();
producer.m = m;
CharacterPrinter processor = new CharacterPrinter();
processor.m = m;
Thread th1 = new Thread(producer);
Thread th2 = new Thread(processor);
th1.start();
th2.start();
}
}
A deadlock is a situation where two or more threads are blocked forever, waiting for each other.
Thread 1 locks A, waits for B
Thread 2 locks B, waits for A
Thread 1 locks A, waits for B
Thread 2 locks B, waits for C
Thread 3 locks C, waits for D
Thread 4 locks D, waits for A
class Task1 implements Runnable {
Object a;
Object b;
public Task1(Object a, Object b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (a) {
System.out.println("Task1: Acquired lock " +
"of 'Object a'");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("Task1: Acquired lock " +
"of 'Object b'");
}
}
}
}
class Task2 implements Runnable {
Object a;
Object b;
public Task2(Object a, Object b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (b) {
System.out.println("Task2: Acquired lock " +
"of 'Object b'");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println("Task2: Acquired lock " +
"of 'Object a'");
}
}
}
}
public class DeadlockBasic {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
Thread th1 = new Thread(new Task1(a, b));
Thread th2 = new Thread(new Task2(a, b));
th1.start();
th2.start();
}
}
The program will likely print this and stay in a deadlock.
Task1: Acquired lock of 'Object a'
Task2: Acquired lock of 'Object b'
Generate a deadlock situation.
Two threads representing transactions
Two shared objects of the Person
class.
Thread Tran1: locks father, waits for son
Thread Tran2: locks son, waits for father
There are several approaches, we will cover:
Deadlock occurs when multiple threads need the same locks but obtain them in different order, so need to make sure that all locks are always taken in the same order by any thread.
Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
wait for A
wait for B
wait for C
Put a timeout on lock attempts.
Task1: Acquired lock A
Task2: Acquired lock B
Task2: Lock attempt on A timed out
Task2: Released lock B
Task1: Acquired lock B
Task1: Released lock B
Task1: Released lock A
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockBasicTimeout {
public static void main(String[] args) {
Lock a = new ReentrantLock();
Lock b = new ReentrantLock();
Thread th1 = new Thread(new Task1(a, b));
Thread th2 = new Thread(new Task2(a, b));
th1.start();
th2.start();
}
}
class Task1 implements Runnable {
Lock a;
Lock b;
public Task1(Lock a, Lock b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
try {
if (a.tryLock(8000, TimeUnit.MILLISECONDS)) {
System.out.println("Task1: Acquired lock A");
Thread.sleep(2000);
if (b.tryLock(8000, TimeUnit.MILLISECONDS)) {
System.out.println("Task1: Acquired lock B");
Thread.sleep(2000);
b.unlock();
System.out.println("Task1: Released lock B");
} else {
System.out.println("Task1: Lock attempt on B timed out");
}
Thread.sleep(2000);
a.unlock();
System.out.println("Task1: Released lock A");
} else {
System.out.println("Task1: Lock attempt on A timed out");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Task2 implements Runnable {
Lock a;
Lock b;
public Task2(Lock a, Lock b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
try {
if (b.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("Task2: Acquired lock B");
Thread.sleep(2000);
if (a.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("Task2: Acquired lock A");
Thread.sleep(2000);
a.unlock();
System.out.println("Task2: Released lock A");
} else {
System.out.println("Task2: Lock attempt on A timed out");
}
b.unlock();
System.out.println("Task2: Released lock B");
} else {
System.out.println("Task2: Lock attempt on B timed out");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
We have covered only the basic of multithreading and the issues that arise.
There are other important topics, such as:
Part of the material has been taken from the following sources. The usage of the referenced copyrighted work is in line with fair use since it is for nonprofit educational purposes.