Java tutorials > Multithreading and Concurrency > Threads and Synchronization > How to use `java.util.concurrent` package?

How to use `java.util.concurrent` package?

The java.util.concurrent package, introduced in Java 5, offers a rich set of tools and classes for concurrent programming, significantly simplifying the development of robust and efficient multithreaded applications. This tutorial explores key components of this package and demonstrates their usage with practical examples.

Introduction to `java.util.concurrent`

The java.util.concurrent package provides a higher level abstraction over the basic thread management facilities offered by Java. It includes features such as:

  • Executors: Manage and control the execution of tasks.
  • Queues: Thread-safe queue implementations.
  • Locks: More flexible alternatives to synchronized blocks.
  • Atomic Variables: Classes that support lock-free, thread-safe programming on single variables.
  • Concurrent Collections: Thread-safe versions of standard collections.

These tools help to avoid common pitfalls of multithreaded programming such as race conditions, deadlocks, and livelocks, making concurrent code easier to write and maintain.

Executors and ExecutorService

The ExecutorService interface represents an asynchronous execution mechanism which is capable of executing tasks concurrently. Executors class provide static methods to create various types of thread pools. Here's how you can use it:

  1. Create an ExecutorService: Use Executors.newFixedThreadPool(int) to create a thread pool with a fixed number of threads.
  2. Submit tasks: Use executor.execute(Runnable) to submit tasks for execution.
  3. Shutdown the executor: Call executor.shutdown() to prevent new tasks from being submitted and wait for existing tasks to complete.

In this example, newFixedThreadPool(5) creates a thread pool with 5 threads. Ten tasks are submitted to the executor, and each task is handled by one of the available threads.

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

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkerThread("Task " + i);
            executor.execute(worker);
          }
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

class WorkerThread implements Runnable {

    private String message;

    public WorkerThread(String s){
        this.message=s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" (Start) message = "+message);
        processmessage();// Simulate work
        System.out.println(Thread.currentThread().getName()+" (End)");
    }

    private void processmessage() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

Concepts Behind the Snippet (Executors)

The ExecutorService decouples task submission from task execution. Instead of creating and managing threads manually, you submit tasks to an executor, which then manages the thread lifecycle. This provides several benefits:

  • Improved performance: Thread creation is expensive. Executors reuse threads, reducing overhead.
  • Simplified code: You don't need to manually manage thread creation, starting, and stopping.
  • Better resource management: Executors allow you to control the number of threads used, preventing resource exhaustion.

Real-Life Use Case Section (Executors)

Consider a web server that handles incoming requests. Instead of creating a new thread for each request, the server can use an ExecutorService to manage a pool of threads that process requests concurrently. This significantly improves the server's ability to handle a large number of requests without creating an excessive number of threads.

Future Interface

The Future interface represents the result of an asynchronous computation. It provides methods to check if the computation is complete, to wait for its completion, and to retrieve the result. This is useful when you need to perform an operation that takes a long time and you don't want to block the main thread.

  1. Submit a Callable task: Use executor.submit(Callable) to submit a task that returns a value.
  2. Get the Future object: The submit() method returns a Future object that represents the result of the task.
  3. Retrieve the result: Use future.get() to retrieve the result. This method blocks until the result is available.

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

public class FutureExample {

    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        Callable<Integer> task = () -> {
            System.out.println("Executing task in thread: " + Thread.currentThread().getName());
            Thread.sleep(1000);
            return 42;
        };

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

        System.out.println("Task submitted. Waiting for result...");
        Integer result = future.get(); // Blocks until the result is available

        System.out.println("Result: " + result);
        executor.shutdown();
    }
}

Concurrent Collections

The java.util.concurrent package provides concurrent versions of standard collection classes, such as ConcurrentHashMap, ConcurrentLinkedQueue, and CopyOnWriteArrayList. These collections are thread-safe and designed for concurrent access.

ConcurrentHashMap is a thread-safe hash table implementation. Multiple threads can read and write to the map concurrently without external synchronization.

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        Map<String, Integer> map = new ConcurrentHashMap<>();

        // Multiple threads can safely read and write to the map
        Runnable task1 = () -> {
            for (int i = 0; i < 1000; i++) {
                map.put("Key" + i, i);
            }
        };

        Runnable task2 = () -> {
            for (int i = 1000; i < 2000; i++) {
                map.put("Key" + i, i);
            }
        };

        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);

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

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

        System.out.println("Map size: " + map.size()); // Output should be 2000
    }
}

Locks and Conditions

The java.util.concurrent.locks package provides more flexible locking mechanisms than the built-in synchronized keyword. The Lock interface provides methods for acquiring and releasing locks, and the Condition interface provides methods for waiting for specific conditions to be met.

  • ReentrantLock: A reentrant mutual exclusion Lock with the same basic behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements, but with extended capabilities.
  • Condition: Provides the ability for one thread to signal another thread that a specific condition has been met.

In this example, the waiter thread waits for the signaler thread to set the signal to true. The waiter thread releases the lock and waits on the condition until the signaler thread signals it. The Condition allows for more precise control over thread synchronization than using wait() and notify().

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

public class ConditionExample {

    private static final Lock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();
    private static boolean signal = false;

    public static void main(String[] args) {
        Thread waiter = new Thread(() -> {
            lock.lock();
            try {
                while (!signal) {
                    System.out.println("Waiter: Waiting for signal...");
                    condition.await(); // Releases the lock and waits
                }
                System.out.println("Waiter: Signal received!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread signaler = new Thread(() -> {
            lock.lock();
            try {
                Thread.sleep(2000); // Simulate some work
                signal = true;
                System.out.println("Signaler: Sending signal...");
                condition.signal(); // Wakes up a waiting thread
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        waiter.start();
        signaler.start();
    }
}

Atomic Variables

The java.util.concurrent.atomic package provides classes for performing atomic operations on single variables. These classes provide lock-free, thread-safe alternatives to using synchronized blocks for simple operations.

AtomicInteger is a class that represents an integer value that can be atomically updated. It provides methods such as incrementAndGet(), decrementAndGet(), and compareAndSet() for performing atomic operations.

In this example, two threads increment the counter variable concurrently. The AtomicInteger class ensures that the increments are atomic, preventing race conditions.

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {

    private static AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

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

        thread1.join();
        thread2.join();

        System.out.println("Counter value: " + counter.get()); // Expected: 2000
    }
}

Best Practices

  • Use Executors for managing threads: Avoid creating and managing threads manually. Executors provide a higher-level abstraction and handle thread lifecycle management.
  • Use Concurrent Collections for thread-safe data structures: Avoid using standard collections in concurrent environments without external synchronization.
  • Use Atomic Variables for simple atomic operations: Atomic variables provide lock-free, thread-safe alternatives to using synchronized blocks.
  • Understand the trade-offs: Choose the right tool for the job. Each class in the java.util.concurrent package has its own trade-offs in terms of performance and complexity.
  • Handle exceptions carefully: Be sure to handle exceptions properly in concurrent code to prevent unexpected behavior.

Interview Tip

When discussing concurrency in Java during interviews, emphasize your understanding of the java.util.concurrent package. Be prepared to explain the purpose of different classes and interfaces, and how they can be used to solve common concurrency problems. Also, be ready to discuss the trade-offs between different approaches, such as using synchronized blocks vs. using Lock objects.

When to use them

Use the java.util.concurrent package when you need to write concurrent code that is:

  • Efficient: The classes in this package are designed for high performance and low overhead.
  • Scalable: The classes in this package can handle a large number of concurrent operations.
  • Robust: The classes in this package help to prevent common concurrency problems such as race conditions and deadlocks.
  • Maintainable: The classes in this package provide a higher-level abstraction that makes concurrent code easier to write and maintain.

Memory footprint

The memory footprint of the java.util.concurrent package depends on the specific classes and interfaces that you use. However, in general, the memory overhead of using this package is relatively low. For example, AtomicInteger requires only a small amount of additional memory compared to a regular int. ConcurrentHashMap has a slightly higher memory overhead than HashMap, but it provides thread safety without requiring external synchronization.

Alternatives

Alternatives to using the java.util.concurrent package include:

  • synchronized blocks: The built-in synchronized keyword can be used to protect critical sections of code. However, synchronized blocks can be less flexible and less efficient than using Lock objects.
  • wait() and notify(): The wait() and notify() methods can be used to synchronize threads. However, these methods can be more difficult to use correctly than using Condition objects.
  • Custom thread management: You can create and manage threads manually. However, this approach is more complex and error-prone than using ExecutorService.

Pros

  • Improved performance: The classes in the java.util.concurrent package are designed for high performance and low overhead.
  • Simplified code: The classes in this package provide a higher-level abstraction that makes concurrent code easier to write and maintain.
  • Increased robustness: The classes in this package help to prevent common concurrency problems such as race conditions and deadlocks.

Cons

  • Increased complexity: The java.util.concurrent package can be more complex to learn and use than the basic thread management facilities offered by Java.
  • Potential for misuse: It's possible to misuse the classes in this package, leading to performance problems or unexpected behavior.

FAQ

  • What is the difference between ExecutorService.shutdown() and ExecutorService.shutdownNow()?

    shutdown() initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. shutdownNow() attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.

  • When should I use ReentrantLock instead of synchronized?

    Use ReentrantLock when you need more advanced features such as fairness, timed lock waits, or the ability to interrupt a thread waiting for the lock. The synchronized keyword is simpler and often sufficient for basic locking needs.

  • What is the purpose of Condition objects in the java.util.concurrent package?

    Condition objects provide a way for threads to suspend execution (wait) until a specific condition is true. They offer more control and flexibility than using wait() and notify() on an object's monitor.