CS F213 Objected Oriented Programming Labsheet 7

Spring 2024

Java Threads

Threads allows a program to operate more efficiently by doing multiple things at the same time. Threads can be used to perform complicated tasks in the background without interrupting the main program.

Threads exist in several states. Here is a general description. A thread can be running. It can be ready to run as soon as it gets CPU time. A running thread can be suspended, which temporarily halts its activity. A suspended thread can then be resumed, allowing it to pick up where it left off. A thread can be blocked when waiting for a resource. At any time, a thread can be terminated, which halts its execution immediately. Once terminated, a thread cannot be resumed.

Creating a Thread

There are two ways to create a thread.

  1. It can be created by extending the Thread class and overriding its run() method:

public class Main extends Thread {
  public void run() {
    System.out.println("This code is running in a thread");
  }
}
  1. Another way to create a thread is to implement the Runnable interface:

public class Main implements Runnable {
  public void run() {
    System.out.println("This code is running in a thread");
  }
}

Inside run( ), you will define the code that constitutes the new thread. It is important to understand that run( ) can call other methods, use other classes, and declare variables, just like the main thread can. The only difference is that run( ) establishes the entry point for another, concurrent thread of execution within your program. This thread will end when run( ) returns.

Running Threads

  1. If the class extends the Thread class, the thread can be run by creating an instance of the class and call its start() method:

public class Main extends Thread {
  public static void main(String[] args) {
    Main thread = new Main();
    thread.start();
    System.out.println("This code is outside of the thread");
  }
  public void run() {
    System.out.println("This code is running in a thread");
  }
}
  1. If the class implements the Runnable interface, the thread can be run by passing an instance of the class to a Thread object's constructor and then calling the thread's start() method:

public class Main implements Runnable {
  public static void main(String[] args) {
    Main obj = new Main();
    Thread thread = new Thread(obj);
    thread.start();
    System.out.println("This code is outside of the thread");
  }
  public void run() {
    System.out.println("This code is running in a thread");
  }
}

Output

The output of this code is non-deterministic due to the concurrent execution of the main thread and the new thread created by extending the Thread class. The two threads run independently, and the order in which they execute their statements is not guaranteed.

If the main thread gets executed before the new thread

This code is outside of the thread
This code is running in a thread

If the new thread gets executed before the main thread

This code is running in a thread
This code is outside of the thread

The actual order of execution depends on the thread scheduler and how the operating system manages the execution of threads. Therefore, you might observe either of the two possible outcomes


Differences between "extending" and "implementing" Threads The major difference is that when a class extends the Thread class, you cannot extend any other class, but by implementing the Runnable interface, it is possible to extend from another class as well, like: class MyClass extends OtherClass implements Runnable.


Concurrency problems

Because threads run at the same time as other parts of the program, there is no way to know in which order the code will run. When the threads and main program are reading and writing the same variables, the values are unpredictable. The problems that result from this are called concurrency problems.

Often you will want the main thread to finish last. This can be accomplished by calling sleep() within main(), with a long enough delay to ensure that all child threads terminate prior to the main thread. However, this is hardly a satisfactory solution, and it also raises a larger question: How can one thread know when another thread has ended? Two ways exist to determine whether a thread has finished.

isAlive() and join()

The isAlive() method returns true if the thread upon which it is called is still running. It returns false otherwise.

The join() method waits until the thread on which it is called terminates. Its name comes from the concept of the calling thread waiting until the specified thread joins it. Additional forms of join() allow you to specify a maximum amount of time that you want to wait for the specified thread to terminate

public class Main extends Thread {
  public static int amount = 0;

  public static void main(String[] args) {
    Main thread = new Main();
    thread.start();
    // Wait for the thread to finish
    while(thread.isAlive()) {
    System.out.println("Waiting...");
  }
  // Update amount and print its value
  System.out.println("Main: " + amount);
  amount++;
  System.out.println("Main: " + amount);
  }
  public void run() {
    amount++;
  }
}

Output

Waiting...
Waiting...
Main: 1
Main: 2
public class Main extends Thread {
    public static int amount = 0;

    public static void main(String[] args) {
        Main thread = new Main();
        thread.start();

        // Wait for the thread to finish
        thread.join();
        System.out.println(" New Thread has finished.");

        // Update amount and print its value
        System.out.println("Main: " + amount);
        amount++;
        System.out.println("Main: " + amount);
    }

    public void run() {
        amount++;
    }
}

Output

Thread has finished.
Main: 1
Main: 2

Synchronization

Multi-threaded programs may often come to a situation where multiple threads try to access the same resources and finally produce erroneous and unforeseen results. Java Synchronization is used to make sure by some synchronization method that only one thread can access the resource at a given point in time.

Java provides a way of creating threads and synchronizing their tasks using synchronized blocks. A synchronized block in Java is synchronized on some object. All synchronized blocks synchronize on the same object and can only have one thread executed inside them at a time. All other threads attempting to enter the synchronized block are blocked until the thread inside the synchronized block exits the block.

synchronized(sync_object)
{
   // Access shared variables and other
   // shared resources
}

Without Synchronized

class PrintDemo {
   public void printCount() {
    for(int i = 5; i > 0; i--) {
                System.out.println("Counter   ---   "  + i );
             }
          }
    }
class ThreadDemo extends Thread {
   private Thread t;
   private String threadName;
   PrintDemo  PD;
   ThreadDemo( String name,  PrintDemo pd) {
      threadName = name;
      PD = pd;
   
   public void run() {
      PD.printCount();
      System.out.println("Thread " +  threadName + " exiting.");
   }
}
class Main {
   public static void main(String args[]) {
      PrintDemo PD = new PrintDemo();
      ThreadDemo T1 = new ThreadDemo( "Thread - 1 ", PD );
      ThreadDemo T2 = new ThreadDemo( "Thread - 2 ", PD );
      T1.start();
      T2.start();
      try {
         T1.join();
         T2.join();
      } catch ( Exception e) {
         System.out.println("Interrupted");
      }
   }
}

This may or may not print counter value in sequence and every time we run it, it produces a different result based on CPU availability to a thread

Counter   ---   5
Counter   ---   4
Counter   ---   5
Counter   ---   3
Counter   ---   4
Counter   ---   2
Counter   ---   3
Counter   ---   1
Counter   ---   2
Counter   ---   1
Thread Thread - 2  exiting.
Thread Thread - 1  exiting.

If we change the run() method and synchronize it like this

public void run() {
      synchronized(PD) {
        PD.printCount();
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }

Then the output becomes

Counter   ---   5
Counter   ---   4
Counter   ---   3
Counter   ---   2
Counter   ---   1
Counter   ---   5
Counter   ---   4
Counter   ---   3
Counter   ---   2
Counter   ---   1
Thread Thread - 2  exiting.
Thread Thread - 1  exiting.

This produces the same result every time you run this program

Interthread Communication

Java includes an elegant interprocess communication mechanism via the wait(), notify(), and notifyAll() methods. These methods are implemented as final methods in Object, so all classes have them. All three methods can be called only from within a synchronized context.

  • wait() tells the calling thread to give up the monitor and go to sleep until some other thread enters the same monitor and calls notify() or notifyAll()

  • notify() wakes up a thread that called wait( ) on the same object.

  • notifyAll() wakes up all the threads that called wait( ) on the same object. One of the threads will be granted access.

These methods are declared within Object, as shown here:

final void wait() throws InterruptedException final void notify() final void notify All()

class demo {
	volatile boolean part1done = false;
	synchronized void part1()
	{
		System.out.println("Welcome to India");
		part1done = true;
		System.out.println(
			"Thread t1 about to surrender lock");
		notify();
	}
	synchronized void part2()
	{
		while (!part1done) {
			try {
				System.out.println("Thread t2 waiting");
				wait(); // wait till notify is called
				System.out.println(
					"Thread t2 running again");
			}
			catch (Exception e) {
				System.out.println(e.getClass()); }
		}
		System.out.println("Do visit Taj Mahal");
	}
}
public class Main {
	public static void main(String[] args)
	{
		demo obj = new demo();
		Thread t1 = new Thread(new Runnable() {
			public void run() { obj.part1(); }
		});
		Thread t2 = new Thread(new Runnable() {
			public void run() { obj.part2(); }
		});
		t2.start(); // Start t2 and then t1
		t1.start();
	}
}

Output

Thread t2 waiting
Welcome to India
Thread t1 about to surrender lock
Thread t2 running again
Do visit Taj Mahal

Obtaining a Thread’s State

A thread can exist in a number of different states. You can obtain the current state of a thread by calling the getState() method defined by Thread.

Thread.State getState()

It returns a value of type Thread.State that indicates the state of the thread at the time at which the call was made.

Values that can be returned by getState( ):

  • BLOCKED : A thread that has suspended execution because it is waiting to acquire a lock

  • NEW : A thread that has not begun execution.

  • RUNNABLE : A thread that either is currently executing or will execute when it gains access to the CPU.

  • TERMINATED : A thread that has completed execution.

  • TIMED_WAITING : A thread that has suspended execution for a specified period of time, such as when it has called sleep(). This state is also entered when a timeout version of wait( ) or join( ) is called.

  • WAITING : A thread that has suspended execution because it is waiting for some action to occur. For example, it is waiting because of a call to a nontimeout version of wait( ) or join( ).

getState( ) is not intended to provide a means of synchronizing threads. It’s primarily used for debugging or for profiling a thread’s run-time characteristics.

Thead Priority

In Java, each thread is assigned a priority which affects the order in which it is scheduled for running. The threads are of the same priority by the Java Scheduler and therefore they share the processor on a First-Come-First-Serve basis.

Java permits us to set the priority of a thread using the setPriority() as follows:

Thread-Name.setPriority(intNumber);

Where intNumber is an integer value to which the thread’s priority is set. The Thread class defines several priority constants:

MIN_PRIORITY = 1 NORM_PRIORITY = 5 MAX_PRIORITY = 10

Summary of Java Threads Methods

Modifier and Type
Method
Description

void

start()

It is used to start the execution of the thread.

void

run()

It is used to perform the main action for a thread.

static void

sleep()

It sleeps a thread for the specified amount of time.

static Thread

currentThread()

It returns a reference to the currently executing thread object.

void

join()

It waits for a thread to die.

int

getPriority()

It returns the priority of the thread.

void

setPriority()

It changes the priority of the thread.

String

getName()

It returns the name of the thread.

void

setName()

It changes the name of the thread.

Thread.State

getState()

It is used to return the state of the thread.

void

notify()

It is used to give the notification for only one thread which is waiting for a particular object.

void

notifyAll()

It is used to give the notification to all waiting threads of a particular object.

Exercises

Exercise 1

Create a program that simulates a shared resource among five threads. The resource is a simple counter that starts from 0. Each thread should increment the counter 1000 times. Use synchronization to ensure that the counter value is accurate, and print the final value of the counter.

Sample Output

Thread 1: Incrementing counter...
Thread 2: Incrementing counter...
Thread 3: Incrementing counter...
Thread 4: Incrementing counter...
Thread 5: Incrementing counter...
Counter Value: 5000

you can use Thread.currentThread().getId() to get the number of the currenly executed thread

Exercise 2

You are tasked with implementing a simple producer-consumer system using a shared array-based queue. The system should support multiple producers and multiple consumers. Each producer will produce integers sequentially (starting from 1), and consumers will consume these integers. The system should have a buffer (queue) of a specified size, and the producers and consumers should synchronize their access to the shared buffer.

Requirements:

  1. Implement a SharedQueue class representing the shared buffer with the following methods:

    • produce(int producerId): Produces integers sequentially (starting from 1) and adds them to the buffer. If the buffer is full, the producer should wait for space to become available.

    • consume(int consumerId): Consumes integers from the buffer. If the buffer is empty, the consumer should wait for items to be produced.

  2. Implement a Producer class that implements the Runnable interface. Each producer should have a unique ID and continuously produce integers using the produce method of the shared buffer.

  3. Implement a Consumer class that also implements the Runnable interface. Each consumer should have a unique ID and continuously consume integers using the consume method of the shared buffer.

  4. In the main method of the ProducerConsumerExample class:

    • Create an instance of SharedQueue with a buffer size of 5.

    • Create three producers and three consumers, each with unique IDs.

    • Start the producer and consumer threads.

Constraints:

  • The shared buffer (queue) should be implemented using a circular array.

  • The buffer size should be fixed at 5.

  • The producer and consumer threads should run indefinitely.

Note:

  • Use synchronization mechanisms to ensure mutual exclusion and coordination between producers and consumers.

  • The output should showcase the sequential production and consumption of integers by producers and consumers.

Sample Output

Producer 1 produced: 1
Consumer 2 consumed: 1
Producer 3 produced: 2
Consumer 1 consumed: 2
Producer 2 produced: 3
Consumer 3 consumed: 3
...

The template is given below for your convenience (You have to understand the whole code and complete the 11 commented parts to get the required output)

class SharedQueue {
    private final int[] queue;
    private int size;
    private int front;
    private int rear;

    public SharedQueue(int capacity) {
        this.queue = new int[capacity];
        this.size = 0;
        this.front = 0;
        this.rear = -1;
    }

    public synchronized void produce(int producerId) throws InterruptedException {
        while (size == queue.length) {
            // 1) Queue is full, wait for consumers to consume
        }

        int value = producerId; 
        size++;
        rear = (rear + 1) % queue.length;
        queue[rear] = value;

        System.out.println("Producer " + producerId + " produced: " + value);

        // 2) Notify consumers that there are items to consume
    }

    public synchronized int consume(int consumerId) throws InterruptedException {
        while (size == 0) {
            // 3) Queue is empty, wait for producers to produce
        }

        int value = queue[front];
        size--;
        front = (front + 1) % queue.length;

        System.out.println("Consumer " + consumerId + " consumed: " + value);

        //4) Notify producers that there is space to produce
        return value;
    }
}

class Producer implements Runnable {
    private final SharedQueue sharedQueue;
    private final int id;

    public Producer(SharedQueue sharedQueue, int id) {
       // 5) Complete
    }

    @Override
    public void run() {
        try {
            // 6) Complete
        } catch (InterruptedException e) {
            // 7) Complete
        }
    }
}

class Consumer implements Runnable {
    private final SharedQueue sharedQueue;
    private final int id;

    public Consumer(SharedQueue sharedQueue, int id) {
        // 8) Complete
    }

    @Override
    public void run() {
        try {
            // 9) Complete
        } catch (InterruptedException e) {
            // 10) Complete
        }
    }

    private void consume(int value) {
    }
}

public class Main {
    public static void main(String[] args) {
        SharedQueue sharedQueue = new SharedQueue(5); 

        // 11) Creating and starting multiple (say 3) producers and consumers
        
    }
}

Exercise 3

You are tasked with creating a simple task scheduler that can execute tasks with different priorities. The program will have two types of tasks: high-priority and low-priority. High-priority tasks should be executed before low-priority tasks. Each task represents a thread in the program.

Create a class named Task that implements the Runnable interface. The Task class should have the following properties:

name: A string representing the name of the task. priority: An integer representing the priority of the task. (1 for high-priority, 2 for low-priority) executionTime: An integer representing the time it takes to execute the task.

Create another class named Main that will manage the execution of tasks. The TaskScheduler class should have the following methods:

addTask(Task task): Adds a task to the scheduler. executeTasks(): Executes the tasks based on their priorities. printTaskStatus(): Prints the status of each task (e.g., running, waiting).

Use synchronized, wait, notify, setPriority, and getState to implement synchronization and priority handling.

The boiler code is given below, you are required to complete the parts given in comments

class Task implements Runnable {
    private final String name;
    private final int priority;
    private final int executionTime;
    private Thread thread;

    public Task(String name, int priority, int executionTime) {
        this.name = name;
        this.priority = priority;
        this.executionTime = executionTime;
    }

    @Override
    public void run() {
        synchronized (this) {
            System.out.println(name + " is running (Priority: " +
                    (priority == 1 ? "High" : "Low") +
                    ", State: " + thread.getState() + ")");
            try {
                // (1) Simulate task execution
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " has completed execution.");
            // (2) Notify waiting threads that the task has completed
        }
    }

    public String getName() {
        return name;
    }

    public int getPriority() {
        return priority;
    }

    public int getExecutionTime() {
        return executionTime;
    }

    public Thread.State getState() {
        return thread.getState();
    }

    public void setThread(Thread thread) {
        this.thread = thread;
    }
}

class Main {
    private Task[] tasks;
    private int numTasks;

    public Main(int capacity) {
        tasks = new Task[capacity];
        numTasks = 0;
    }

    public synchronized void addTask(Task task) {
        tasks[numTasks++] = task;
    }

    public synchronized void executeTasks() {
        System.out.println("Executing tasks...");

        // (3) Sort tasks by priority and then by execution time      

        for (Task task : tasks) {
            executeTask(task);
        }

    }

    private void executeTask(Task task) {
        Thread taskThread = new Thread(task);
        task.setThread(taskThread);
        taskThread.setPriority((task.getPriority() == 1) ? Thread.MAX_PRIORITY : Thread.NORM_PRIORITY);
        // (4) start the taskthread thread
    }

    public synchronized void printTaskStatus() {
        System.out.println("\nTask Status:");
        for (Task task : tasks) {
            System.out.println("Task: " + task.getName() + " Priority: " +
                    (task.getPriority() == 1 ? "High" : "Low") +
                    " State: " + /*(5) use getstate() to get state of task*/);
        }
    }

    public static void main(String[] args) {
        Main scheduler = new Main(4);

        Task task1 = new Task("Task1", 1, 300);
        Task task2 = new Task("Task2", 2, 500);
        Task task3 = new Task("Task3", 1, 200);
        Task task4 = new Task("Task4", 2, 400);

        scheduler.addTask(task1);
        scheduler.addTask(task2);
        scheduler.addTask(task3);
        scheduler.addTask(task4);

        scheduler.executeTasks();
        scheduler.printTaskStatus();
    }
}

Sample Output

Executing tasks...

Task Status:
Task3 is running (Priority: High, State: RUNNABLE)
Task: Task3 Priority: High State: RUNNABLE
Task1 is running (Priority: High, State: RUNNABLE)
Task4 is running (Priority: Low, State: RUNNABLE)
Task2 is running (Priority: Low, State: RUNNABLE)
Task: Task1 Priority: High State: BLOCKED
Task: Task4 Priority: Low State: TIMED_WAITING
Task: Task2 Priority: Low State: TIMED_WAITING
Task3 has completed execution.
Task1 has completed execution.
Task4 has completed execution.
Task2 has completed execution.

Last updated