โ† Back to Multithreading Course

Class 46: Building a Real-World Multithreaded Java Application

Part 8 โ€” Reliability, Debugging and Real-World Applications

๐ŸŽ‰ Congratulations! You Made It to the Final Class!

Before we begin, take a moment to appreciate how far you have come.

You started this course by learning what a thread is and how to create one in Java.

From there, you gradually explored synchronization, shared data, thread communication, thread pools, concurrent collections, asynchronous programming, virtual threads, debugging, and much more.

And now, you are here โ€” Class 46, the final class of our Java Multithreading course!

46 classes. 8 parts. One complete learning journey.

Well done! Give yourself a pat on the back. Completing a course is an achievement, but understanding what you have learned and applying it in real projects is where the real growth begins.

Let's finish this journey by building something practical and bringing our learning together.

1. What Are We Going to Build?

Imagine that we are building a simple order processing system for an online shopping application.

Customers place orders, and our application needs to process them.

Suppose five customers place five orders.

We could process these orders one by one. But that would mean waiting for each order to finish before starting the next one.

Instead, we can use multiple worker threads to process different orders concurrently.

Our application will:

This is a simplified example of how we can apply multithreading concepts in an application.

2. Understanding the Application Design

Let's first understand the different responsibilities.

Order

Represents an order placed by a customer. It contains an order ID and a product name.

OrderProcessor

Represents the task responsible for processing an individual order.

It implements Callable because we want each task to return a result after processing.

OrderService

Manages the execution of order-processing tasks. It submits tasks, collects their results, and manages the executor's lifecycle.

Shared Data

Multiple worker threads update the processed-order count and order-status collection.

We will use AtomicInteger and ConcurrentHashMap to handle these shared updates safely.

3. Create the Order Class

Let's start with a simple Order class.

public class Order {

    private final int orderId;
    private final String productName;

    public Order(int orderId, String productName) {

        this.orderId = orderId;
        this.productName = productName;
    }

    public int getOrderId() {

        return orderId;
    }

    public String getProductName() {

        return productName;
    }
}

Notice that we have used private final fields.

Once an Order object is created, its data cannot be changed through this class.

This makes it easier to share Order objects between threads because their state is immutable.

4. Create the OrderProcessor Class

Now let's create the task that processes an order.

import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class OrderProcessor implements Callable<String> {

    private final Order order;

    private final AtomicInteger processedCount;

    private final ConcurrentHashMap<Integer, String> orderStatuses;

    public OrderProcessor(
            Order order,
            AtomicInteger processedCount,
            ConcurrentHashMap<Integer, String> orderStatuses) {

        this.order = order;
        this.processedCount = processedCount;
        this.orderStatuses = orderStatuses;
    }

    @Override
    public String call() throws Exception {

        String threadName =
                Thread.currentThread().getName();

        try {

            orderStatuses.put(
                order.getOrderId(),
                "PROCESSING"
            );

            System.out.println(
                threadName + " started processing Order "
                + order.getOrderId()
            );

            // Simulate order processing time
            Thread.sleep(1000);

            orderStatuses.put(
                order.getOrderId(),
                "COMPLETED"
            );

            int totalProcessed =
                    processedCount.incrementAndGet();

            return "Order " + order.getOrderId()
                    + " for " + order.getProductName()
                    + " completed by " + threadName
                    + ". Total processed: " + totalProcessed;

        } catch (InterruptedException e) {

            orderStatuses.put(
                order.getOrderId(),
                "INTERRUPTED"
            );

            Thread.currentThread().interrupt();

            throw e;
        }
    }
}

Let's understand what is happening here.

First, our OrderProcessor implements Callable with String as its return type.

This means that the call() method returns a String containing the processing result.

We also receive three objects through the constructor:

Multiple OrderProcessor tasks share these objects.

Let's understand why we chose these particular classes.

Why AtomicInteger?

Multiple threads can complete their orders at approximately the same time.

We want to increment the processed-order count safely.

Therefore, we use:

processedCount.incrementAndGet();

This performs an atomic increment operation.

Why ConcurrentHashMap?

Multiple threads update the order-status collection.

A normal HashMap is not designed for concurrent updates without appropriate synchronization.

ConcurrentHashMap provides thread-safe operations for concurrent access.

Therefore, we use it to store order statuses.

5. Create the OrderService Class

Now let's create the service responsible for managing the order-processing tasks.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class OrderService {

    private final AtomicInteger processedCount =
            new AtomicInteger(0);

    private final ConcurrentHashMap<Integer, String> orderStatuses =
            new ConcurrentHashMap<>();

    public void processOrders(List<Order> orders)
            throws InterruptedException {

        ExecutorService executor =
                Executors.newFixedThreadPool(3);

        List<Future<String>> futures =
                new ArrayList<>();

        try {

            // Submit all orders for processing
            for (Order order : orders) {

                Callable<String> task =
                        new OrderProcessor(
                            order,
                            processedCount,
                            orderStatuses
                        );

                Future<String> future =
                        executor.submit(task);

                futures.add(future);
            }

            // No more tasks will be submitted
            executor.shutdown();

            // Collect the results
            for (Future<String> future : futures) {

                try {

                    String result = future.get();

                    System.out.println(result);

                } catch (ExecutionException e) {

                    System.out.println(
                        "Order processing failed: "
                        + e.getCause()
                    );
                }
            }

            // Wait for executor termination
            if (!executor.awaitTermination(
                    10, TimeUnit.SECONDS)) {

                System.out.println(
                    "Processing is taking too long."
                );

                executor.shutdownNow();
            }

        } catch (InterruptedException e) {

            executor.shutdownNow();

            Thread.currentThread().interrupt();

            throw e;

        } finally {

            if (!executor.isTerminated()) {

                executor.shutdownNow();
            }
        }

        System.out.println(
            "Total completed orders: "
            + processedCount.get()
        );

        System.out.println(
            "Final order statuses: "
            + orderStatuses
        );
    }
}

This is where we bring several concepts together.

Creating the Thread Pool

ExecutorService executor =
        Executors.newFixedThreadPool(3);

We create a fixed thread pool containing three worker threads.

This means that up to three order-processing tasks can execute concurrently.

If more than three tasks are submitted, the remaining tasks wait in the executor's work queue until a worker becomes available.

Submitting Tasks

Future<String> future =
        executor.submit(task);

We submit each OrderProcessor task to the ExecutorService.

Because the task implements Callable, submit() returns a Future containing the task's result.

Collecting Results

String result = future.get();

We use get() to retrieve the result of each task.

Remember that get() blocks until that particular task completes.

If a task fails, get() throws ExecutionException. We can inspect its cause to understand the original failure.

Shutting Down the Executor

executor.shutdown();

Once all tasks have been submitted, we call shutdown().

This prevents new tasks from being accepted while allowing already submitted tasks to complete.

We then use awaitTermination() to wait for the executor to finish its work.

If the timeout expires, shutdownNow() requests interruption of active tasks and attempts to stop queued tasks from starting.

It does not forcibly terminate threads.

6. Create the Main Class

Finally, let's create the main class that starts our application.

import java.util.Arrays;
import java.util.List;

public class Main {

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

        List<Order> orders = Arrays.asList(

            new Order(101, "Laptop"),

            new Order(102, "Mobile Phone"),

            new Order(103, "Headphones"),

            new Order(104, "Keyboard"),

            new Order(105, "Monitor")
        );

        OrderService orderService =
                new OrderService();

        orderService.processOrders(orders);
    }
}

That's it!

We have now created a simple multithreaded order-processing application.

7. Understanding the Complete Execution Flow

Let's understand what happens when we run Main.

  1. The main thread creates five Order objects.
  2. OrderService creates a fixed thread pool with three workers.
  3. Each order is submitted as a Callable task.
  4. The executor assigns tasks to available worker threads.
  5. Each worker processes an order concurrently.
  6. AtomicInteger safely tracks the number of completed orders.
  7. ConcurrentHashMap safely stores the order statuses.
  8. Future objects allow the main thread to collect task results.
  9. The executor is shut down gracefully.
  10. The application displays the final processing count and order statuses.

Notice that we have five orders but only three worker threads.

Therefore, the executor can process up to three orders concurrently, while the remaining orders wait for an available worker.

8. What Will the Output Look Like?

Your output may look similar to this:

pool-1-thread-1 started processing Order 101
pool-1-thread-2 started processing Order 102
pool-1-thread-3 started processing Order 103

Order 101 for Laptop completed by pool-1-thread-1. Total processed: 1
Order 102 for Mobile Phone completed by pool-1-thread-2. Total processed: 2
Order 103 for Headphones completed by pool-1-thread-3. Total processed: 3

pool-1-thread-1 started processing Order 104
pool-1-thread-2 started processing Order 105

Order 104 for Keyboard completed by pool-1-thread-1. Total processed: 4
Order 105 for Monitor completed by pool-1-thread-2. Total processed: 5

Total completed orders: 5

Final order statuses: {
    101=COMPLETED,
    102=COMPLETED,
    103=COMPLETED,
    104=COMPLETED,
    105=COMPLETED
}

Your actual output may be different.

The order in which threads start and finish is not guaranteed.

Even though we collect Future results in submission order, the tasks themselves execute concurrently.

This is an important characteristic of multithreaded applications.

9. Which Concepts Did We Use?

Let's connect our application to the concepts we learned throughout this course.

Java Concept How We Used It
Thread Worker threads execute order-processing tasks.
Callable Each order-processing task returns a result.
ExecutorService Manages task execution.
Fixed Thread Pool Limits concurrent worker threads to three.
Future Retrieves task results.
AtomicInteger Maintains a thread-safe processed-order count.
ConcurrentHashMap Stores order statuses safely across threads.
Thread Interruption Allows processing tasks to respond to interruption.
Graceful Shutdown Ensures the executor is shut down properly.
Immutable Objects Order data is not modified after creation.

10. How Can We Improve This Application?

Our application is intentionally simple.

A real-world order-processing system would require additional features.

For example:

We could also introduce concepts such as CompletableFuture, BlockingQueue, Semaphore, and CountDownLatch depending on the requirements.

But remember something important:

We should not use every concurrency feature simply because it exists.

We should choose the simplest tool that solves the actual problem.

That is an important part of becoming a good software engineer.

11. Final Revision โ€” What Have We Learned?

Let's take a moment to revisit the complete journey of this course.

Test Your Understanding

Let's see how well you understood this final class!

Question 1: Why did we use a fixed thread pool of three threads?

Question 2: Why did OrderProcessor implement Callable<String>?

Question 3: Why did we use AtomicInteger?

Question 4: Why did we use ConcurrentHashMap?

Question 5: What does executor.shutdown() do?

Final Class Summary

In this class, we built a simple multithreaded order-processing application.

  • We used ExecutorService to manage worker threads.
  • We used Callable to execute tasks that return results.
  • We used Future to collect task results.
  • We used AtomicInteger to maintain a thread-safe counter.
  • We used ConcurrentHashMap to store shared order statuses.
  • We handled task failures and interruption.
  • We shut down the executor properly.

Most importantly, we saw how individual multithreading concepts can work together to solve a practical problem.

๐ŸŽ“ You Did It! You Completed the Java Multithreading Course!

Congratulations on completing all 46 classes of Java Multithreading at Full Stack School!

We started with the basics of threads and gradually moved towards advanced concurrency concepts and practical application development.

It was a journey from understanding what a thread is to building an application that uses multiple threads to process work concurrently.

I hope you enjoyed this journey and, more importantly, developed a clear understanding of Java multithreading.

But remember, completing a course is not the end of learning.

Now is the time to practise.

Experiment with the examples, modify the applications, break things, debug them, and build something of your own.

Do not worry about remembering every method or every class.

Focus on understanding the concepts and knowing which tool to use when a problem requires it.

Keep practising, keep building, and keep improving one step at a time.

Thank you for learning with Full Stack School.

I wish you all the very best in your Java development journey.

What's Next?

You can now explore other courses at Full Stack School, strengthen your Java fundamentals, and apply your multithreading knowledge in Spring Boot and real-world backend applications.

Keep learning. Keep practising. Keep building.

Lots of love and best wishes โ€” Full Stack School.

Learn. Practise. Build. ๐Ÿš€

๐Ÿ† TAKE THE FINAL COURSE ASSESSMENT 50 Questions ยท Challenge Yourself on Everything You've Learned
0% Completed

You have completed 46 out of 46 classes.

๐Ÿ† Course Completed!

Java Multithreading โ€” 46 / 46 Classes

You have successfully reached the end of this course. Be proud of the effort you have put into learning these concepts.

Your next achievement will not come from simply finishing another lesson. It will come from using what you have learned to build something meaningful.

Keep going. Your learning journey continues!