How do I send requests using different HTTP Methods with HttpClient?

Java 11 introduced the HttpClient API to simplify and modernize HTTP communications. This API supports sending requests using different HTTP methods (GET, POST, PUT, DELETE, etc.). Below is an explanation and example of how to perform these operations.

1. Setup

You will use the HttpClient and related classes from java.net.http package:

  • HttpClient – To execute HTTP requests.
  • HttpRequest – To construct and describe HTTP requests.
  • HttpResponse – To handle HTTP responses.

2. Example Code

Here’s how you can send HTTP requests with different methods using HttpClient in Java 11.

package org.kodejava.net.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;

public class HttpClientMethodExample {
   public static void main(String[] args) {
      try {
         // Create an HttpClient instance
         HttpClient httpClient = HttpClient.newHttpClient();

         // Example: GET Request
         HttpRequest getRequest = HttpRequest.newBuilder()
                 .uri(new URI("https://jsonplaceholder.typicode.com/posts/1"))
                 .GET() // Default is GET, this is optional
                 .build();
         HttpResponse<String> getResponse = httpClient.send(getRequest, BodyHandlers.ofString());
         System.out.println("GET Response: " + getResponse.body());

         // Example: POST Request
         HttpRequest postRequest = HttpRequest.newBuilder()
                 .uri(new URI("https://jsonplaceholder.typicode.com/posts"))
                 .POST(BodyPublishers.ofString("{\"title\":\"foo\",\"body\":\"bar\",\"userId\":1}"))
                 .header("Content-Type", "application/json")
                 .build();
         HttpResponse<String> postResponse = httpClient.send(postRequest, BodyHandlers.ofString());
         System.out.println("POST Response: " + postResponse.body());

         // Example: PUT Request
         HttpRequest putRequest = HttpRequest.newBuilder()
                 .uri(new URI("https://jsonplaceholder.typicode.com/posts/1"))
                 .PUT(BodyPublishers.ofString("{\"id\":1,\"title\":\"updated\",\"body\":\"new content\",\"userId\":1}"))
                 .header("Content-Type", "application/json")
                 .build();
         HttpResponse<String> putResponse = httpClient.send(putRequest, BodyHandlers.ofString());
         System.out.println("PUT Response: " + putResponse.body());

         // Example: DELETE Request
         HttpRequest deleteRequest = HttpRequest.newBuilder()
                 .uri(new URI("https://jsonplaceholder.typicode.com/posts/1"))
                 .DELETE()
                 .build();
         HttpResponse<String> deleteResponse = httpClient.send(deleteRequest, BodyHandlers.ofString());
         System.out.println("DELETE Response Code: " + deleteResponse.statusCode());

      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

3. Explanation

  1. HttpClient Creation: The HttpClient instance is reusable for making multiple requests.
  2. GET Request:
    • Use .GET() method to send a GET request.
    • Response is parsed as a String using BodyHandlers.ofString().
  3. POST Request:
    • Use .POST(BodyPublishers.ofString(content)) to send a POST request with a payload.
    • Set the Content-Type header for JSON or other content types.
  4. PUT Request:
    • Use .PUT(BodyPublishers.ofString(content)) for PUT requests with a payload.
    • Similar to POST, set the proper headers.
  5. DELETE Request:
    • Use .DELETE() to send DELETE requests.
    • Rarely includes a body; that’s why no publisher is used.
  6. Error Handling:
    • Be sure to include error handling for exceptions such as IOException and InterruptedException.

4. Output Example

If you run the code using the example endpoints, the output might look something like this:

GET Response: {
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
POST Response: {
  "title": "foo",
  "body": "bar",
  "userId": 1,
  "id": 101
}
PUT Response: {
  "id": 1,
  "title": "updated",
  "body": "new content",
  "userId": 1
}
DELETE Response Code: 200

5. Notes

  • You’ll need an API endpoint that supports CRUD operations for realistic testing.
  • Avoid hardcoding URIs in production; keep them configurable.
  • Handle response status codes appropriately for error cases (like 404, 500).

This approach provides a clean and modern way to work with HTTP in Java!

How do I write custom synchronizers using AbstractQueuedSynchronizer?

Writing custom synchronizers with AbstractQueuedSynchronizer (AQS) in Java involves creating classes that encapsulate synchronization logic, such as locks, semaphores, or barriers. AQS simplifies the development of concurrency tools by handling queueing and thread state management. Below are the key steps and guidelines to write custom synchronizers using AbstractQueuedSynchronizer:

1. Understand AQS Concepts

  • state: AQS maintains a simple int value (state) to represent the synchronization state. The meaning of state varies depending on the synchronizer you create (e.g., lock held count for a reentrant lock, permits for a semaphore, etc.).
  • Exclusive Mode: Only one thread can hold the resource (e.g., ReentrantLock).
  • Shared Mode: Multiple threads can share the resource (e.g., Semaphore, CountDownLatch).

2. Create a Custom Synchronizer Class

Your custom synchronizer class will generally:

  • Extend AbstractQueuedSynchronizer.
  • Use the state variable to model your synchronization logic.
  • Implement key methods such as tryAcquire, tryRelease, tryAcquireShared, and tryReleaseShared based on whether you’re implementing exclusive or shared behavior.

3. Implement Required Methods

For Exclusive Mode:

Override:

  • tryAcquire(int arg): Define the logic to acquire the resource exclusively. Return true if the acquisition is successful, otherwise return false.
  • tryRelease(int arg): Define the logic to release the resource. Return true if the state transition occurs and allows waiting threads to proceed.

For Shared Mode:

Override:

  • tryAcquireShared(int arg): Define the logic to acquire the resource in shared mode. Return:
    • Negative if the acquisition fails.
    • Zero if no more shared acquisitions are allowed.
    • Positive if the acquisition is successful, and more threads can share the resource.
  • tryReleaseShared(int arg): Define the logic to release the resource in shared mode. Usually, decrement the state and decide if more threads can proceed.

4. Publish the Synchronizer to Clients

AQS is always used as part of a larger custom synchronization object. Expose public methods in your custom class to wrap the AQS functionality. For instance:

  • For exclusive locks: lock and unlock methods.
  • For shared locks: Methods such as acquireShared and releaseShared.

5. Example Implementations

Example 1: Simple Mutex (ReentrantLock Equivalent)

Code for an exclusive synchronizer:

package org.kodejava.util.concurrent;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class SimpleMutex {
    // Custom synchronizer extending AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int ignored) {
            // Attempt to set state to 1 if it's currently 0 (lock is free)
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int ignored) {
            // Only lock owner can release
            if (getState() == 0 || getExclusiveOwnerThread() != Thread.currentThread()) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0); // Free the lock
            return true; // Allow further attempts to acquire the lock
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
        }
    }

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

Example 2: Simple Semaphore (Shared Synchronizer)

Code demonstrating shared mode:

package org.kodejava.util.concurrent;

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class SimpleSemaphore {
    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int permits) {
            setState(permits);
        }

        @Override
        protected int tryAcquireShared(int permits) {
            for (; ; ) {
                int current = getState();
                int remaining = current - permits;
                // Check if we can acquire the permits
                if (remaining < 0 || compareAndSetState(current, remaining)) {
                    return remaining;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int permits) {
            for (; ; ) {
                int current = getState();
                int next = current + permits;
                // Release the permits
                if (compareAndSetState(current, next)) {
                    return true;
                }
            }
        }
    }

    private final Sync sync;

    public SimpleSemaphore(int permits) {
        sync = new Sync(permits);
    }

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public void release() {
        sync.releaseShared(1);
    }
}

6. Testing and Validation

  • Test your custom synchronizer in multithreaded environments to ensure correctness.
  • Use proper test tools like JUnit or TestNG.
  • Validate edge cases, such as reentrancy (if applicable), releasing resources by non-owners, or negative state transitions.

Best Practices

  • Always ensure a clean state transition in synchronization methods.
  • Use atomic operations to modify state (e.g., compareAndSetState).
  • Avoid busy spinning (e.g., Thread.yield() or blocking mechanisms are better).
  • Use AQS’s built-in blocking mechanisms like acquire, acquireShared, release, or releaseShared.

By following these steps and practices, you can create robust custom synchronizers tailored to your concurrency requirements.

How do I implement async pipelines with CompletableFuture chaining?

Asynchronous pipelines can be implemented efficiently in Java using CompletableFuture. The ability of CompletableFuture to chain multiple steps through its functional programming model (e.g., thenApply, thenCompose, thenAccept, etc.) makes it a powerful tool for building non-blocking pipelines.

Here’s a step-by-step guide to implement async pipelines using CompletableFuture chaining:


1. Basics of CompletableFuture

CompletableFuture is a class in java.util.concurrent that represents a future result of an asynchronous computation. You can chain computations, handle exceptions, and run them all asynchronously.

2. Example Async Pipeline Workflow

Let’s assume we have a pipeline with multiple steps:

  1. Fetch data from a remote source.
  2. Transform the data.
  3. Save the data to a database.
  4. Notify a user.

Each of these steps will be represented as a method returning a CompletableFuture.


3. Implementation

a) Define the Asynchronous Tasks

Each step in the pipeline is encapsulated as an asynchronous method using CompletableFuture.supplyAsync() or runAsync().

package org.kodejava.util.concurrent;

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

public class AsyncPipeline {

   private static final ExecutorService executor = Executors.newFixedThreadPool(10); // Thread pool

   // 1. Fetch data from a remote source
   public CompletableFuture<String> fetchData() {
      return CompletableFuture.supplyAsync(() -> {
         try {
            Thread.sleep(1000); // Simulating delay
         } catch (InterruptedException e) {
            throw new IllegalStateException(e);
         }
         System.out.println("Fetched data");
         return "Data from remote source";
      }, executor);
   }

   // 2. Transform the data
   public CompletableFuture<String> transformData(String data) {
      return CompletableFuture.supplyAsync(() -> {
         System.out.println("Transforming data");
         return data.toUpperCase();
      }, executor);
   }

   // 3. Save the data to a database
   public CompletableFuture<Void> saveToDatabase(String transformedData) {
      return CompletableFuture.runAsync(() -> {
         System.out.println("Saving data to database: " + transformedData);
      }, executor);
   }

   // 4. Notify the user
   public CompletableFuture<Void> notifyUser() {
      return CompletableFuture.runAsync(() -> {
         System.out.println("User notified!");
      }, executor);
   }

   // Build the pipeline
   public void executePipeline() {
      fetchData()
              .thenCompose(this::transformData) // Pass the result from "fetchData" to "transformData"
              .thenCompose(this::saveToDatabase) // Then save transformed data to database
              .thenCompose(aVoid -> notifyUser()) // Finally notify the user
              .exceptionally(ex -> { // Handle exceptions globally
                 System.err.println("Pipeline execution failed: " + ex.getMessage());
                 return null;
              })
              .join(); // Block and wait for the pipeline to complete
   }

   public static void main(String[] args) {
      new AsyncPipeline().executePipeline();
   }
}

4. Explanation of the Code

  1. supplyAsync():
    • Used for methods that generate results, such as fetchData() and transformData().
  2. runAsync():
    • Used for methods that don’t produce results (return void), like saveToDatabase() and notifyUser().
  3. Chaining with thenCompose:
    • thenCompose() is used for chaining tasks where each subsequent task depends on the result of the previous task.
  4. Error Handling with exceptionally:
    • exceptionally() is used to handle any error in the pipeline and provide fallback logic.
  5. Thread Pool:
    • You can specify a custom ExecutorService for better control over thread resources.
  6. join():
    • Blocks the main thread until the entire pipeline is complete.

5. Key Methods in CompletableFuture

Method Description
supplyAsync() Executes a task asynchronously and provides a result.
runAsync() Executes a task asynchronously without any result.
thenApply() Transforms the result of a CompletableFuture.
thenCompose() Chains a dependent CompletableFuture, useful when a task depends on the output of the previous.
thenAccept() Consumes the result of a task without returning a result itself.
exceptionally() Handles exceptions that occur during pipeline execution.
allOf() Combines multiple CompletableFutures and waits for all to complete.
anyOf() Completes when any of the provided CompletableFutures completes first.

6. Advanced: Combining Multiple Futures

If there’s a need to combine results from multiple independent asynchronous tasks, you can use allOf() or anyOf().

Example with allOf:

public void fetchMultipleSources() {
    CompletableFuture<String> source1 = fetchSource1();
    CompletableFuture<String> source2 = fetchSource2();

    CompletableFuture<Void> combined = CompletableFuture.allOf(source1, source2)
        .thenRun(() -> {
            try {
                // Retrieve results once all futures are complete
                System.out.println("Source1: " + source1.get());
                System.out.println("Source2: " + source2.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

    combined.join(); // Wait for the combined task to complete
}

private CompletableFuture<String> fetchSource1() {
    return CompletableFuture.supplyAsync(() -> {
        System.out.println("Fetching source 1...");
        return "Data from Source 1";
    });
}

private CompletableFuture<String> fetchSource2() {
    return CompletableFuture.supplyAsync(() -> {
        System.out.println("Fetching source 2...");
        return "Data from Source 2";
    });
}

This approach allows building flexible and robust asynchronous pipelines while keeping the code clear and concise.

How do I use CompletableFuture for reactive-style concurrency?

Using CompletableFuture in Java can be a powerful way to implement reactive-style concurrency. It provides a clean and functional way to perform asynchronous tasks, compose their results, and handle exceptions without blocking threads. Here’s an explanation with examples to guide you through its reactive-style usage:


Key Features of CompletableFuture

  1. Asynchronous execution – Run tasks on background threads.
  2. Chaining tasks – Perform dependent actions when a task completes using thenApply, thenAccept, thenCompose, etc.
  3. Combining tasks – Execute multiple tasks in parallel and combine their results using thenCombine, allOf, or anyOf.
  4. Exception handling – Handle errors gracefully with handle, exceptionally, or whenComplete.

Example Use Cases

1. Basic Asynchronous Execution

You can run a task asynchronously without blocking the main thread:

package org.kodejava.util.concurrent;

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            // Simulate a long computation
            System.out.println("Running in background...");
            return "Result";
        }).thenAccept(result -> {
            // Use the result once completed
            System.out.println("Completed with: " + result);
        });

        System.out.println("Main thread is free to do other work...");
    }
}

Output:

Main thread is free to do other work...
Running in background...
Completed with: Result

2. Chaining Dependent Tasks

Reactive-style programming involves chaining tasks, which can be done with thenApply or thenCompose:

package org.kodejava.util.concurrent;

import java.util.concurrent.CompletableFuture;

public class CompletableFutureChaining {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            // Fetch some data (simulate API/database call)
            return "Data from API";
        }).thenApply(data -> {
            // Transform the data
            return data.toUpperCase();
        }).thenAccept(processedData -> {
            // Use transformed data
            System.out.println("Processed Data: " + processedData);
        });
    }
}

3. Combining Multiple Async Tasks

To run multiple tasks in parallel and combine their results:

package org.kodejava.util.concurrent;

import java.util.concurrent.CompletableFuture;

public class CompletableFutureCombine {
    public static void main(String[] args) {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Task 1 Result");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Task 2 Result");

        CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + " & " + result2;
        });

        combinedFuture.thenAccept(System.out::println);
    }
}

Output:

Task 1 Result & Task 2 Result

4. Waiting for All Tasks to Complete

If you need to wait for multiple independent tasks to complete:

package org.kodejava.util.concurrent;

import java.util.concurrent.CompletableFuture;
import java.util.List;

public class CompletableFutureAllOf {
    public static void main(String[] args) {
        CompletableFuture<Void> allTasks = CompletableFuture.allOf(
                CompletableFuture.runAsync(() -> System.out.println("Task 1")),
                CompletableFuture.runAsync(() -> System.out.println("Task 2")),
                CompletableFuture.runAsync(() -> System.out.println("Task 3"))
        );

        // Wait for all tasks to complete
        allTasks.join();
        System.out.println("All tasks completed.");
    }
}

5. Handling Exceptions

You can handle exceptions gracefully with methods like exceptionally, handle, or whenComplete:

package org.kodejava.util.concurrent;

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExceptionHandling {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
                    // Simulate an error
                    if (true) throw new RuntimeException("Something went wrong!");
                    return "Task Result";
                })
                .exceptionally(ex -> {
                    System.out.println("Error: " + ex.getMessage());
                    return "Fallback Result";
                })
                .thenAccept(result -> System.out.println("Result: " + result));
    }
}

Output:

Error: Something went wrong!
Result: Fallback Result

6. Running Tasks in a Custom Executor

By default, CompletableFuture uses the common ForkJoinPool, but you can specify a custom executor:

package org.kodejava.util.concurrent;

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

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

        CompletableFuture.runAsync(() -> {
            System.out.println("Task executing in custom executor");
        }, executor).thenRun(() -> executor.shutdown());
    }
}

Summary of Key Methods

Method Purpose
supplyAsync(Supplier) Run a computation in another thread and return a result.
runAsync(Runnable) Run a computation without returning a result.
thenApply(Function) Transform result of the stage.
thenCompose(Function) Chain another async computation dependent on the previous one.
thenAccept(Consumer) Consume the result.
thenCombine(CompletableFuture, BiFunction) Combine results of two independent computations.
allOf(CompletableFuture...) Wait for all tasks to complete.
anyOf(CompletableFuture...) Return as soon as any task is complete.
exceptionally(Function) Handle exceptions and provide a fallback value.
handle(BiFunction) Process the result or handle exceptions.

Benefits of Using CompletableFuture for Reactive Programming

  • Non-blocking and efficient concurrency.
  • Easier composition of asynchronous operations compared to traditional threads.
  • Fine-grained exception handling and coordination of parallel tasks.
  • Works well with APIs like REST or streaming in a reactive pipeline.

By taking advantage of these features, you can implement clean, reactive, and efficient systems.

How do I manage thread-local state in highly concurrent environments?

Managing thread-local state in a highly concurrent environment requires an understanding of ThreadLocal in Java and certain best practices to ensure correctness and good performance. Here’s how you can effectively manage thread-local state:


1. Use ThreadLocal for Thread-Specific State

The ThreadLocal class provides thread-local variables. Each thread accessing such a variable gets its own, independent copy of the variable.

Example:

package org.kodejava.util.concurrent;

public class ThreadLocalExample {
    private static final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Default Value");

    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
            threadLocal.set(Thread.currentThread().getName() + "'s Value");
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        };

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

        thread1.start();
        thread2.start();
    }
}
  • Each thread gets its own copy of the threadLocal state.
  • The withInitial factory method ensures a default value is provided.

2. Clean Up Thread-Local State

Thread-local variables are bound to the lifecycle of the thread. In environments with thread pools (e.g., in Jakarta EE or Spring), threads are reused, so failing to clean up thread-local state may lead to memory leaks or stale data being visible to new tasks.

  • Manually invoke threadLocal.remove() after using it:
try {
    threadLocal.set("Some value");
    // Perform operations with thread-local value
} finally {
    threadLocal.remove();
}
  • Always clean up ThreadLocal in a finally block to ensure it’s removed if an exception occurs.

3. Avoid Overuse of ThreadLocal

While ThreadLocal is useful, overusing it can make code harder to understand, maintain, or debug. Use thread-local variables only when:

  1. There’s truly a need for per-thread state.
  2. Passing state explicitly through method arguments is not feasible.

4. Use Context Propagation Utilities

When working with frameworks like Jakarta EE or Spring, it’s common to propagate context across threads. This is particularly challenging in ExecutorService or reactive programming where threads move between states.

  • Spring: Use RequestContextHolder or libraries like Spring Security which leverage ThreadLocal to store security contexts.
  • ExecutorService Context Propagation: Use libraries like Apache Geronimo’s java-concurrent utilities or ThreadContext from MicroProfile Context Propagation to manage state transfer between threads.

5. Best Practices in Highly Concurrent Environments

  • Use Immutable Objects: Avoid mutable data in thread-local variables to prevent unintended side effects.
  • Limit Scope of ThreadLocal: Declare thread-local variables as private static final and restrict usage to specific classes or methods.
  • Profile and Test: Profiling tools like VisualVM can help ensure thread-local state isn’t causing unexpected memory leaks or bottlenecks.

6. Alternatives to ThreadLocal in Reactive Paradigms

In reactive, non-blocking environments:

  1. Avoid thread-local state as threads are not bound to a single request.
  2. Use explicit state passing chained with reactive operators (from frameworks like Reactor or RxJava).

Example of explicit state passing in a reactive flow:

Mono.just("Reactive State")
    .flatMap(state -> {
        // State is explicitly passed to the next step
        return Mono.just(state + " Modified");
    })
    .subscribe(System.out::println);

7. Debugging ThreadLocal Issues

If you run into issues such as memory leaks:

  • Use tools like Eclipse Memory Analyzer (MAT) to analyze thread-local references.
  • Validate that every ThreadLocal is removed (remove()) when it’s no longer needed.

By adhering to these guidelines, you can effectively and safely manage thread-local states in highly concurrent environments.