Back to Java
2026-03-277 min read

Multithreaded (Java)

Learn Multithreaded (Java) step by step with clear examples and exercises.

Title: Mastering Multithreaded Programming in Java - A full guide

Why This Matters

In today's fast-paced world, multithreading has become a crucial aspect of modern programming to ensure efficient execution of multiple tasks concurrently. By learning multithreaded programming in Java, you will be better equipped to handle real-world scenarios that require parallel processing and improved application performance. This knowledge can prove valuable during job interviews, exams, or when debugging complex issues in your projects.

Prerequisites

Before diving into the core concept of multithreading, it's essential to have a solid understanding of the following topics:

  • Java basics (variables, data types, operators, control statements)
  • Object-oriented programming concepts (classes, objects, inheritance, interfaces)
  • Exception handling in Java
  • Basic knowledge of synchronization and concurrency concepts
  • Understanding of core Java collections such as ArrayList, HashMap, and ConcurrentHashMap

Core Concept

Understanding Threads and Multithreading

A thread is a separate path of execution within a program. In Java, every Java application has at least one thread—the main thread (also known as the main method's thread). Multithreading allows you to create multiple threads within a single Java application for concurrent execution of tasks.

Creating Threads in Java

To create a new thread, you can either extend the Thread class or implement the Runnable interface. Here's an example using both methods:

public class MyThread extends Thread {
public void run() {
System.out.println("Running in the MyThread!");
}
}

class AnotherThread implements Runnable {
public void run() {
System.out.println("Running in the AnotherThread!");
}

public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new Thread(new AnotherThread());

t1.start(); // Starts the thread by calling the run() method
t2.start();
}
}

In this example, we have created two threads—one by extending the Thread class and another by implementing the Runnable interface. The start() method initiates the execution of a thread's run() method.

Synchronization and Deadlocks

When multiple threads access shared resources (like variables or objects), it can lead to synchronization issues such as race conditions and deadlocks. To prevent these problems, Java provides various synchronization mechanisms like synchronized blocks, Lock, and ReentrantLock.

Synchronized Blocks

A synchronized block ensures that only one thread can access the code within it at a time. Here's an example:

public class SynchronizedExample {
private int counter = 0;

public void incrementCounter() {
synchronized (this) {
counter++;
}
}
}

In this example, the incrementCounter() method is protected by a synchronized block. This means that only one thread can call this method at any given moment to prevent race conditions.

Thread Priorities and Daemon Threads

Thread priorities determine the order in which threads are executed when they become ready to run. The higher the priority, the sooner a thread will be executed. You can set a thread's priority using the setPriority() method.

Daemon threads have lower priority than user threads and are used for background tasks that don't require immediate attention. By default, the main thread is a user thread, while other threads created in the program are daemon threads unless explicitly marked as user threads by calling setDaemon(false).

Thread Communication and Coordination

Threads often need to communicate or coordinate with each other to share information or wait for specific conditions. Java provides several mechanisms for thread communication, including wait(), notify(), and notifyAll() methods within synchronized blocks, as well as CountDownLatch, CyclicBarrier, and Semaphore.

Wait, Notify, and NotifyAll

The wait(), notify(), and notifyAll() methods are used to block a thread until it is notified by another thread. These methods should be called within synchronized blocks. Here's an example:

public class WaitNotifyExample {
private int counter = 0;
private final Object lock = new Object();

public void incrementCounter() {
synchronized (lock) {
counter++;
lock.notifyAll(); // Notifies all waiting threads
}
}

public void decrementCounter() throws InterruptedException {
synchronized (lock) {
while (counter <= 0) {
lock.wait(); // Blocks the current thread until it is notified
}
counter--;
}
}
}

In this example, the incrementCounter() method increments the counter and notifies all waiting threads. The decrementCounter() method blocks the current thread if the counter is less than or equal to 0 until it is notified by another thread.

Thread Pool Executors

Thread pool executors allow you to create a fixed number of threads for executing tasks concurrently, improving performance and reducing resource consumption. Java provides several implementations of ExecutorService, such as Executors.newFixedThreadPool() and Executors.newCachedThreadPool().

Worked Example

Let's create a simple multithreaded example that prints "Hello" and "World" concurrently:

class HelloWorld implements Runnable {
private String message;

public HelloWorld(String message) {
this.message = message;
}

@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(message);
try {
Thread.sleep(1000); // Sleep for 1 second before printing again
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new HelloWorld("Hello"));
executor.execute(new HelloWorld("World"));
executor.shutdown(); // Wait for all tasks to complete before terminating the executor
}
}

In this example, we have created a thread pool with 2 threads using Executors.newFixedThreadPool(). The execute() method submits the runnable tasks (HelloWorld objects) for execution in the thread pool.

Common Mistakes

1. Forgetting to call start() on the thread

Remember to call the start() method on the thread object after creating it, as shown in the worked example above.

Thread t = new Thread(); // Wrong!
t.start(); // Throws IllegalThreadStateException

2. Accessing shared resources without synchronization

When multiple threads access shared resources like variables or objects, it can lead to race conditions and unexpected behavior. Always use synchronization mechanisms when necessary.

Example of Race Condition

class Counter {
private int count = 0;

public void incrementCounter() {
count++;
}
}

public class RaceConditionExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> counter.incrementCounter());
Thread t2 = new Thread(() -> counter.incrementCounter());

t1.start();
t2.start();
t1.join(); // Wait for thread t1 to finish execution
t2.join(); // Wait for thread t2 to finish execution

System.out.println("Total count: " + counter.count); // Output may vary due to race condition
}
}

In this example, two threads increment a shared counter without synchronization. The output may not always be the expected value of 2 because of the race condition.

3. Ignoring thread priorities and daemon threads

Understanding thread priorities and using them appropriately can improve the performance of your multithreaded applications. Also, be aware of when to use daemon threads and user threads based on the requirements of your program.

Practice Questions

  1. Write a multithreaded Java program that simulates a simple bank account with two accounts (AccountA and AccountB). Each account should have a balance, and there should be methods for depositing and withdrawing money. Create two threads—one for each account—that perform transactions concurrently.
  1. Implement a multithreaded Java program that reads input from two different files simultaneously using separate threads. The program should print the lines read from both files interleaved.
  1. (Bonus) Write a multithreaded Java program that solves the famous "Producer-Consumer" problem, where one thread produces random numbers and another thread consumes them. Implement synchronization mechanisms to ensure proper communication between threads.
  1. (Advanced) Write a multithreaded Java program that implements a concurrent hash map using locks for synchronization. Compare its performance with the built-in ConcurrentHashMap in terms of throughput and latency.

FAQ

1. What is the difference between Thread and Runnable in Java?

Both Thread and Runnable are used to create new threads in Java. Thread is a built-in class that provides methods for managing threads, while Runnable is an interface that defines the logic of what a thread does. You can either extend the Thread class or implement the Runnable interface to create a new thread.

2. How do I set the priority of a thread in Java?

You can set the priority of a thread using the setPriority() method. The priority is an integer value between 1 (lowest) and 10 (highest). Here's an example:

Thread t = new Thread();
t.setPriority(5); // Sets the priority to 5

3. What are some common synchronization mechanisms in Java?

Some common synchronization mechanisms in Java include synchronized blocks, Lock, and ReentrantLock. These mechanisms help prevent race conditions and deadlocks when multiple threads access shared resources.

4. How do I create a thread pool executor with a custom number of threads in Java?

To create a thread pool executor with a custom number of threads, use the Executors.newFixedThreadPool() method and specify the desired number of threads as an argument:

ExecutorService executor = Executors.newFixedThreadPool(10); // Creates a thread pool with 10 threads

5. What is the difference between wait(), notify(), and notifyAll() in Java?

The wait() method blocks the current thread until it is notified by another thread, while notify() and notifyAll() unblock a single waiting thread or all waiting threads, respectively. These methods should be called within synchronized blocks.

Multithreaded (Java) | Java | XQA Learn