Back to Java
2026-03-258 min read

Worker Threads (Java)

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

Why This Matters

Worker threads are essential in Java applications as they enable concurrent execution of multiple tasks, improving performance and responsiveness. By using worker threads, we can offload time-consuming operations such as I/O operations or lengthy computations from the main thread, ensuring that our application remains responsive even during these operations. This is particularly important for user interfaces where unresponsive periods can lead to a poor user experience.

Prerequisites

Before diving into worker threads, it's essential to have a good understanding of the following topics:

  1. Basic Java syntax and control structures (if, for, while loops, etc.)
  2. Understanding of classes and objects in Java
  3. Concept of methods and their overloading
  4. Exception handling in Java
  5. Synchronization in Java (synchronized blocks and methods)
  6. Understanding the difference between Thread and Runnable interfaces
  7. Familiarity with Java's I/O operations (FileReader, BufferedReader, etc.)
  8. Knowledge of ArrayList and List data structures
  9. Basic understanding of ExecutorService and its benefits

Core Concept

A worker thread is a separate thread that performs specific tasks within an application. In Java, we can create new threads by extending the Thread class or implementing the Runnable interface.

Creating a Worker Thread using the Thread Class (Expanded)

To create and start a worker thread using the Thread class, follow these steps:

  1. Extend the Thread class in your class definition.
  2. Override the run() method to define what the thread should do when it starts executing.
  3. Create an instance of the extended Thread class and call the start() method to start the thread.
  4. Optionally, you can override other methods like getName(), setName(), setPriority(), or setDaemon(boolean) for customization.

Here's a simple example:

public class WorkerThreadExample extends Thread {
private String threadName;

public WorkerThreadExample(String name) {
this.threadName = name;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Worker Thread: " + threadName + ", " + i);
try {
Thread.sleep(1000); // Sleep for 1 second before printing next number
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

@Override
public String getName() {
return threadName;
}

public static void main(String[] args) {
WorkerThreadExample worker1 = new WorkerThreadExample("Worker 1");
WorkerThreadExample worker2 = new WorkerThreadExample("Worker 2");
worker1.start(); // Start the first thread
worker2.start(); // Start the second thread

for (int i = 0; i < 10; i++) {
System.out.println("Main Thread: " + i);
}
}
}

In this example, we have a WorkerThreadExample class that extends the Thread class and overrides the run(), getName(), and optional methods for customization. We create two instances of the extended Thread class, start them with the start() method, and print numbers from 0 to 9 concurrently with the worker threads.

Creating a Worker Thread using the Runnable Interface (Expanded)

Alternatively, we can create a worker thread by implementing the Runnable interface and passing an instance of the implementing class to a new Thread object. Here's how:

  1. Implement the Runnable interface in your class definition.
  2. Override the run() method to define what the thread should do when it starts executing.
  3. Create a separate class that extends Thread, pass an instance of the implementing Runnable class as an argument to its constructor, and call the start() method to start the thread.
  4. Optionally, you can override other methods like getName(), setName(), setPriority(), or setDaemon(boolean) for customization.

Here's a simple example:

public class WorkerRunnableExample implements Runnable {
private String threadName;

public WorkerRunnableExample(String name) {
this.threadName = name;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Worker Thread: " + threadName + ", " + i);
try {
Thread.sleep(1000); // Sleep for 1 second before printing next number
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

@Override
public String getName() {
return threadName;
}

public static void main(String[] args) {
WorkerRunnable worker1 = new WorkerRunnable("Worker 1"); // Create a Runnable object
WorkerRunnable worker2 = new WorkerRunnable("Worker 2"); // Create another Runnable object

Thread thread1 = new Thread(worker1); // Wrap the first Runnable object in a Thread object
Thread thread2 = new Thread(worker2); // Wrap the second Runnable object in another Thread object

thread1.start(); // Start the first thread
thread2.start(); // Start the second thread

for (int i = 0; i < 10; i++) {
System.out.println("Main Thread: " + i);
}
}
}

In this example, we have a WorkerRunnableExample class that implements the Runnable interface and overrides the run(), getName(), and optional methods for customization. We create two instances of the Runnable class, wrap them in separate Thread objects, start them with the start() method, and print numbers from 0 to 9 concurrently with the worker threads.

Worked Example

Let's consider an example where we have a file containing a list of words, and we want to read the file content into an ArrayList using multiple worker threads for better performance. We will use ExecutorService for managing the worker threads.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class WorkerThreadExample {
public static void main(String[] args) throws IOException, InterruptedException {
List<String> words = new ArrayList<>();
String filePath = "words.txt";
int numberOfThreads = 4; // Number of worker threads to use

ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);

BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line;
while ((line = reader.readLine()) != null) {
WorkerTask task = new WorkerTask(line, words);
executor.submit(task);
}
reader.close();

executor.shutdown(); // Wait for all tasks to complete
while (!executor.isTerminated()) {}

System.out.println("Total Words: " + words.size());
}

static class WorkerTask implements Runnable {
private String line;
private List<String> words;

public WorkerTask(String line, List<String> words) {
this.line = line;
this.words = words;
}

@Override
public void run() {
String[] wordArray = line.split("\\s+");
for (String word : wordArray) {
if (!word.isEmpty()) {
words.add(word);
}
}
}
}
}

In this example, we create an ExecutorService with a fixed number of threads (4 in this case). We read the file line by line and submit a WorkerTask for each line to the executor service. The WorkerTask class splits the line into words and adds them to the words list.

Common Mistakes

  1. Not using start() method: Remember to call the start() method on the Thread or ExecutorService instance to actually start the thread execution.
  2. Sharing data between threads without synchronization: If multiple threads access and modify shared data, it can lead to inconsistencies and unexpected behavior. Use synchronized blocks or atomic variables to ensure proper synchronization.
  3. Not handling exceptions properly: Make sure to handle exceptions appropriately in your worker threads, as unhandled exceptions can cause the entire application to crash.
  4. Not shutting down executor service: Always call shutdown() on the ExecutorService instance before exiting the application to ensure that all tasks are completed and resources are released.
  5. Ignoring thread interruptions: Use the interrupt() method to signal a Thread or Runnable object to stop its execution, and make sure your threads check for interruptions periodically to handle shutdown requests gracefully.
  6. Not using ExecutorService effectively: Make sure to use the appropriate ExecutorService configuration (FixedThreadPool, SingleThreadExecutor, or ScheduledExecutorService) based on your application's requirements.
  7. Misusing synchronization: Overuse of synchronization can lead to performance bottlenecks. Use it judiciously and only where necessary to avoid race conditions and inconsistencies.
  8. Not testing concurrent code: Always test your concurrent code thoroughly to ensure that it behaves as expected under various threading scenarios.

Practice Questions

  1. Write a program that uses worker threads to find the sum of the first N Fibonacci numbers (up to 40) using separate threads for even and odd numbers. Use ExecutorService for managing the worker threads.
  2. Modify the file reading example to read multiple files concurrently using different worker threads for each file. Use ExecutorService for managing the worker threads.
  3. Implement a simple web server using worker threads that can handle multiple client requests concurrently. Use an ExecutorService to manage the worker threads and a blocking queue to store incoming requests.
  4. Write a program that simulates a bank account with multiple accounts (each represented as a separate thread) performing transactions (deposits and withdrawals). Implement proper synchronization to ensure that the balance of each account is accurate and transactions are processed in the correct order.
  5. Implement a producer-consumer problem using worker threads where a producer generates random integers and a consumer processes them. Use blocking queues to pass data between the producer and consumer threads.

FAQ

Q: Why use worker threads instead of a single thread with a loop?

A: Using a single thread with a loop can lead to performance bottlenecks, especially when dealing with I/O-bound operations or long computations. Worker threads allow for concurrent execution of multiple tasks, improving the overall responsiveness and performance of the application.

Q: How do I ensure proper synchronization between worker threads?

A: Use synchronized blocks or atomic variables to protect shared data from race conditions and inconsistencies. You can also use higher-level concurrency utilities like CopyOnWriteArrayList for thread-safe list operations.

Q: What is the difference between Thread and ExecutorService in Java?

A: A Thread represents a single unit of execution, while an ExecutorService manages a pool of threads to execute multiple tasks concurrently. The main advantage of using ExecutorService is that it simplifies thread management by handling thread creation, task submission, and termination.

Q: How do I handle exceptions in worker threads?

A: You can use try-catch blocks to handle exceptions within the run() method or implement a custom exception handler that propagates errors back to the main thread for proper handling. Make sure to handle exceptions appropriately to ensure your application remains stable and responsive.

Q: How do I create a daemon thread in Java?

A: To create a daemon thread, call the setDaemon(true) method on the Thread instance before calling the start() method. Daemon threads are background threads that run when there are no user-created non-daemon threads left running. They are typically used for performing tasks that do not require user interaction or can be interrupted at any time.

Q: What is thread pooling, and why is it important?

A: Thread pooling is the practice of reusing a fixed number of threads to execute multiple tasks concurrently. It helps improve performance by reducing the overhead associated with thread creation and destruction. By using a thread pool, you can ensure that your application has enough threads to handle incoming requests while minimizing resource consumption

Worker Threads (Java) | Java | XQA Learn