Java Concurrency: Advanced Features

Tomasz Niegowski

Introduction

In the previous article we introduced the basics of concurrency and multithreading in Java. But in the dynamic world of software development, where responsiveness and efficiency are paramount, harnessing the power of multithreading becomes imperative. The article “Java Concurrency: Advanced Features” delves into the intricacies of concurrent programming, shedding light on the advanced capabilities offered by the java.util.concurrent package.

While the basics of multithreading lay the foundation, the java.util.concurrent package elevates the game by providing a comprehensive toolkit for tackling complex concurrency challenges. This package serves as a treasure trove of specialized classes and utilities meticulously designed to streamline concurrent operations, enhance scalability, and mitigate potential pitfalls.

The article embarks on a journey to uncover the gems within the java.util.concurrent package, offering insights into its core classes and mechanisms. From thread-safe collections to thread pools, from synchronization primitives to high-level abstractions, this package empowers developers to wield the prowess of multithreading with precision and finesse.

java.util.concurrent package

The java.util.concurrent package in Java is a comprehensive collection of classes and interfaces that provide advanced concurrency utilities for managing multi-threaded programming. This package was introduced in Java 5 to address the complexities of concurrent programming and provide developers with tools to write efficient, scalable, and thread-safe applications. It offers a higher level of abstraction compared to traditional low-level synchronization mechanisms.

The java.util.concurrent package includes a wide range of classes and interfaces, each designed to address specific concurrency challenges.

Thread Pools

The package offers classes like ExecutorService and ThreadPoolExecutor that simplify the management of a pool of worker threads.

A thread pool in Java is a managed collection of pre-initialized worker threads that are ready to perform tasks concurrently. Thread pools are used to manage the execution of multiple tasks in a more efficient manner than creating a new thread for each task. They help to reduce the overhead of thread creation, control resource usage, and improve the overall performance of multithreaded applications.

Java provides the Executor framework to work with thread pools. The most commonly used implementation of Executor is the ExecutorService. This framework manages the pooling of threads, submission of tasks, and the execution of tasks on available threads.

Here’s an example of using a thread pool with the ExecutorService in Java:

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

public class ThreadPoolExample {

    public static void main(String[] args) {
        // Create a thread pool with a fixed number of threads
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // Submit tasks to the thread pool
        for (int i = 0; i < 10; i++) {
            executor.execute(new Task(i));
        }

        // Shutdown the thread pool when done
        executor.shutdown();
    }
}

class Task implements Runnable {
    private int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is being executed by thread: " + Thread.currentThread().getName());
    }
}

output:

Task 1 is being executed by thread: pool-1-thread-2
Task 2 is being executed by thread: pool-1-thread-3
Task 5 is being executed by thread: pool-1-thread-2
Task 6 is being executed by thread: pool-1-thread-3
Task 4 is being executed by thread: pool-1-thread-5
Task 3 is being executed by thread: pool-1-thread-4
Task 0 is being executed by thread: pool-1-thread-1
Task 9 is being executed by thread: pool-1-thread-5
Task 8 is being executed by thread: pool-1-thread-3
Task 7 is being executed by thread: pool-1-thread-2

In this example, we create a thread pool using Executors.newFixedThreadPool(5), which creates a pool of 5 threads. We then submit 10 tasks to the thread pool using the execute method. Each task is an instance of the Task class, which implements the Runnable interface. The run method of the Task class contains the code to be executed by each thread.

After submitting all tasks, we call executor.shutdown() to gracefully shut down the thread pool once all tasks are completed.

Thread pools are especially useful in scenarios where tasks are short-lived and can be executed concurrently. They help in managing the number of threads, avoiding excessive thread creation and destruction overhead, and efficiently utilizing system resources.

Thread-safe collections

Thread-safe collections in the java.util.concurrent package are data structures that are designed to be used safely in a multithreaded environment without the need for external synchronization. They ensure that concurrent access to the collection by multiple threads doesn’t result in data inconsistencies, race conditions, or other threading issues.

Here are some examples:

1. ConcurrentHashMap

It’s a thread-safe version of the HashMap class. It allows multiple threads to access the map concurrently without explicit synchronization.

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

    public static void main(String[] args) {
        Map<String, Integer> map = new ConcurrentHashMap<>();
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);

        int value = map.get("Two");
        System.out.println("Value for key 'Two': " + value);
    }
}

2. CopyOnWriteArrayList

It’s a thread-safe version of the ArrayList class. It allows concurrent read operations without synchronization, but write operations create a new copy of the underlying array.

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {

    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        list.add("One");
        list.add("Two");
        list.add("Three");

        for (String item : list) {
            System.out.println(item);
        }
    }
}

3. BlockingQueue

The BlockingQueue interface provides thread-safe queues that support blocking operations for adding and removing elements. Common implementations include LinkedBlockingQueue and ArrayBlockingQueue.

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueExample {

    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);

        try {
            queue.put(1);
            queue.put(2);
            queue.put(3);

            int item = queue.take();
            System.out.println("Removed: " + item);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

These are just a few examples of thread-safe collections in Java. Using thread-safe collections helps developers avoid the complexities of manual synchronization and ensures safe concurrent access to data structures, making multithreaded programming more manageable and less error-prone.

Synchronization primitives

Synchronization primitives in the java.util.concurrent package are classes or mechanisms that facilitate coordination and synchronization between multiple threads to ensure a proper execution order, prevent race conditions, and manage concurrent access to shared resources. These primitives provide a structured way for threads to communicate and synchronize their activities.

Here are examples of some commonly used synchronization primitives in Java:

1. Semaphore

A semaphore is a synchronization primitive that controls access to a shared resource through a set of permits. Threads can acquire and release permits to control their access to the resource.

import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2); // Initialize with 2 permits

        Runnable task = () -> {
            try {
                semaphore.acquire(); // Acquire a permit
                System.out.println(Thread.currentThread().getName() + " is accessing the resource");
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " released the resource");
                semaphore.release(); // Release the permit
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

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

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

2. CountDownLatch

A CountDownLatch is a synchronization primitive that allows one or more threads to wait until a specified number of operations (count) are completed before proceeding.

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3); // Initialize with 3 counts

        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName() + " is performing a task");
            latch.countDown(); // Decrease the count
        };

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

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

        latch.await(); // Wait until count becomes 0

        System.out.println("All tasks are completed");
    }
}

3. CyclicBarrier

A CyclicBarrier is a synchronization primitive that allows a set number of threads to wait for each other at a common barrier point before proceeding.

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {

  public static void main(String[] args) {
    CyclicBarrier barrier = new CyclicBarrier(3); // Initialize with 3 parties

    Runnable task = () -> {
      System.out.println(Thread.currentThread().getName() + " is waiting at the barrier");
      try {
        barrier.await(); // Wait at the barrier
        System.out.println(Thread.currentThread().getName() + " has passed the barrier");
      } catch (Exception e) {
        e.printStackTrace();
      }
    };

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

    thread1.start();
    thread2.start();
    thread3.start();
  }
}

4. Exchanger

An Exchanger is a synchronization primitive that provides a point of data exchange between two threads. It allows threads to exchange objects when they both reach the exchanger point.

import java.util.concurrent.Exchanger;

public class ExchangerExample {

  public static void main(String[] args) {
    Exchanger<String> exchanger = new Exchanger<>();

    Runnable task = () -> {
      try {
        System.out.println(Thread.currentThread().getName() + " is exchanging data");
        String data = exchanger.exchange("Data from " + Thread.currentThread().getName());
        System.out.println(Thread.currentThread().getName() + " received: " + data);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    };

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

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

These synchronization primitives provide powerful tools for managing synchronization, coordination, and communication between threads, enabling developers to build efficient and well-structured multithreaded applications.

Atomic operations

Atomic operations in the java.util.concurrent package are operations that are designed to be performed atomically without interference from other threads. These operations ensure that the value of a variable is read and updated in a single, indivisible step, preventing race conditions and ensuring thread safety. They are essential for managing shared data in multithreaded environments.

Java provides several classes in the java.util.concurrent.atomic package to perform atomic operations on primitive data types and reference types. These classes use low-level hardware support to ensure atomicity without the need for explicit synchronization.

Here are some commonly used atomic classes and their operations:

  • AtomicInteger – allows atomic operations on int values.
  • AtomicLong – allows atomic operations on long values.
  • AtomicReference – allows atomic operations on reference types.
  • AtomicBoolean – allows atomic operations on boolean values.

Atomic operations are especially useful when dealing with shared variables that need to be updated by multiple threads simultaneously. They eliminate the need for explicit synchronization and help in building efficient and thread-safe concurrent programs.

High-level synchronization classes

High-level synchronization classes in the java.util.concurrent package, such as Lock, ReentrantLock, and ReadWriteLock, provide more flexible and powerful mechanisms for managing concurrent access to shared resources compared to traditional synchronization using synchronized blocks/methods. These classes offer greater control over locking, fairness, and concurrency than the built-in synchronization constructs.

Lock Interface

The Lock interface provides a more advanced way to achieve synchronization than synchronized blocks/methods. It offers methods like lock() and unlock() for acquiring and releasing locks explicitly.

ReentrantLock

ReentrantLock is an implementation of the Lock interface that allows a thread to repeatedly lock and unlock the same lock, unlike the built-in synchronization which automatically re-enters the lock.

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

public class LockExample {

  public static void main(String[] args) {
    Lock lock = new ReentrantLock();

    Runnable task = () -> {
      lock.lock(); // Acquire the lock
      try {
        // Critical section
      } finally {
        lock.unlock(); // Release the lock
      }
    };

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

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

ReadWriteLock

The ReadWriteLock interface provides a mechanism for managing read and write access to a shared resource. It allows multiple threads to have concurrent read access, while write access is exclusive.

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {

  public static void main(String[] args) {
    ReadWriteLock rwLock = new ReentrantReadWriteLock();

    Runnable readTask = () -> {
      rwLock.readLock().lock();
      try {
        // Read the shared resource
      } finally {
        rwLock.readLock().unlock();
      }
    };

    Runnable writeTask = () -> {
      rwLock.writeLock().lock();
      try {
        // Modify the shared resource
      } finally {
        rwLock.writeLock().unlock();
      }
    };

    Thread reader1 = new Thread(readTask);
    Thread reader2 = new Thread(readTask);
    Thread writer = new Thread(writeTask);

    reader1.start();
    reader2.start();
    writer.start();
  }
}

High-level synchronization mechanisms like Lock, ReentrantLock, and ReadWriteLock offer more control and flexibility in managing concurrent access to shared resources, making it easier to build efficient and thread-safe applications.

Summary

Java’s concurrency capabilities empower developers to create high-performance applications that harness the power of multithreading. Navigating synchronization, managing shared resources, and orchestrating thread interactions require a deep understanding of the tools and challenges involved. Through synchronized methods, volatile variables, locks, and sophisticated mechanisms like thread pools, Java provides an extensive toolkit for effective concurrent programming. Armed with this knowledge, developers can confidently embrace the world of multithreading, optimizing applications to meet the demands of modern computing. By mastering Java concurrency, you’ll be well-equipped to unlock the true potential of parallel execution, transforming your code into efficient, responsive, and robust software solutions.

Reference

Meet the geek-tastic people, and allow us to amaze you with what it's like to work with j‑labs!

Contact us