Java MultiThreading Tutorial: From Threads to CompletableFutures

Java multithreading is a powerful feature that allows a program to perform multiple tasks concurrently. This can significantly improve the performance and responsiveness of applications, especially those dealing with I/O operations or complex computations. In this blog, we will start from the basics of creating and managing threads in Java and gradually move towards more advanced concepts like CompletableFutures, which provide a more convenient and flexible way to handle asynchronous operations.

Table of Contents

  1. Fundamental Concepts of Java MultiThreading
  2. Creating and Managing Threads
  3. Synchronization and Thread Safety
  4. Executor Service
  5. CompletableFutures
  6. Common Practices and Best Practices
  7. Conclusion
  8. References

Fundamental Concepts of Java MultiThreading

Thread

A thread is the smallest unit of execution in a program. In Java, a thread is represented by the Thread class. Each thread has its own call stack and can execute code independently.

Concurrency vs Parallelism

  • Concurrency: It is the ability of a program to handle multiple tasks at the same time, but not necessarily executing them simultaneously. For example, a single-core CPU can switch between multiple threads to give the illusion of concurrent execution.
  • Parallelism: It refers to the actual simultaneous execution of multiple tasks. This requires multiple CPU cores.

Asynchronous Programming

Asynchronous programming allows a program to continue executing other tasks while waiting for a particular task to complete. This is useful for I/O-bound operations where the program would otherwise be blocked.

Creating and Managing Threads

Extending the Thread Class

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running: " + Thread.currentThread().getName());
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

Implementing the Runnable Interface

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running: " + Thread.currentThread().getName());
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Synchronization and Thread Safety

When multiple threads access shared resources, there is a risk of data inconsistency. Synchronization is used to ensure that only one thread can access a shared resource at a time.

Synchronized Methods

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedExample {
    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();
        t2.join();

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

Executor Service

The ExecutorService is a higher-level abstraction for managing threads. It provides a pool of threads and allows you to submit tasks for execution.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorServiceExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.submit(() -> {
            System.out.println("Task 1 is running: " + Thread.currentThread().getName());
        });

        executorService.submit(() -> {
            System.out.println("Task 2 is running: " + Thread.currentThread().getName());
        });

        executorService.shutdown();
    }
}

CompletableFutures

CompletableFuture is a class introduced in Java 8 that provides a more convenient way to handle asynchronous operations. It allows you to chain multiple asynchronous tasks and handle the results in a more functional way.

Basic Usage

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello, World!";
        });

        String result = future.get();
        System.out.println(result);
    }
}

Chaining CompletableFutures

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureChainingExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "Hello";
        }).thenApply(s -> s + ", World!");

        String result = future.get();
        System.out.println(result);
    }
}

Common Practices and Best Practices

  • Use Thread Pools: Instead of creating threads manually, use thread pools provided by the ExecutorService to manage resources efficiently.
  • Avoid Deadlocks: Make sure to use synchronization carefully to avoid deadlocks, where two or more threads are blocked forever waiting for each other.
  • Handle Exceptions Properly: When using CompletableFutures, handle exceptions using methods like exceptionally or handle to prevent unexpected behavior.
  • Use CompletableFutures for Asynchronous Operations: For complex asynchronous tasks, CompletableFutures provide a more readable and maintainable way to handle the results.

Conclusion

Java multithreading is a powerful feature that can greatly enhance the performance and responsiveness of applications. Starting from basic thread creation and management, we have explored more advanced concepts like CompletableFutures. By understanding these concepts and following best practices, you can write more efficient and reliable multithreaded Java programs.

References