18. Multithreading
Programming Project 2021/22

18.5. Thread Signaling

Consider the following scenario.

  • Thread A produces some data.
  • Thread B processes the data generated by Thread A.

To make sure our program works correctly, we need to make sure that Thread B waits for Thread A to finish its job.

  • If it is a one time thing, we can invoke join() on Thread A.
  • If it is not, we need some kind of notification mechanism.

Busy wait

Busy wait is a technique in which a process repeatedly checks a given condition to proceed with its execution.

Busy wait

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();
      }
   }
}

Busy wait: Limitations

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.

  • Thread A calls wait() on the shared signaling object, which blocks thread A.
  • Thread B calls 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.

Example

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();
   }
}

Revisiting the busy wait program

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();
	}
}

Deadlock

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

Deadlock example

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'

Exercise 4

  • Generate a deadlock situation.

  • Two threads representing transactions

    • T1: father gives 100 euro to son
    • T2: son gives 20 euro to father
  • Two shared objects of the Person class.

    • father
    • son
    Thread Tran1:  locks father, waits for son
    Thread Tran2:  locks son, waits for father

Preventing deadlocks

There are several approaches, we will cover:

  • Lock Ordering
  • Lock Timeout

Lock ordering

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

Lock timeout

  • Put a timeout on lock attempts.

    • A thread trying to obtain a lock will only try for so long before giving up
    • If a thread does not succeed in taking all necessary locks within the given timeout, it will
      • back up,
      • free all locks taken,
      • and possibly wait for a random amount of time and then retry.
    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

Lock timeout: Example

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();
      }
   }
}

Further multithreading/synchronization topics

We have covered only the basic of multithreading and the issues that arise.

There are other important topics, such as:

  • Locks
  • Semaphores
  • Starvation
  • Fairness
  • Reentrance Lockout

References

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.