Java Multithreading Example Tutorial

Introduction

Multithreading in Java allows concurrent execution of two or more parts of a program to maximize CPU utilization. Each part of such a program is called a thread. Java provides built-in support for multithreaded programming through its Thread class and the Runnable interface.

Table of Contents

  1. Basics of Multithreading
  2. Creating Threads
    • Extending the Thread Class
    • Implementing the Runnable Interface
  3. Thread Lifecycle
  4. Thread Methods
  5. Synchronization
  6. Inter-Thread Communication
  7. Thread Pools
  8. Example Programs
  9. Conclusion

1. Basics of Multithreading

Multithreading allows multiple threads to run concurrently. Each thread runs a separate path of execution. Java's built-in Thread class and Runnable interface make it easy to implement multithreading.

Benefits of Multithreading:

  • Improved performance and responsiveness.
  • Efficient utilization of CPU resources.
  • Simplified modeling of real-world systems.

2. Creating Threads

Extending the Thread Class

To create a thread by extending the Thread class, you need to:

  1. Create a new class that extends Thread.
  2. Override the run method with the code you want the thread to execute.
  3. Create an instance of the new class and call the start method.

Example:

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running. Count: " + i);
            try {
                Thread.sleep(1000);  // Simulate some work with sleep
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        thread1.start();
        thread2.start();
    }
}

Implementing the Runnable Interface

To create a thread by implementing the Runnable interface, you need to:

  1. Create a new class that implements Runnable.
  2. Override the run method with the code you want the thread to execute.
  3. Create an instance of Thread class, passing the Runnable object to the Thread constructor.
  4. Call the start method.

Example:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running. Count: " + i);
            try {
                Thread.sleep(1000);  // Simulate some work with sleep
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread1 = new Thread(myRunnable);
        Thread thread2 = new Thread(myRunnable);

        thread1.start();
        thread2.start();
    }
}

3. Thread Lifecycle

A thread in Java goes through various states in its lifecycle:

  1. New: A thread that has been created but not yet started.
  2. Runnable: A thread that is ready to run and waiting for CPU time.
  3. Running: A thread that is executing.
  4. Blocked: A thread that is blocked waiting for a monitor lock.
  5. Waiting: A thread that is waiting indefinitely for another thread to perform a particular action.
  6. Timed Waiting: A thread that is waiting for another thread to perform a particular action within a stipulated amount of time.
  7. Terminated: A thread that has exited.

4. Thread Methods

start()

Starts the execution of the thread.

Example:

thread.start();

run()

If a thread was constructed using a separate Runnable object, then that Runnable object's run method is called; otherwise, this method does nothing and returns.

Example:

public void run() {
    // code to be executed
}

sleep(long millis)

Puts the currently executing thread to sleep for the specified number of milliseconds.

Example:

Thread.sleep(1000); // Sleeps for 1 second

interrupt()

Interrupts a thread that is in the sleeping, waiting, or blocked state.

Example:

thread.interrupt();

join()

Waits for the thread to die.

Example:

thread.join();

isAlive()

Tests if the thread is alive.

Example:

boolean alive = thread.isAlive();

getName() and setName(String name)

Gets or sets the name of the thread.

Example:

String name = thread.getName();
thread.setName("New Thread Name");

getPriority() and setPriority(int priority)

Gets or sets the priority of the thread.

Example:

int priority = thread.getPriority();
thread.setPriority(Thread.MAX_PRIORITY);

getState()

Returns the state of the thread.

Example:

Thread.State state = thread.getState();

5. Synchronization

Synchronization is used to control the access of multiple threads to shared resources. It prevents data inconsistency and race conditions.

Synchronized Methods

A synchronized method ensures that only one thread can execute it at a time for a particular object.

Example:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

Synchronized Blocks

A synchronized block is a block of code within a method that is synchronized on a specified object, providing more granular control over the synchronization.

Example:

class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

6. Inter-Thread Communication

Inter-thread communication in Java is achieved using wait(), notify(), and notifyAll() methods. These methods are used to synchronize the activities of multiple threads.

Example:

class SharedResource {
    private int data = 0;
    private boolean available = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (available) {
            wait();
        }
        data = value;
        available = true;
        notify();
    }

    public synchronized int consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        notify();
        return data;
    }
}

public class InterThreadCommunicationExample {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();

        Thread producer = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    sharedResource.produce(i);
                    System.out.println("Produced: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    int value = sharedResource.consume();
                    System.out.println("Consumed: " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

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

7. Thread Pools

Thread pooling is a technique to reuse a fixed number of threads to execute tasks, which improves the performance of a multithreaded application.

Using ExecutorService

Example:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolingExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000); // Simulate a long-running task
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executorService.shutdown();
    }
}

Using Executors Factory Methods

  • newFixedThreadPool(int nThreads): Creates a thread pool with a fixed number of threads.
  • newCachedThreadPool(): Creates a thread pool that creates new threads as needed but will reuse previously constructed threads when they are available.
  • newSingleThreadExecutor(): Creates an executor that uses a single worker thread.
  • newScheduledThreadPool(int corePoolSize): Creates a thread pool that can schedule commands to run after a given delay or to execute periodically.

8. Example Programs

Example 1: Creating Multiple Threads

Example:

class MyRunnable implements Runnable {
    private final int threadNumber;

    public MyRunnable(int threadNumber) {
        this.threadNumber = threadNumber;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread " + threadNumber + " is running. Count: " + i);
            try {
                Thread.sleep(1000);  // Simulate some work with sleep
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class MultiThreadExample {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            Thread thread = new Thread(new MyRunnable(i));
            thread.start();
        }
    }
}

Example 2: Synchronizing Threads

Example:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizationExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

Example 3: Inter-Thread Communication

Example:

class SharedResource {
    private int data = 0;
    private boolean available = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (available) {
            wait();
        }
        data = value;
        available = true;
        notify();
    }

    public synchronized int consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        notify();
        return data;
    }
}

public class InterThreadCommunicationExample {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();

        Thread producer = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    sharedResource.produce(i);
                    System.out.println("Produced: " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    int value = sharedResource.consume();
                    System.out.println("Consumed: " + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

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

9. Conclusion

Multithreading in Java is a powerful feature that allows for concurrent execution of tasks, improving performance and responsiveness of applications. By understanding the basics of thread creation, lifecycle, synchronization, inter-thread communication, and thread pooling, you can effectively utilize multithreading in your Java programs. The examples provided demonstrate various aspects of multithreading and serve as a foundation for more advanced multithreaded applications.

Happy coding!

Comments