Mastering Java Multithreading: A Beginner’s Guide

Are you curious about making your Java programs faster and more efficient? Meet Java multithreading! In today’s fast-paced digital world, being able to perform several tasks at once is crucial. Java multithreading allows your programs to multitask, just like when you juggle texting, watching TV, and eating popcorn all at once! But what exactly is multithreading, and why should you care? Whether you’re just starting out with Java or looking to enhance your coding skills, join me as we unravel this fascinating concept. Keep reading, and you’ll soon master the art of multitasking with Java multithreading!

Understanding Threads in Java

In Java, a thread is the smallest unit of execution within a process. It allows a program to perform multiple tasks simultaneously, improving performance and responsiveness. Java provides built-in support for multithreading through the Thread class and Runnable interface.

Thread Lifecycle in Java

A thread in Java goes through several states during its execution:

  1. New: When a thread object is created using Thread or Runnable, but start() has not been called yet.
  2. Runnable: After calling start(), the thread enters the Runnable state, meaning it is ready to run but is waiting for CPU time.
  3. Blocked: If a thread tries to access a locked resource, it moves to the Blocked state and waits until the resource is available.
  4. Waiting: A thread enters this state if it is waiting indefinitely for another thread’s signal using wait(). It remains here until another thread calls notify().
  5. Timed Waiting: Similar to Waiting, but with a time limit. Methods like sleep(time), join(time), or wait(time) put the thread in this state.
  6. Terminated: Once the thread completes execution or is stopped, it enters the Terminated state and cannot be restarted.

By understanding thread states and their transitions, developers can efficiently manage multithreading in Java applications.

Creating Threads in Java

Java provides two primary ways to create a thread:

  1. Extending the Thread Class
  2. Implementing the Runnable Interface

1. Extending the Thread Class

In this approach, a new class is created by extending the Thread class and overriding its run() method.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running using Thread class.");
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // Start the thread
    }
}

2. Implementing the Runnable Interface

This method involves creating a class that implements the Runnable interface and defines the run() method. The thread is then created by passing an instance of the class to a Thread object.

Example:

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread is running using Runnable interface.");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start(); // Start the thread
    }
}

Pros and Cons of Each Method

MethodProsCons
Extending ThreadSimpler, no need to create a separate Runnable object.Cannot extend any other class since Java allows single inheritance.
Implementing RunnableAllows multiple inheritance as the class can extend another class while implementing Runnable.Requires creating an instance of Thread separately.

For better flexibility and maintainability, implementing Runnable is generally preferred in Java applications.

Thread Synchronization in Java

In multithreaded applications, multiple threads often access shared resources simultaneously. Without proper synchronization, this can lead to data inconsistency and race conditions, where the final output depends on the unpredictable execution order of threads. Synchronization ensures that only one thread can access a critical section at a time, maintaining data integrity.

Synchronized Methods and Blocks

Java provides the synchronized keyword to prevent multiple threads from modifying shared resources simultaneously.

Synchronized Method Example:

class SharedResource {
    synchronized void display(String message) {
        System.out.println("[" + message);
        try { Thread.sleep(500); } catch (InterruptedException e) {}
        System.out.println("]");
    }
}

class MyThread extends Thread {
    SharedResource resource;
    String message;

    MyThread(SharedResource resource, String message) {
        this.resource = resource;
        this.message = message;
    }

    public void run() {
        resource.display(message);
    }
}

public class SyncExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        MyThread t1 = new MyThread(resource, "Hello");
        MyThread t2 = new MyThread(resource, "World");

        t1.start();
        t2.start();
    }
}

Synchronized Block Example:

synchronized(this) {
    // Critical section
}

Volatile Keyword

The volatile keyword ensures visibility of shared variables across threads. Without it, a thread might read a cached value instead of the updated one.

Example:

class SharedData {
    volatile boolean flag = false;
}

Using volatile ensures that updates to flag are immediately visible to all threads, preventing stale reads.

4. Inter-Thread Communication in Java

In Java, threads can communicate with each other using the wait(), notify(), and notifyAll() methods. These methods help coordinate the execution of multiple threads, allowing them to signal and wait for each other’s actions.

How Inter-Thread Communication Works

  • wait(): A thread releases the lock and enters the waiting state until another thread calls notify().
  • notify(): Wakes up a single thread waiting on the object’s monitor.
  • notifyAll(): Wakes up all threads waiting on the monitor.

Example: Producer-Consumer Problem

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

    synchronized void produce(int value) {
        while (available) {
            try { wait(); } catch (InterruptedException e) {}
        }
        data = value;
        available = true;
        System.out.println("Produced: " + data);
        notify();
    }

    synchronized void consume() {
        while (!available) {
            try { wait(); } catch (InterruptedException e) {}
        }
        System.out.println("Consumed: " + data);
        available = false;
        notify();
    }
}

class Producer extends Thread {
    SharedResource resource;
    Producer(SharedResource resource) { this.resource = resource; }

    public void run() {
        for (int i = 1; i <= 5; i++) {
            resource.produce(i);
        }
    }
}

class Consumer extends Thread {
    SharedResource resource;
    Consumer(SharedResource resource) { this.resource = resource; }

    public void run() {
        for (int i = 1; i <= 5; i++) {
            resource.consume();
        }
    }
}

public class ThreadCommunication {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        new Producer(resource).start();
        new Consumer(resource).start();
    }
}

5. Thread Pools and Executors

Creating new threads for every task can be inefficient. Thread pools allow us to manage a fixed number of threads, reducing overhead and improving performance.

Using ExecutorService for Thread Pool Management

The ExecutorService framework provides a way to manage a pool of worker threads efficiently.

Example: Using a Fixed Thread Pool

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

class Task implements Runnable {
    private int taskId;

    Task(int id) { this.taskId = id; }

    public void run() {
        System.out.println("Executing Task " + taskId + " by " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    }
}

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // 3 threads in the pool

        for (int i = 1; i <= 5; i++) {
            executor.execute(new Task(i));
        }

        executor.shutdown(); // Shut down after task completion
    }
}

Advantages of Thread Pools

  • Better resource management: Limits the number of active threads.
  • Performance improvement: Avoids the overhead of thread creation/destruction.
  • Prevents system overload: Manages the number of concurrent tasks efficiently.

By using ExecutorService, we ensure efficient multi-threading while avoiding performance bottlenecks.

Test Your Knowledge: Java Multithreading Quiz



  1. What is java multithreading used for?

    a) Running multiple tasks simultaneously

    b) Connecting databases

    c) Compiling Java files




  2. Which method is used to start a thread in Java?

    a) begin()

    b) run()

    c) start()




  3. What is thread synchronization?

    a) Running threads without order

    b) Managing access to shared resources

    c) Halting threads forever




  4. Which interface is implemented by a thread in Java?

    a) Callable

    b) Runnable

    c) Cloneable




  5. What are concurrent collections in Java?

    a) Data structures that allow multiple threads to read and write

    b) Collections that never change

    c) Collections used in a single-threaded program

Common Issues in Multithreading

Multithreading improves performance but also introduces issues like deadlocks and race conditions, which can lead to unpredictable behavior and system failures.

Deadlocks

A deadlock occurs when two or more threads are waiting indefinitely for resources locked by each other.

Example of Deadlock:

class Resource {
    void methodA(Resource other) {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " locked this resource.");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (other) {
                System.out.println(Thread.currentThread().getName() + " locked other resource.");
            }
        }
    }
}

public class DeadlockExample {
    public static void main(String[] args) {
        Resource r1 = new Resource();
        Resource r2 = new Resource();

        Thread t1 = new Thread(() -> r1.methodA(r2), "Thread 1");
        Thread t2 = new Thread(() -> r2.methodA(r1), "Thread 2");

        t1.start();
        t2.start();
    }
}
Strategies to Avoid Deadlocks:
  • Avoid Nested Locks: Minimize multiple locks inside synchronized blocks.
  • Use Lock Ordering: Always acquire locks in a consistent order.
  • Use Try-Lock: ReentrantLock.tryLock() prevents indefinite waiting.

Race Conditions

A race condition occurs when multiple threads access and modify shared data simultaneously, leading to inconsistent results.

Example of a Race Condition:

class Counter {
    int count = 0;

    void increment() {
        count++; // Not thread-safe
    }
}

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

        Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) counter.increment(); });
        Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) counter.increment(); });

        t1.start();
        t2.start();

        try { t1.join(); t2.join(); } catch (InterruptedException e) {}

        System.out.println("Final count: " + counter.count); // Output may be incorrect
    }
}
How to Prevent Race Conditions:
  • Use Synchronized Methods/Blocks:
synchronized void increment() {
    count++;
}
  • Use Atomic Variables: AtomicInteger ensures atomic operations.
import java.util.concurrent.atomic.AtomicInteger;
class Counter {
    AtomicInteger count = new AtomicInteger(0);
    void increment() {
        count.incrementAndGet();
    }
}

Best Practices for Multithreaded Programming in Java

Writing efficient and error-free multithreaded code requires following best practices:

  1. Minimize Shared Resources
    • Reduce the number of shared variables to avoid synchronization overhead.
    • Favor local variables over global ones where possible.
  2. Use Thread-Safe Collections
    • Instead of ArrayList or HashMap, use thread-safe alternatives like:
      • ConcurrentHashMap
      • CopyOnWriteArrayList
      • BlockingQueue for producer-consumer scenarios
  3. Properly Handle Thread Exceptions
    • Use try-catch inside the run() method to prevent threads from terminating unexpectedly.
class SafeThread extends Thread {
    public void run() {
        try {
            // Task execution
        } catch (Exception e) {
            System.err.println("Exception in thread: " + e.getMessage());
        }
    }
}
  1. Prefer ExecutorService Over Creating New Threads
    • Managing threads manually is inefficient. Instead, use thread pools with ExecutorService:
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.execute(() -> System.out.println("Task executed"));
executor.shutdown();
  1. Use Locks Instead of synchronized When Needed
    • ReentrantLock allows better control than synchronized.
import java.util.concurrent.locks.ReentrantLock;
class SharedResource {
    private final ReentrantLock lock = new ReentrantLock();

    void accessResource() {
        lock.lock();
        try {
            System.out.println("Thread-safe access.");
        } finally {
            lock.unlock();
        }
    }
}
  1. Avoid Busy-Waiting
    • Instead of constantly checking a condition, use wait() and notify().
  2. Use Volatile for Visibility, Not Atomicity
    • volatile ensures visibility of changes across threads but does not guarantee atomic updates.
  3. Test Multithreaded Code Extensively
    • Use tools like Thread Dump, VisualVM, and Concurrency Testing Frameworks to detect issues.

By following these best practices, Java developers can write efficient, safe, and scalable multithreaded applications.

Conclusion

Mastering ‘java multithreading’ unlocks incredible possibilities, making your programs more efficient and responsive. Explore more with Newtum, where resources and tutorials await. Don’t hesitate—dive deeper, practice more, and enhance your coding journey. Engage today and become a multithreading maven!

Edited and Compiled by

This blog was compiled and edited by Rasika Deshpande, who has over 4 years of experience in content creation. She’s passionate about helping beginners understand technical topics in a more interactive way.

About The Author