How do I detect and avoid deadlocks in concurrent Java applications?

To detect and avoid deadlocks in concurrent Java applications, you need to understand what causes deadlocks and how to mitigate them effectively. Here’s a structured explanation:


What is a Deadlock?

A deadlock occurs when two or more threads are waiting for each other’s resources indefinitely, preventing further progress. For example:

  1. Thread A locks Resource 1 and then waits for Resource 2.
  2. Thread B locks Resource 2 and then waits for Resource 1.

This creates a cyclic dependency, leading to a deadlock.


How to Detect Deadlocks in Java

  1. Using Thread Dumps
    • Generate a thread dump of the JVM during execution (On Unix/Linux: kill -3 <pid>, on Windows: use tools like jstack or Ctrl+Break in the command line).
    • Look for “deadlock detected” or analyze threads that are in the BLOCKED state and the resources they are waiting for.
  2. Using jconsole or VisualVM
    • Attach jconsole or VisualVM to your application.
    • Use the “Threads” view to identify deadlocks visually.
  3. Programmatically with java.lang.management.ThreadMXBean
    • Java provides a ThreadMXBean to monitor and detect deadlocks:
    package org.kodejava.util.concurrent;
    
    import java.lang.management.ManagementFactory;
    import java.lang.management.ThreadMXBean;
    
    public class DeadlockDetector {
      public static void main(String[] args) {
         ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
         long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
         if (deadlockedThreads != null) {
            System.out.println("Deadlock detected!");
         } else {
            System.out.println("No deadlocks detected.");
         }
      }
    }
    
  4. Using IDE Debuggers
    • Use IntelliJ Debugger or Eclipse Debugger to pause your threads and inspect locked resources or deadlock issues.

How to Avoid Deadlocks

  1. Adhere to Resource Lock Ordering
    • Always acquire resources in a consistent global order.
    • Example: If two threads need Resource A and Resource B, ensure they always lock Resource A before Resource B in the same order.
  2. Use tryLock with Timeout
    • Use ReentrantLock from java.util.concurrent.locks to attempt acquiring locks with a timeout, avoiding indefinite blocking:
    package org.kodejava.util.concurrent;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class LockExample {
      private final ReentrantLock lock1 = new ReentrantLock();
      private final ReentrantLock lock2 = new ReentrantLock();
    
      public void task1() {
         try {
            if (lock1.tryLock() && lock2.tryLock()) {
               // Perform work
            }
         } finally {
            if (lock1.isHeldByCurrentThread()) lock1.unlock();
            if (lock2.isHeldByCurrentThread()) lock2.unlock();
         }
      }
      // Similarly for task2
    }
    
  3. Minimize Lock Scope
    • Reduce the amount of time locks are held to the absolute minimum.
  4. Avoid Nested Locks
    • Refrain from acquiring a lock inside a block of code that holds another lock, where possible.
  5. Use Higher-Level Concurrency Utilities
    • Instead of manually managing locks, use high-level utilities like:
      • java.util.concurrent.ExecutorService for managing threads.
      • java.util.concurrent.Semaphore or java.util.concurrent.CountDownLatch for synchronization.
  6. Detect and Handle Circular Dependencies
    • Identify possible resource dependencies during code design and avoid cyclic locking.
  7. Thread Dump Analysis During Testing
    • Regularly analyze thread dumps in test environments to identify potential deadlocks before releasing the application.

Conclusion

By carefully managing threads and resources using the techniques above, you can both detect and avoid deadlocks in Java applications. Use tools such as thread dumps, jconsole, and high-level concurrency APIs to simplify development and debugging.

How do I build a work-stealing pool with ForkJoinPool?

Building a work-stealing pool using the ForkJoinPool in Java is straightforward, as the ForkJoinPool class natively supports the work-stealing mechanism. Work-stealing allows idle threads to “steal” tasks from the queues of other busy threads, increasing the efficiency of the processing.

Here’s how you can create and use a work-stealing pool with ForkJoinPool:


1. Understanding ForkJoinPool

  • A ForkJoinPool is designed for tasks that can be recursively divided into smaller subtasks (i.e., the “fork” step). These subtasks may then be processed in parallel by multiple threads in the pool.
  • If some threads are idle, they can “steal” tasks from the queues of other threads (i.e., the “work-stealing” part).

2. Creating a ForkJoinPool

To create the pool:

  • Use the ForkJoinPool constructor with a desired parallelism level (number of threads in the pool).
  • You can also use the ForkJoinPool.commonPool(), a shared instance available to your application.

Example:

int parallelism = Runtime.getRuntime().availableProcessors(); // Number of threads in the pool
ForkJoinPool forkJoinPool = new ForkJoinPool(parallelism);

3. Submitting Tasks to the ForkJoinPool

Create tasks using RecursiveTask<T> for tasks that return a result, or RecursiveAction for tasks that do not return a result.

These tasks implement the compute() method, which contains the logic for splitting and processing the tasks.


4. Example: Using RecursiveTask

Here is an example of using a ForkJoinPool with work-stealing to calculate the sum of a large array:

package org.kodejava.util.concurrent;

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class WorkStealingExample {
   // RecursiveTask to compute the sum of an array
   static class SumTask extends RecursiveTask<Long> {
      private static final int THRESHOLD = 1_000; // Threshold for splitting tasks
      private final int[] array;
      private final int start, end;

      public SumTask(int[] array, int start, int end) {
         this.array = array;
         this.start = start;
         this.end = end;
      }

      @Override
      protected Long compute() {
         if ((end - start) <= THRESHOLD) {
            // Base case: process the task directly
            long sum = 0;
            for (int i = start; i < end; i++) {
               sum += array[i];
            }
            return sum;
         } else {
            // Split task: fork/join
            int mid = (start + end) / 2;
            SumTask leftTask = new SumTask(array, start, mid);
            SumTask rightTask = new SumTask(array, mid, end);

            // Fork the subtasks
            leftTask.fork(); // Fork the left task
            Long rightResult = rightTask.compute(); // Process the right task directly
            Long leftResult = leftTask.join(); // Wait for the left task to complete

            // Combine the results
            return leftResult + rightResult;
         }
      }
   }

   public static void main(String[] args) {
      int[] array = new int[10_000_000];
      for (int i = 0; i < array.length; i++) {
         array[i] = i + 1; // Initialize array
      }

      long result;
      // Default pool size: available processors
      try (ForkJoinPool pool = new ForkJoinPool()) {
         SumTask task = new SumTask(array, 0, array.length);

         // Submit and retrieve the result
         result = pool.invoke(task);
      }

      System.out.println("Total Sum: " + result);
   }
}

5. Key Points in the Example

  1. Threshold-Based Splitting:
    • The THRESHOLD constant defines at what point tasks are small enough to process directly.
    • Larger tasks are split into smaller subtasks (forked) recursively.
  2. Fork/Join Paradigm:
    • fork(): Spawns a new subtask asynchronously.
    • compute(): Performs computation directly or splits into subtasks.
    • join(): Waits for a subtask’s result.
  3. Work-Stealing:
    • If a thread finishes its tasks early, it “steals” tasks from other busy threads, making use of all available processors efficiently.

6. Using the Common ForkJoinPool

You can alternatively use the common pool (a shared ForkJoinPool):

ForkJoinPool.commonPool().invoke(task);

The common pool is created globally with threads equal to the number of processors by default.


7. Tuning the ForkJoinPool

You can fine-tune the pool by providing custom parameters, such as:

  • parallelism: Number of worker threads.
  • ForkJoinPool.ManagedBlocker: For handling thread blocking when using external resources.

Example:

ForkJoinPool pool = new ForkJoinPool(4); // Create a pool with 4 threads

This approach enables parallel computation with efficient load balancing and idle idle-thread utilization via work-stealing.

How do I throttle concurrent threads using ThreadPoolExecutor settings?

To throttle concurrent threads using ThreadPoolExecutor settings in Java, you can configure its key parameters: core pool size, maximum pool size, and queue capacity. These settings control how ThreadPoolExecutor manages the number of concurrently running threads and queued tasks.

Explanation of Key ThreadPoolExecutor Settings:

  1. Core Pool Size:
    • This defines the number of threads that are kept in the pool even when they are idle.
    • If the number of actively running threads is less than the core pool size, a new thread is created to handle a task, even if there are idle threads.
  2. Maximum Pool Size:
    • This is the maximum number of threads that can exist in the pool.
    • If the pool reaches this limit, tasks are queued instead of creating new threads.
  3. Queue Capacity:
    • A BlockingQueue is used to hold tasks that are waiting to execute.
    • If the queue is full and the number of active threads is already at the maximum pool size, new tasks will be rejected according to the specified RejectedExecutionHandler.

By adjusting these parameters, you can throttle the number of active threads, controlling concurrency.


Steps to Throttle Threads:

  1. Use a Fixed Maximum Pool Size:
    Set a value for corePoolSize and maximumPoolSize, controlling the maximum number of threads allowed to execute concurrently.

  2. Configure the Queue Size:
    Use a bounded queue (e.g., ArrayBlockingQueue) with a fixed size to limit the number of pending tasks. Once the queue is full, no additional tasks will be accepted unless threads become available.

  3. Avoid Overloading the System:
    Ensure that the total number of threads and tasks in the queue doesn’t overwhelm system resources like CPU or memory.


Example Solution:

package org.kodejava.util.concurrent;

import java.util.concurrent.*;

public class ThreadPoolThrottle {
   public static void main(String[] args) {
      // Define Executor settings
      int corePoolSize = 5;  // Minimum threads
      int maxPoolSize = 10;  // Maximum threads
      int queueCapacity = 20; // Queue size
      long keepAliveTime = 1; // Threads idle time in seconds

      // Create a ThreadPoolExecutor
      ThreadPoolExecutor executor = new ThreadPoolExecutor(
              corePoolSize,
              maxPoolSize,
              keepAliveTime,
              TimeUnit.SECONDS,
              new ArrayBlockingQueue<>(queueCapacity),
              new ThreadPoolExecutor.CallerRunsPolicy() // Rejected tasks run in the caller thread
      );

      // Submit tasks to throttle
      for (int i = 0; i < 50; i++) {
         final int taskID = i;
         executor.execute(() -> {
            try {
               System.out.println("Task " + taskID + " is running");
               Thread.sleep(1000); // Simulate work
            } catch (InterruptedException e) {
               Thread.currentThread().interrupt();
            }
         });
      }

      // Shut down the executor
      executor.shutdown();
   }
}

Key Points in the Example:

  1. The corePoolSize is 5, meaning at least 5 threads are always active.
  2. The maximum number of threads is limited to maxPoolSize, which is 10 threads.
  3. ArrayBlockingQueue with a size of 20 prevents too many pending tasks from being enqueued at once.
  4. RejectedExecutionHandler.CallerRunsPolicy ensures that tasks are executed in the caller thread when the queue is full, preventing silent task rejection.

Resulting Throttling Behavior:

  1. No more than 10 threads will run concurrently.
  2. A maximum of 20 tasks will be queued at any time.
  3. Tasks beyond the queue/throttle limit are forced to run in the caller thread or handled by a custom rejection policy.

By tweaking these settings, you can fine-tune thread throttling behavior for specific performance and resource requirements.

How do I safely share data between threads with ConcurrentHashMap?

When working with a multithreaded application, ConcurrentHashMap is a great choice for safely sharing data between threads. It is a thread-safe version of a HashMap that provides high concurrency for both retrieval and updates. Here are some guidelines to safely use a ConcurrentHashMap in a multithreaded environment:


1. Use Thread-Safe Access Operations

ConcurrentHashMap ensures that operations like put(), get(), remove(), containsKey() are thread-safe. Unlike HashMap, you can safely use these methods concurrently across multiple threads without additional synchronization.

package org.kodejava.util.concurrent;

import java.util.concurrent.ConcurrentHashMap;

public class ExampleConcurrentHashMap {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);

        // Reading and updating the map from multiple threads
        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName());
            Integer value = map.get("key1");
            if (value != null) {
                map.put("key1", value + 1);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

This code works safely across threads because the put() and get() operations are thread-safe.


2. Avoid Compound Operations

While individual operations like put() and get() are thread-safe, compound operations (operations that consist of multiple actions, e.g., check-then-act) are not atomic by default. For example, the following code might fail in a multithreaded scenario:

if (!map.containsKey("key")) {  // Thread 1 might pass this check
    map.put("key", 42);         // Thread 2 might also pass this check before Thread 1 puts the value
}

To perform compound operations atomically, use methods provided by ConcurrentHashMap, such as putIfAbsent(), compute(), or merge().

Example: Use putIfAbsent

map.putIfAbsent("key", 42); // Ensures that "key" is inserted only if it isn't already present

Example: Use compute

map.compute("key", (k, v) -> (v == null) ? 1 : v + 1);
// Safely updates the value of "key" atomically

Example: Use merge

map.merge("key", 1, Integer::sum);
// Combines a new value with the existing value of "key" in a thread-safe manner

3. Leverage Concurrent Iteration

ConcurrentHashMap allows thread-safe iteration over its entries using iterators. However, note that the iterator reflects the state of the map at the moment it was created. Any changes made to the map by other threads after the iterator creation will not throw ConcurrentModificationException, but they may or may not be seen during iteration.

Safe Iteration Example

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);

map.forEach((key, value) -> {
    System.out.println(key + ": " + value);
});

Iterating and updating simultaneously can still be done safely through operations like compute() or computeIfPresent() within the iteration.


4. Understand Default Concurrency Level

ConcurrentHashMap partitions the map into segments internally to reduce contention among threads. You can adjust the level of concurrency (number of segments) by specifying it during construction, but the default value is sufficient for most use cases.

Custom Concurrency Level Example:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(16, 0.75f, 32);
// 32 is the concurrency level (number of threads allowed to modify without contention)

5. Use Bulk Operations for Performance

ConcurrentHashMap includes bulk operations like forEach(), reduce(), and search(). These operations are implemented to efficiently work with large volumes of data in a concurrent environment.

Example: Use forEach

map.forEach(1, (key, value) -> {
    System.out.println(key + ": " + value);
});
// The first parameter is parallelismThreshold (minimum size to make it parallelizable)

Example: Use reduce

Integer sum = map.reduceValues(1, Integer::sum);
System.out.println("Sum of all values: " + sum);

6. Avoid Manual Synchronization

Avoid adding explicit locks like synchronized or ReentrantLock with ConcurrentHashMap, as this can lead to deadlocks or significantly hinder performance. Instead, rely on the built-in atomic methods provided by the class.


7. Be Aware of Null Restrictions

Unlike HashMap, ConcurrentHashMap does not support null keys or null values. If you try to use null, it will throw a NullPointerException. Use valid non-null keys and values at all times.


Conclusion

ConcurrentHashMap is a powerful and flexible tool for managing shared data across multiple threads. To use it safely and efficiently:

  1. Use atomic methods like putIfAbsent, compute, or merge for compound operations.
  2. Avoid manual synchronization.
  3. Leverage bulk operations for large datasets.
  4. Handle data consistently without assuming atomicity for compound actions unless explicitly supported by the API.

By following these guidelines, you can minimize race conditions and improve the safety and performance of your multithreaded application.

How do I use ConcurrentHasMap forEach() method?

The forEach() method in ConcurrentHashMap is used for iteration over the entries in the map. The method takes a BiConsumer as an argument, which is a functional interface that represents an operation that accepts two input arguments and returns no result.

Here’s an example of how to use forEach() with a ConcurrentHashMap:

package org.kodejava.util.concurrent;

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapForEachExample {
    public static void main(String[] args) {
        // Create a new ConcurrentHashMap
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // Add some key-value pairs
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);
        map.put("Four", 4);

        // Use forEach to iterate over the ConcurrentHashMap.
        // The BiConsumer takes a key (k) and value (v), and we're
        // just printing them here.
        map.forEach((k, v) -> System.out.println("Key: " + k + ", Value: " + v));
    }
}

Output:

Key: One, Value: 1
Key: Four, Value: 4
Key: Two, Value: 2
Key: Three, Value: 3

In the above example, forEach() is used to iterate over the entries of the map. For each entry, the key and value are printed. The forEach() method is often more convenient to use than an iterator, especially when you’re only performing a single operation (like print) for each entry in the map.

What is ConcurrentHashMap and how do I use it in Java?

The ConcurrentHashMap is a class in Java that implements the ConcurrentMap interface. It is part of the Java Collection Framework and extends the AbstractMap class.

ConcurrentHashMap is thread-safe, which means it is designed to support high concurrency levels by handling multiple threads concurrently without any inconsistencies. It allows multiple threads to perform retrieve (get) and update (insert & delete) operations. Internally, ConcurrentHashMap uses concepts of Segmentation to store data which allows higher degree of concurrency.

Here is an example of how to use ConcurrentHashMap in Java:

package org.kodejava.util;

import java.util.concurrent.ConcurrentHashMap;

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

        // Add elements
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);

        // Retrieve elements
        Integer one = map.get("One");
        System.out.println("Retrieved value for 'One': " + one);

        // Remove an element
        map.remove("Two");

        // Print all elements
        map.forEach((key, value) -> System.out.println(key + " = " + value));
    }
}

Output:

Retrieved value for 'One': 1
One = 1
Three = 3

In this example, we’re creating a ConcurrentHashMap, adding some elements to it, retrieving an element, removing an element, and finally printing all the elements.

One thing to note is that while ConcurrentHashMap allows multiple threads to read and write concurrently, a get() operation might not reflect the latest put() operation, since it might be looking at a previous segment. Further thread synchronization mechanisms might be necessary depending on your exact use case.

Also, worth mentioning, null values and null keys are not permitted in ConcurrentHashMap to prevent ambiguities and potential errors in multithreaded contexts. If you try to use null, ConcurrentHashMap will throw a NullPointerException.

Here’s an example demonstrating the usage of ConcurrentHashMap in a multithreaded context:

package org.kodejava.util;

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

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

        // Create a ThreadPool with 5 threads
        try (ExecutorService executor = Executors.newFixedThreadPool(5)) {

            // Runnable task to increment a value in the map
            Runnable task = () -> {
                for (int i = 0; i < 10; i++) {
                    map.compute("TestKey", (key, value) -> {
                        if (value == null) {
                            return 1;
                        } else {
                            return value + 1;
                        }
                    });
                }
            };

            // Submit the task to each thread in the pool
            for (int i = 0; i < 5; i++) {
                executor.submit(task);
            }

            // Shut down the executor and wait for tasks to complete
            executor.shutdown();
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        }

        System.out.println("Final value for 'TestKey': " + map.get("TestKey"));
    }
}

Output:

Final value for 'TestKey': 50

In this example, we’re creating a ConcurrentHashMap and a thread pool with ExecutorService. We’re then defining a Runnable task, which increments the value of the “TestKey” key in the map 10 times.

The task uses ConcurrentHashMap‘s compute() method, which is atomic, meaning that the retrieval and update of the value is done as a single operation that cannot be interleaved with other operations. We then submit the task to each of the five threads in our thread pool. After all threads have completed their tasks, we retrieve and print the final value of “TestKey”.

If everything works correctly, the output should be “Final value for ‘TestKey’: 50”, because we have 5 threads each incrementing the value 10 times. This demonstrates the thread-safety of ConcurrentHashMap, as the compute() operation is done atomically and many threads were able to modify the map simultaneously without causing inconsistencies. If we were using a plain HashMap instead, we could not guarantee this would be the case.

How do I use PriorityBlockingQueue class?

This example demonstrate how to use the PriorityBlockingQueue class. The PriorityBlockingQueue is one implementation of the BlockingQueue interface. It is an unbounded concurrent queue. The object place in this type of queue must implement the java.lang.Comparable interface. The Comparable interface defines how the order priority of the elements inside this queue.

For simplicity, in this example we use strings object as the elements to be placed in the queue. The String class implements the comparable interface. Running this example will print out the names in the string array in alphabetical orders.

package org.kodejava.util.concurrent;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

public class PriorityBlockingQueueExample {
    public static void main(String[] args) {
        final String[] names =
                {"carol", "alice", "malory", "bob", "alex", "jacobs"};

        final BlockingQueue<String> queue = new PriorityBlockingQueue<>();

        new Thread(() -> {
            for (String name : names) {
                try {
                    queue.put(name);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Producer").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < names.length; i++) {
                    System.out.println(queue.take());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Consumer").start();
    }
}

This code print out:

alex
alice
bob
carol
jacobs
malory

How do I use LinkedBlockingQueue class?

In the following code snippet you will learn how to use the LinkedBlockingQueue class. This class implements the BlockingQueue interface. We can create a bounded queue by specifying the queue capacity at the object construction. If we do not define the capacity, the upper bound is limited to the size of Integer.MAX_VALUE.

The data in the queue represented as a linked node in a FIFO (First-In-First-Out) order. The head element of the queue is the longest element placed in the queue and the tail element is the latest element added to the queue.

Let’s see the code in action below:

package org.kodejava.util.concurrent;

import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class LinkedBlockingQueueExample {
    public static void main(String[] args) {
        final BlockingQueue<String> queue = new LinkedBlockingQueue<>(1024);

        // Producer Tread
        new Thread(() -> {
            while (true) {
                try {
                    String data = UUID.randomUUID().toString();
                    System.out.printf("[%s] PUT [%s].%n",
                            Thread.currentThread().getName(), data);
                    queue.put(data);
                    Thread.sleep(250);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Producer").start();

        // Consumer-1 Thread
        new Thread(() -> {
            while (true) {
                try {
                    String data = queue.take();
                    System.out.printf("[%s] GET [%s].%n",
                            Thread.currentThread().getName(), data);
                    Thread.sleep(550);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Consumer-1").start();

        // Consumer-2 Thread
        new Thread(() -> {
            while (true) {
                try {
                    String data = queue.take();
                    System.out.printf("[%s] GET [%s].%n",
                            Thread.currentThread().getName(), data);
                    Thread.sleep(750);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Consumer-2").start();
    }
}

The output of the code snippet above:

[Producer] PUT [34418601-19cf-41fc-aecc-a0fa7caa72bb].
[Consumer-1] GET [34418601-19cf-41fc-aecc-a0fa7caa72bb].
[Producer] PUT [f2050eaa-c575-4faf-89e8-1ad0e3fbce0e].
[Consumer-2] GET [f2050eaa-c575-4faf-89e8-1ad0e3fbce0e].
[Producer] PUT [2f24985b-009b-44c3-9cec-7a066aa08ecf].
[Consumer-1] GET [2f24985b-009b-44c3-9cec-7a066aa08ecf].
[Producer] PUT [b4332823-c1a2-4587-bf5f-1f67407d6945].
[Consumer-2] GET [b4332823-c1a2-4587-bf5f-1f67407d6945].
[Producer] PUT [33edcd78-1b14-4913-afe2-7f44d76db50b].
[Consumer-1] GET [33edcd78-1b14-4913-afe2-7f44d76db50b].

How to implement queue using the DelayQueue?

The java.util.concurrent.DelayQueue class in an implementation of the BlockingQueue interface. Elements added to the queue must implement the java.util.concurrent.Delayed interface.

The queue is unbound in size, enabling adds to return immediately, we can only take an element from the queue when the delay time has expired. If multiple elements have expired delays, the element with the longest delay expiration will be taken first.

package org.kodejava.util.concurrent;

import java.util.Random;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;

public class DelayQueueExample {
    public static void main(String[] args) {
        // Creates an instance of blocking queue using the DelayQueue.
        final BlockingQueue<DelayObject> queue = new DelayQueue<>();
        final Random random = new Random();

        new Thread(() -> {
            while (true) {
                try {
                    // Put some Delayed object into the Queue.
                    int delay = random.nextInt(10000);
                    DelayObject object = new DelayObject(
                            UUID.randomUUID().toString(), delay);

                    System.out.printf("Put object = %s%n", object);
                    queue.put(object);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Producer Thread").start();

        new Thread(() -> {
            while (true) {
                try {
                    // Take elements out from the DelayQueue object.
                    DelayObject object = queue.take();
                    System.out.printf("[%s] - Take object = %s%n",
                            Thread.currentThread().getName(), object);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Consumer Thread-1").start();

        new Thread(() -> {
            while (true) {
                try {
                    // Take elements out from the DelayQueue object.
                    DelayObject object = queue.take();
                    System.out.printf("[%s] - Take object = %s%n",
                            Thread.currentThread().getName(), object);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Consumer Thread-2").start();
    }
}

Below is an implementation of the Delayed interface. In the implementation class we have to implement the getDelay(TimeUnit) and the compareTo(Object) methods.

package org.kodejava.util.concurrent;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayObject implements Delayed {
    private final String data;
    private final long startTime;

    public DelayObject(String data, long delay) {
        this.data = data;
        this.startTime = System.currentTimeMillis() + delay;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        long diff = startTime - System.currentTimeMillis();
        return unit.convert(diff, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        return Long.compare(this.startTime, ((DelayObject) o).startTime);
    }

    @Override
    public String toString() {
        return "{" +
                "data='" + data + '\'' +
                ", startTime=" + startTime +
                '}';
    }
}

Running this example give you some kind of the following output:

Put object = {data='ff217d8f-30c7-4ff6-b107-f5e6b13b5cf1', startTime=1635830327667}
Put object = {data='ad9b7f64-04f6-45ba-8b4a-6d9d25785303', startTime=1635830326742}
Put object = {data='7e439219-486a-473f-b093-2ada26b682f4', startTime=1635830328540}
Put object = {data='1fd6f3d2-60b1-4986-a8d0-ceb0863b30a7', startTime=1635830328498}
[Consumer Thread-1] - Take object = {data='ad9b7f64-04f6-45ba-8b4a-6d9d25785303', startTime=1635830326742}
Put object = {data='9d8811a2-5f2a-487b-a229-11ad8ed54515', startTime=1635830330952}
Put object = {data='baf72a83-0242-42cb-93db-9ef08da5f78d', startTime=1635830336088}
[Consumer Thread-2] - Take object = {data='ff217d8f-30c7-4ff6-b107-f5e6b13b5cf1', startTime=1635830327667}
Put object = {data='9d4d14c5-0777-4675-aa67-a49a0519d9b2', startTime=1635830328952}
Put object = {data='c4979d10-4a11-44a8-b79c-ff31309e968b', startTime=1635830331430}

How do I close or shutdown a BlockingQueue?

In this example you’ll learn how to close or shutdown a BlockingQueue when no more element available in the queue. We will use the common strategy by making the Producer to send a marker object in a Producer – Consumer scenario. This marker object also known as the poison object will be considered as a sign that the queue contain no more object that need to be processed. Which then will allow us to break the operation of the consumer thread.

package org.kodejava.util.concurrent;

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

public class BlockingQueueShutdown {
    public static void main(String[] args) {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(32);

        MyDataProducer producer = new MyDataProducer(queue);
        MyDataConsumer consumer = new MyDataConsumer(queue);

        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

Below is the Producer object that put data into the queue. The string DONE is our marker object. This is the last data will be placed in the queue for the consumer to pick up.

package org.kodejava.util.concurrent;

import java.util.concurrent.BlockingQueue;

public class MyDataProducer implements Runnable {
    BlockingQueue<String> queue;

    public MyDataProducer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        System.out.println("MyDataProducer.run");
        String[] data = {"D001", "D002", "D003", "D004", "D005", "DONE"};

        try {
            for (String element : data) {
                queue.put(element);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The Consumer object loops to retrieve elements from the queue. And it will break the loop and ended the thread when it receives the marker object from the queue.

package org.kodejava.util.concurrent;

import java.util.concurrent.BlockingQueue;

public class MyDataConsumer implements Runnable {
    BlockingQueue<String> queue;

    public MyDataConsumer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        System.out.println("MyDataConsumer.run");

        while (true) {
            try {
                String element = queue.take();
                if ("DONE".equals(element)) {
                    System.out.println("Exiting consumer thread, " +
                            "end of data reached.");
                    break;
                }
                System.out.println("Element = " + element);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}