Last mod: 2024.12.03

Java threads - Runnable and Thread

Java threads are a fundamental concept in concurrent programming, though modern frameworks often handle them automatically. While threads are not commonly used directly in everyday development, there are scenarios—especially in high-performance and specialized applications where understanding and managing threads becomes essential. Gaining control over threads can help optimize tasks, improve application responsiveness, and achieve faster execution when required.

Multithreading from Java 1.0

I will start the description of multithreading support with the mechniques available in the first versions of Java.

Inheriting from Thread

We can define our own thread by inheriting from the Thread class:

public class ExampleThread extends Thread {
   @Override
   public void run() {
	// Logic executed in a new thread.
   }
}

Starting ExampleThread:

new ExampleThread().start();
Implementation of the Runnable interface

The second option for defining your own thread is to implement the Runnable interface.

public class ExampleRunnable implements Runnable {
    @Override
    public void run() {
        // Logic executed in a new thread.
    }
}

Starting ExampleThread:

new Thread(new ExampleRunnable()).start();

How to stop my own thread

The Thread class has a stop() method for stopping a thread. Currently, the use of this method directly is not recommended and is marked as @Deprecated.

Using stop() method and internal flag

The first solution is to use an internal flag. Definition class with internal flag running:

class ExampleRunnable implements Runnable {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            try {
                // Logic divided into parts executed in this loop.
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // Restore interrupt flag            
                break;
            }
        }
    }

    public void stop() {
        running = false;
    }
}

Start and stop thread:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ExampleRunnable();
        thread.start();     // Start thread
        Thread.sleep(1000); // Thread activity for 3 seconds.
        thread.stop();      // Stop thread
    }
}
Using interruption handling
class ExampleRunnable implements Runnable {
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // Logic divided into parts executed in this loop.
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // Restore interrupt flag
                break;
            }
        }
    }
}

Start and stop thread:

public class ThreadInterruptExample {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ExampleRunnable());
        thread.start();
        Thread.sleep(1000);  // Thread activity for 3 seconds.
        thread.interrupt();  // Interrupt (stop) thread
    }
}
Thread control by methods: wait(), notify(), notifyAll()

The producer-consumer problem is a classic synchronization problem that involves two types of threads: a producer that generates data and a consumer that processes the data. The challenge is to ensure the producer doesn't produce more data than the consumer can handle, and the consumer doesn't consume data that doesn't exist.

In Java, we can solve this problem using the wait(), notify(), and notifyAll() methods provided by the Object class. These methods allow threads to communicate with each other by using a shared object as a lock.

Here we have three methods defined in the Object class:

  • wait() - a thread calls this method to release the lock on the shared object and enters the waiting state until another thread calls notify() or notifyAll() on the same object
  • notify() - wakes up a single thread waiting on the shared object's monitor
  • notifyAll() - wakes up all threads waiting on the shared object's monitor

Implementation outline:

  • shared resource - Use a shared data structure (e.g., a queue or buffer) to hold items produced by the producer and consumed by the consumer.
  • producer thread - Adds items to the buffer. If the buffer is full, it waits until the consumer consumes some items.
  • consumer thread - Removes items from the buffer. If the buffer is empty, it waits until the producer produces more items.
  • synchronization - Both threads synchronize on the shared object to prevent race conditions and manage the buffer's state.

Implementation producer-consumer:

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumer {
    private final Queue<Integer> buffer = new LinkedList<>();
    private final int MAX_SIZE = 5;

    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        Thread producer = new Thread(pc::produce, "Producer");
        Thread consumer = new Thread(pc::consume, "Consumer");

        producer.start();
        consumer.start();
    }

    public void produce() {
        int value = 0;
        while (true) {
            synchronized (this) {
                while (buffer.size() == MAX_SIZE) {
                    try {
                        wait(); // Wait for the consumer to consume
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                buffer.add(value);
                System.out.println("Produced: " + value++);
                notify(); // Notify the consumer
            }
        }
    }

    public void consume() {
        while (true) {
            synchronized (this) {
                while (buffer.isEmpty()) {
                    try {
                        wait(); // Wait for the producer to produce
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                int value = buffer.poll();
                System.out.println("Consumed: " + value);
                notify(); // Notify the producer
            }
        }
    }
}
Other methods

Remaining methods that need to be explained are:

  • sleep(..) - Pauses the execution of the current thread for a specified time in milliseconds.
Thread.sleep(1000); // Sleep for 1 second
  • join() - Waits for a thread to finish execution.
Thread thread = new Thread(() -> { /* tasks */ });
thread.start();
thread.join(); // Wait for finish
  • interrupt() - Interrupts a thread that is in a sleeping or waiting state.
thread.interrupt();
  • yield() - Hints to the thread scheduler that the current thread is willing to yield its current.
Thread.yield();
  • setPriority(..) - As the name suggests setting priority of thread
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
  • setDaemon(..) - method is used to mark a thread as either a daemon thread or a user thread.
thread.setDaemon(true);

To be continued...