Multithreading in Java – What is Java Multithreading?

Learn about Java Multithreading, its features, benefits, and how it enhances performance by allowing concurrent execution of threads.

Multithreading in Java

Multithreading is a technique of programming in which a program can perform several tasks at the same time by breaking these tasks into smaller threads that may run independently. But it also helps to improve the performance of applications, making them more responsive and efficient. 

Because Java is a solid and frequently used programming language, it offers considerable support for multithreading via its rich sets of APIs. Multithreading is important for modern software development whether you are trying to improve responsiveness of user interfaces, run background tasks, or trying to optimize CPU utilization. 

In this blog we will go over basic multithreading from the ground up, building towards some more advanced topics. You’ll also learn how to use this feature effectively in Java with clear explanations and with practical examples. Let’s dive in!

What is a Thread?

A thread is the smallest unit of a program that can run independently. In Java:

  • Each thread has its own lifecycle.
  • Threads within the same application share memory, making communication efficient but requiring synchronization.

Key Concepts in Multithreading

Process vs Thread

  1. Process: An independent program running in memory.
  2. Thread: A smaller, lightweight unit within a process.

Difference Between Multithreading and Multitasking

AspectMultithreadingMultitasking
ScopeMultiple threads in one processMultiple processes running simultaneously
MemoryThreads share memoryProcesses have isolated memory
SpeedFaster (less overhead)Slower due to process isolation

Key Features of Multithreading in Java

  1. Concurrency: Concurrent execution with multiple threads reduces idle CPU time.
  2. Resource Sharing: The reason for threads being able to share the same memory space is that communication is efficient.
  3. Independent Execution: Each thread runs independently, so failure in one thread doesn’t affect others.
  4. Synchronization Support: Java provides tools to manage thread interference and memory consistency.

Creating Threads in Java

1. Extending the Thread Class

In Java, you can create threads by extending the Thread class.

Code

// Define a thread by extending Thread

class MyThread extends Thread {

    public void run() {

        // Task to be performed by the thread

        System.out.println("Thread is running: " + Thread.currentThread().getName());

    }

}

public class ThreadExample {

    public static void main(String[] args) {

        MyThread thread = new MyThread(); // Create a thread

        thread.start(); // Start the thread

    }

}

Explanation:

  • run() Method: Contains the task the thread will execute.
  • start() Method: Begins the thread execution by calling run() internally.

2. Implementing the Runnable Interface

If you want your class to extend another class (Java doesn’t support multiple inheritance), you can use the Runnable interface.

class MyRunnable implements Runnable {

    public void run() {

        // Task for the thread

        System.out.println("Runnable thread is running: " + Thread.currentThread().getName());

    }

}

public class RunnableExample {

    public static void main(String[] args) {

        Thread thread = new Thread(new MyRunnable()); // Pass Runnable to Thread

        thread.start(); // Start the thread

    }

}

Why use Runnable?

  • It’s more flexible and promotes code reusability.

Thread Lifecycle

Threads in Java go through the following states:

  1. New: Thread is created but not started.
  2. Runnable: Thread is ready to run but waiting for the CPU.
  3. Running: Thread is executing.
  4. Waiting/Blocked: Thread is waiting for resources or another thread.
  5. Terminated: Thread has completed execution.

Thread Synchronization

When multiple threads access shared resources, synchronization ensures data consistency by allowing only one thread to access the resource at a time.

Example: Synchronizing a Counter

Code 

class Counter {

    private int count = 0;

    public synchronized void increment() {

        count++; // Increment the count

    }

    public int getCount() {

        return count;

    }

}

public class SynchronizationExample {

    public static void main(String[] args) throws InterruptedException {

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

        t1.join(); // Wait for t1 to finish

        t2.join(); // Wait for t2 to finish

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

    }

}

Explanation:

  • synchronized Keyword: Ensures that only one thread can execute increment() at a time.
  • join() Method: Waits for threads to complete before proceeding.

Exception Handling in Threads

Threads may encounter exceptions, such as invalid input or divide-by-zero errors. Proper exception handling ensures stable execution.

1. Using try-catch

Code

class ExceptionThread extends Thread {

    public void run() {

        try {

            int result = 10 / 0; // Will throw ArithmeticException

        } catch (ArithmeticException e) {

            System.out.println("Exception handled: " + e.getMessage());

        }

    }

}

public class ExceptionHandlingExample {

    public static void main(String[] args) {

        ExceptionThread thread = new ExceptionThread();

        thread.start();

    }

}

2. Using UncaughtExceptionHandler

Code

class RiskyThread extends Thread {

    public void run() {

        int result = 10 / 0; // Throws exception

    }

}

public class UncaughtHandlerExample {

    public static void main(String[] args) {

        Thread thread = new RiskyThread();

        thread.setUncaughtExceptionHandler((t, e) -> {

            System.out.println("Exception in thread " + t.getName() + ": " + e.getMessage());

        });

        thread.start();

    }

}

Key Points:

  • Try-catch is for handling expected exceptions locally.
  • UncaughtExceptionHandler catches global exceptions in threads.

Thread Pooling

Thread pools reuse a fixed number of threads to execute multiple tasks, reducing the overhead of thread creation.

Example Using Executor Service

Code

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class ThreadPoolExample {

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(3); // Pool size of 3

        for (int i = 1; i <= 5; i++) {

            final int task = i;

            executor.execute(() -> {

                System.out.println("Executing task " + task + " on thread: " + Thread.currentThread().getName());

            });

        }

        executor.shutdown(); // Shutdown the pool

    }

}

Explanation:

  • FixedThreadPool: Limits the number of threads running simultaneously.
  • Efficient for handling large numbers of short-lived tasks.

Advanced Multithreading Concepts

  1. Thread Priority: Threads can be assigned priorities (MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY).
  2. Thread Group: Threads can be grouped and managed collectively.
  3. Volatile Keyword: Ensures visibility of changes to a variable across threads.

Multithreading in Java Example

Here’s an example of multithreading where threads perform different tasks concurrently.

Code

// Multithreading Example

class PrintNumbers implements Runnable {

    @Override

    public void run() {

        for (int i = 1; i <= 5; i++) {

            System.out.println("Number: " + i + " by Thread: " + Thread.currentThread().getId());

        }

    }

}

class PrintAlphabets implements Runnable {

    @Override

    public void run() {

        for (char c = 'A'; c <= 'E'; c++) {

            System.out.println("Alphabet: " + c + " by Thread: " + Thread.currentThread().getId());

        }

    }

}

public class Main {

    public static void main(String[] args) {

        Thread t1 = new Thread(new PrintNumbers());

        Thread t2 = new Thread(new PrintAlphabets());

        t1.start(); // Start number printing thread

        t2.start(); // Start alphabet printing thread

    }

}

Output:

Number: 1 by Thread: 13

Alphabet: A by Thread: 14

Number: 2 by Thread: 13

Alphabet: B by Thread: 14

Conclusion

Multithreading in Java allows developers to write efficient, responsive, and scalable programs. From thread creation using Thread or Runnable to synchronization, exception handling, and thread pooling, Java provides robust APIs to manage concurrency.

By mastering these concepts and applying them effectively, you can build powerful, multi-threaded applications.

 FAQs about Multithreading in Java

1. How does the Java Virtual Machine (JVM) handle threads?

The JVM uses the underlying operating system’s threading model to manage threads. In most modern JVMs, threads are mapped to native OS threads, enabling efficient scheduling and execution. The JVM also manages thread priorities and provides a memory model to ensure safe interaction between threads.

2. Can multiple threads in Java access the same method at the same time?

Yes, multiple threads can access the same method unless the method is marked as synchronized. Without synchronization, threads may cause race conditions or inconsistent data. Synchronizing methods or blocks ensures only one thread can access the critical section at a time.

3. What is thread starvation, and how can it be avoided in Java?

Thread starvation occurs when a thread is unable to gain CPU time due to higher-priority threads monopolizing the processor. It can be avoided by:
-Using fair locks (ReentrantLock with fairness policy).
-Avoiding excessive thread priority adjustments.
-Ensuring a balanced thread pool configuration in the application.

4. What is the difference between wait() and sleep() methods in Java multithreading?

Wait(): Releases the monitor lock and suspends the thread until another thread calls notify() or notifyAll(). It is used for inter-thread communication.
Sleep(): Pauses the thread for a specified time without releasing the lock. It is used for timing delays.

5. What is a deadlock in Java, and how can it be prevented?

A deadlock occurs when two or more threads are waiting indefinitely for each other to release locks, causing the program to freeze.
Prevention techniques:
-Avoid nested locks.
-Use a consistent lock acquisition order.
-Use tryLock() from ReentrantLock to avoid indefinite blocking.

Software engineering courses certificates
Software engineering courses placements
Software engineering courses syllabus
Software engineering courses fees
Software engineering courses eligibility

→ Explore this Curated Program for You ←

Avatar photo
Great Learning Editorial Team
The Great Learning Editorial Staff includes a dynamic team of subject matter experts, instructors, and education professionals who combine their deep industry knowledge with innovative teaching methods. Their mission is to provide learners with the skills and insights needed to excel in their careers, whether through upskilling, reskilling, or transitioning into new fields.

Full Stack Software Development Course from UT Austin

Learn full-stack development and build modern web applications through hands-on projects. Earn a certificate from UT Austin to enhance your career in tech.

4.8 ★ Ratings

Course Duration : 28 Weeks

Cloud Computing PG Program by Great Lakes

Enroll in India's top-rated Cloud Program for comprehensive learning. Earn a prestigious certificate and become proficient in 120+ cloud services. Access live mentorship and dedicated career support.

4.62 ★ (2,760 Ratings)

Course Duration : 8 months

Scroll to Top