Multi-Threading in Java

Saurav Kumar
11 min readSep 17, 2023

--

Multithreading in Java refers to the concurrent execution of multiple threads within a Java program. A thread is the smallest unit of a program that can be executed independently, and multithreading allows you to write programs that can perform multiple tasks simultaneously. Java provides built-in support for multithreading through its java.lang.Thread class and the java.lang.Runnable interface.

Here are some key concepts related to multithreading in Java:

  • Thread Class: In Java, you can create and manage threads using the Thread class. You can create a new thread by extending the Thread class and overriding its run() method, which contains the code that will be executed in the new thread.
class MyThread extends Thread {
public void run() {
// Code to be executed in the new thread
}
}
  • Runnable Interface: An alternative way to create threads in Java is by implementing the Runnable interface. This interface defines a single run() method, and you can pass a Runnable object to a Thread constructor.
class MyRunnable implements Runnable {
public void run() {
// Code to be executed in the new thread
}
}

// Create a thread using a Runnable
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
  • Thread Lifecycle: A thread goes through several states during its lifecycle, including new, runnable, running, blocked, and terminated. You can use various methods provided by the Thread class to manage and query the state of threads.
  • Thread Synchronization: When multiple threads access shared resources, you need to ensure proper synchronization to avoid data inconsistencies and race conditions. Java provides synchronization mechanisms like the synchronized keyword and the java.util.concurrent package to help you achieve this.
  • Thread Priorities: Threads in Java can have different priorities, which can influence how the operating system schedules them for execution. You can set thread priorities using the setPriority() method.
  • Thread Joining: The join() method allows one thread to wait for another thread to complete its execution. This is useful for coordinating the execution of threads.
  • Thread Pools: Java provides the java.util.concurrent.Executor framework for managing thread pools, which is a more efficient way to manage and reuse threads in applications with a high number of concurrent tasks.
  • Thread Safety: Ensuring that your multithreaded code is thread-safe is crucial to avoid problems like data corruption and deadlocks. Thread safety can be achieved through various techniques like using synchronized blocks, locks, or thread-safe data structures.

Multithreading in Java is a powerful feature that can help improve the performance and responsiveness of applications by taking advantage of modern multi-core processors. However, it also introduces complexity, and you need to be careful when designing and implementing multithreaded code to avoid issues like race conditions and deadlocks.

Lifecycle of a Thread

The lifecycle of a thread in Java consists of several states that a thread goes through from its creation until its termination. Understanding the thread lifecycle is essential for managing and coordinating threads effectively in a Java program. The thread states in Java are as follows:

  • New: When a thread is created, it is in the “New” state. In this state, the thread has been instantiated but has not yet started its execution. You can create a new thread by instantiating a Thread object or by creating a class that extends the Thread class and then creating an instance of that class.
Thread newThread = new Thread();
  • Runnable: A thread enters the “Runnable” state when it is ready to execute but hasn’t been scheduled for execution by the operating system yet. This means that the start() method has been called on the thread, or the thread has been added to a thread pool. However, it doesn't necessarily mean that the thread is actively executing; it might be waiting for its turn to run.
Thread myThread = new Thread();
myThread.start(); // Transition to the Runnable state
  • Running: In the “Running” state, the thread’s code is actively executing. When the operating system schedules the thread, it starts executing its run() method or the code provided to it. A thread can be in this state for a variable amount of time.
  • Blocked/Waiting: A thread may enter the “Blocked” or “Waiting” state when it is temporarily inactive or waiting for a specific condition to be met. This can happen for various reasons, such as when a thread is waiting for I/O operations, synchronization locks, or for another thread to notify it. Threads in this state are not executing their code.
  • Timed Waiting: Threads in this state are similar to the “Waiting” state but with a time limit. They are waiting for a specific condition but with a timeout. For example, a thread might be waiting for a resource to become available for a certain amount of time before giving up and moving to another state.
  • Terminated/Dead: A thread enters the “Terminated” or “Dead” state when it has completed its execution or when it is explicitly stopped. Once a thread is terminated, it cannot be restarted. You can check if a thread is in the terminated state using the Thread.isAlive() method.

Here’s a simplified representation of the thread lifecycle:

New -> Runnable -> (Running) -> Blocked/Waiting/Timed Waiting -> (Running) -> Terminated

Main Constructors of Thread class:

The Thread class in Java provides several constructors to create and configure threads. Here are the main constructors of the Thread class:

  • Thread with No Arguments:
package com.tipsontech.demo;

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


public class MultiThreading {
public static void main(String[] args) {
Thread th = new MyThread();
th.start();
}
}

This constructor creates a new thread with no associated Runnable object. You would typically override the run() method in a subclass of Thread to define the thread's behavior.

  • Thread with a Runnable:
package com.tipsontech.demo;

public class RunnableThreadExample {

public static void main(String[] args) {
// Create a Runnable object
Runnable myRunnable = new MyRunnable();

// Create a thread and associate it with the Runnable
Thread thread = new Thread(myRunnable);

// Start the thread
thread.start();

// Main thread continues its execution
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class MyRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread: Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

This constructor creates a new thread with the specified Runnable object as its target. The thread will execute the run() method of the Runnable object when started. In the above example, we create a thread by passing a Runnable object (MyRunnable) as a parameter to the Thread constructor. The MyRunnable class implements the Runnable interface and overrides the run() method to define the thread's behavior. Both the main thread and the new thread execute concurrently, with the run() method of MyRunnable being executed in the new thread.

  • Thread with a Name:
package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {
// Create a thread with a name
Thread namedThread = new Thread("MyNamedThread") {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread " + getName() + ": Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};

// Start the thread
namedThread.start();

// Main thread continues its execution
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

This constructor creates a new thread with the given name. Naming threads can be helpful for debugging and identifying threads in your application. In the above example, we create a thread named “MyNamedThrea” by passing the name as a parameter to the Thread constructor. The thread's run() method prints messages and sleeps for a second between each message. The main thread also prints messages simultaneously.

  • Thread with a Runnable and a Name:
package com.tipsontech.demo;

public class RunnableWithNamedThreadExample {

public static void main(String[] args) {
// Create a Runnable object
Runnable myRunnable = new MyRunnable();

// Create a thread with a name and associate it with the Runnable
Thread namedThread = new Thread(myRunnable, "MyNamedThread");

// Start the named thread
namedThread.start();

// Main thread continues its execution
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class MyRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread " + Thread.currentThread().getName() + ": Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

This constructor combines both a Runnable object and a name for the thread. In the above example, we create a thread with the name “MyNamedThread” by passing the name as the second parameter to the Thread constructor, along with the Runnable object (MyRunnable) as the first parameter. The MyRunnable class implements the Runnable interface and defines the thread's behavior in its run() method.

Both the main thread and the named thread execute concurrently. The named thread’s name is displayed in the output, making it easy to identify.

Methods of Thread class:

The Thread class in Java provides several methods for managing and controlling threads. Here are some of the most commonly used methods of the Thread class:

  • start():

start() is used to begin the execution of a thread. It will call the run() method of the thread's associated Runnable object or the run() method if the thread is a subclass of Thread.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {
Thread th = new Thread(() -> {
System.out.println("Thread is running...");
});

th.start();
}
}
  • run():

run() is the method that contains the code to be executed by the thread. You typically override this method when creating a custom thread by extending the Thread class.

package com.tipsontech.demo;

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

public class MultiThreading {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
  • currentThread():

currentThread() returns a reference to the currently executing thread. It's a static method, so you can call it using the class name.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {

Thread current = Thread.currentThread();
System.out.println("Current thread: " + current.getName());

}
}
  • setName(String name):

setName() is used to set the name of the thread for identification and debugging purposes.

  • getName():

getName() returns the name of the thread.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {

Thread thread = new Thread(() -> {
System.out.println("Thread name: " + Thread.currentThread().getName());
});
thread.setName("MyThread");
thread.start();

}
}
  • setPriority(int priority):

setPriority() is used to set the priority of the thread. Thread priorities are integers ranging from Thread.MIN_PRIORITY to Thread.MAX_PRIORITY.

  • getPriority():

getPriority() returns the priority of the thread.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {

Thread thread1 = Thread.currentThread();
thread1.setPriority(Thread.MAX_PRIORITY);
System.out.println("Thread priority: " + thread1.getPriority());

Thread thread2 = Thread.currentThread();
thread2.setPriority(Thread.MIN_PRIORITY);
System.out.println("Thread priority: " + thread2.getPriority());

Thread thread3 = Thread.currentThread();
thread3.setPriority(Thread.NORM_PRIORITY);
System.out.println("Thread priority: " + thread3.getPriority());
}
}
  • isAlive():

isAlive() checks if the thread is still alive or has terminated.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {

Thread thread = new Thread(() -> {
// Simulate some work
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
boolean isAlive = thread.isAlive();
System.out.println("Is thread alive? " + isAlive);
}
}
  • isDaemon():

isDaemon() checks if the thread is a daemon thread.

  • setDaemon(boolean on):

setDaemon() is used to mark the thread as a daemon thread or a user thread. Daemon threads are terminated when all user threads have finished executing.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {

Thread daemonThread = new Thread(() -> {
System.out.println("Daemon thread is running.");
});

daemonThread.setDaemon(true);
boolean isDaemon = daemonThread.isDaemon();

System.out.println("Is daemon thread? " + isDaemon);

daemonThread.start();

}
}
  • sleep(long milliseconds):

sleep() causes the thread to sleep for the specified number of milliseconds.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {

Thread thread = new Thread(() -> {
try {
System.out.println("Thread is sleeping.");
Thread.sleep(2000);
System.out.println("Thread woke up.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});

thread.start();
}
}
  • join() and join(long milliseconds):

join() blocks the calling thread until the thread on which it's called has finished execution.

join(long milliseconds) blocks the calling thread for at most the specified number of milliseconds.

package com.tipsontech.demo;

public class MultiThreading {
public static void main(String[] args) {

Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 is running.");
});

Thread thread2 = new Thread(() -> {
System.out.println("Thread 2 is running.");
});

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

try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Both threads have finished.");
}
}

Types of Thread in Java:

In Java, there are two main types of threads: user threads and daemon threads. These types of threads are distinguished by their behavior and purpose:

User Threads:

  • User threads are the most common type of threads in Java.
  • They are created by application developers and are responsible for carrying out the main tasks and operations of the application.
  • When the JVM (Java Virtual Machine) detects that there are active user threads, it will not terminate the program until all user threads have finished executing.
  • User threads are created using the Thread class or by implementing the Runnable interface and passing instances to a Thread constructor.
package com.tipsontech.demo;

public class UserThreadExample {

public static void main(String[] args) {
// Create a user thread
Thread userThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("User Thread: Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

// Start the user thread
userThread.start();

// Main thread continues its execution
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

In this example, we create a user thread by passing a Runnable lambda expression to the Thread constructor. The user thread's job is to count from 1 to 5 with a one-second delay between each count and print messages.

The main thread and the user thread execute concurrently. You can observe that both threads are running in parallel, and the user thread performs its task while the main thread continues its own execution.

Daemon Threads:

  • Daemon threads, sometimes referred to as daemonized threads, are background threads that provide support to user threads.
  • They are designed to perform tasks that do not need to keep the application running. When all user threads finish executing, daemon threads are automatically terminated by the JVM without allowing them to complete their tasks.
  • Daemon threads are typically used for tasks like garbage collection, monitoring, or maintaining background services.
  • You can set a thread as a daemon thread using the setDaemon(true) method before starting the thread.
package com.tipsontech.demo;

public class DaemonThreadExample {

public static void main(String[] args) {
// Create a daemon thread
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon Thread: Running as a background task.");
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

// Set the thread as a daemon thread
daemonThread.setDaemon(true);

// Start the daemon thread
daemonThread.start();

// Main thread continues its execution
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: Count " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

In this example, we create a daemon thread by passing a Runnable lambda expression to the Thread constructor. The daemon thread's job is to run continuously in the background, printing messages every second.

We set the thread as a daemon thread using the setDaemon(true) method before starting it. Daemon threads are automatically terminated when all user threads (in this case, the main thread) have finished executing.

The main thread and the daemon thread execute concurrently. You’ll notice that the daemon thread continues running even after the main thread has finished, and it is eventually terminated by the JVM.

--

--

Saurav Kumar

Experienced Software Engineer adept in Java, Spring Boot, Microservices, Kafka & Azure.