How to leverage Java 10’s Thread-Local Handshakes for performance tuning

Java 10 introduced Thread-Local Handshakes as a powerful feature in the JVM, designed to improve the performance and responsiveness of applications, especially in scenarios involving thread-based operations and garbage collection.

Here’s what you need to know about Thread-Local Handshakes and how to leverage them for performance tuning:


1. What are Thread-Local Handshakes?

Thread-Local Handshakes allow a thread to execute a callback function locally without stopping all threads. This contrasts with traditional global safepoints in Java, where all threads must come to a safe state before any work can be done, such as garbage collection or code deoptimization.

In other words:

  • A handshake is a mechanism to perform operations on a subset of threads (or even individual threads) without stopping the entire JVM.
  • This is useful for operations that don’t require a global JVM safepoint, improving responsiveness and reducing latency.

2. Benefits of Thread-Local Handshakes

  • Avoids Global Safepoints: Operations can target some threads or a single thread, meaning other threads continue their work unaffected.
  • Reduces Latency: No need to pause all threads, improving performance for multithreaded applications.
  • Fine-Grained Control: Perform thread-specific tasks like flushing thread-specific memory buffers, deoptimizing code for just one thread, or collecting specific thread-local objects without interrupting the entire JVM.

3. Use Cases

Here are some scenarios where Thread-Local Handshakes can be beneficial:

  • Garbage Collection
    Garbage collectors rely on safepoints to pause threads while managing memory. Thread-Local Handshakes can isolate such operations to only the threads that need it, reducing pause times and improving application throughput.

  • Code Deoptimization
    This happens during just-in-time (JIT) compilation when compiled code needs to revert to interpreted mode. Utilizing handshakes allows deoptimization to occur on specific threads, minimizing the impact on other threads.

  • Thread-Specific Profiling and Debugging
    A developer or monitoring agent can perform profiling or diagnostic tasks on a single thread without disturbing other threads.

  • Thread-Specific Resource Cleanup
    Thread-local data structures can be cleaned up or flushed for specific threads, optimally managing system resources.


4. How Thread-Local Handshakes Work Internally

Thread-Local Handshakes introduce thread-specific “safepoints.” When a request is initiated:

  1. The JVM signals specific threads to execute a callback function (like releasing resources or processing pending tasks).
  2. Unlike global safepoints, only the targeted thread(s) pause and execute the operation.
  3. Once the operation is complete, the thread resumes execution.

This makes operations more granular and non-blocking at the JVM level.


5. Leveraging Thread-Local Handshakes in Performance Tuning

Although Thread-Local Handshakes are implemented at the JVM level, you can indirectly leverage them for performance tuning in the following ways:

  1. Tuning for Garbage Collection
    If you’re using a garbage collector like G1GC or ZGC, you can reduce garbage collection pauses since these collectors take advantage of handshakes to avoid halting all threads during certain operations.

    • How to Monitor: Use tools like Java Mission Control (JMC), VisualVM, or JVM logging to monitor GC pause times and ensure thread-local synchronization is being effectively utilized.

    Relevant JVM Options:

    • -XX:+UseG1GC (or any GC of choice) to enable advanced garbage collection strategies.
    • Use -Xlog:gc to monitor GC logs and observe pauses.
  2. Reducing Latency in Thread-Sensitive Applications
    If your application uses many threads (e.g., for handling requests or background tasks), Thread-Local Handshakes reduce overall latency by targeting specific threads instead of pausing all threads unnecessarily.

    Best practices:

    • Profile your application for thread contention and safepoints using tools like Async Profiler or JFR (Java Flight Recorder).
    • Optimize thread management through thread pools (using ForkJoinPool, ThreadPoolExecutor, etc.) to prevent thread starvation and maximize throughput.
  3. Tuning Thread-Specific Tasks
    For tasks that manipulate thread-local data or thread-specific settings:

    • Optimize performance by ensuring the work is allocated to specific threads that need operations (e.g., specific callbacks).
    • Reduce contention by designing operations that leverage locality (thread-local memory, caches, etc.).

6. Practical Tips for Developers

While Thread-Local Handshakes are managed by the JVM, the following tips help you align your code and architecture to take full advantage:

  1. Choose Modern JVMs: Use JDK 10 or later for applications where fine-grained thread optimization matters. Newer garbage collectors like ZGC or Shenandoah optimize handshakes even further.

  2. Monitor Safepoints and Utilization:

    • Safepoint statistics can be enabled using -XX:+PrintSafepointStatistics to understand how your threads interact with JVM-managed resources.
    • Use tools like JFR to detect safepoint delays or thread-local handshake activity.
  3. Minimize Global Syncs in Application Code:
    • Avoid global thread synchronization where possible.
    • Use thread-local structures (e.g., ThreadLocal API) for thread-scoped data.
  4. Benchmark Your Application:
    Profile how your code interacts with the JVM and threads. Use tools like JMH (Java Microbenchmark Harness) for thread and synchronization benchmarking.


7. Example: Monitoring Thread Safepoints

java -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 -XX:+LogVMOutput -XX:LogFile=safepoints.log -jar YourApp.jar

This will output safepoint-related logs, showing where Thread-Local Handshakes may improve performance by reducing pauses.


8. Conclusion

Thread-Local Handshakes represent an evolutionary step in how the JVM manages thread interactions, replacing costly global operations with thread-targeted approaches. While you may not directly invoke or control handshakes, you can optimize your application and JVM configuration to reap their benefits:

  • Select JVM options and garbage collection strategies that leverage handshakes.
  • Profile and diagnose thread safepoints to find opportunities for performance tuning.

These adjustments ensure better efficiency, reduced latency, and improved performance in multithreaded applications.

How do I secure API requests with OAuth2 using Java 11 HttpClient?

Securing API requests with OAuth2 using Java 11’s HttpClient involves obtaining an access token from the OAuth2 provider and including it in the header of your HTTP requests. Here’s a step-by-step guide:

1. Understand OAuth2 Flow

OAuth2 involves several flows (e.g., Authorization Code, Client Credentials, etc.). For simplicity, we’ll focus on the Client Credentials Grant flow where your application (client) authenticates with the OAuth2 provider and retrieves an access token for API calls.

2. Dependencies

You don’t need external dependencies unless you choose to use a library (like Spring Security). Java 11’s HttpClient can directly process token requests.

3. Steps to Secure API Requests

Obtain Access Token

To obtain an access token, make a POST request to the OAuth2 token endpoint with the required parameters:

  • client_id: Your application’s client ID.
  • client_secret: Your application’s secret key.
  • grant_type: For example, client_credentials.

Use the Access Token in API Requests

Once you have the access token, include it in the Authorization header of your requests.

4. Sample Code

Here’s an example of how to secure API requests with OAuth2 using Java 11’s HttpClient:

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.URLEncoder;
import java.nio.charset.StandardCharsets;

public class OAuth2HttpClient {

   public static void main(String[] args) throws Exception {
      // OAuth2 Token Endpoint and Client Details
      String tokenEndpoint = "https://your-oauth2-provider.com/token";
      String clientId = "your-client-id";
      String clientSecret = "your-client-secret";
      String scope = "your-api-scope";

      // Step 1: Obtain Access Token
      String accessToken = getAccessToken(tokenEndpoint, clientId, clientSecret, scope);

      // Step 2: Use Access Token for API Request
      String apiEndpoint = "https://api.example.com/secure-resource";
      sendSecureApiRequest(apiEndpoint, accessToken);
   }

   private static String getAccessToken(String tokenEndpoint, String clientId, String clientSecret, String scope) throws Exception {
      // Create request body
      String requestBody = "grant_type=client_credentials" +
                           "&client_id=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8) +
                           "&client_secret=" + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8) +
                           "&scope=" + URLEncoder.encode(scope, StandardCharsets.UTF_8);

      // Create HttpClient and HttpRequest
      HttpClient client = HttpClient.newHttpClient();
      HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create(tokenEndpoint))
              .header("Content-Type", "application/x-www-form-urlencoded")
              .POST(HttpRequest.BodyPublishers.ofString(requestBody))
              .build();

      // Send request and parse response
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      if (response.statusCode() == 200) {
         // Extract access token from JSON response
         String responseBody = response.body();
         return extractAccessToken(responseBody);
      } else {
         throw new RuntimeException("Failed to get access token. Status: " + response.statusCode() + ", Body: " + response.body());
      }
   }

   private static String extractAccessToken(String responseBody) {
      // Parse JSON response to extract the "access_token" (you can use a library like Jackson or Gson)
      // For simplicity, assume the response contains: {"access_token":"your-token"}
      int startIndex = responseBody.indexOf("\"access_token\":\"") + 16;
      int endIndex = responseBody.indexOf("\"", startIndex);
      return responseBody.substring(startIndex, endIndex);
   }

   private static void sendSecureApiRequest(String apiEndpoint, String accessToken) throws Exception {
      // Create HttpClient and HttpRequest
      HttpClient client = HttpClient.newHttpClient();
      HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create(apiEndpoint))
              .header("Authorization", "Bearer " + accessToken)
              .GET()
              .build();

      // Send request and print response
      HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
      System.out.println("API Response Status: " + response.statusCode());
      System.out.println("API Response Body: " + response.body());
   }
}

5. Explanation

  1. Obtaining Access Token:
    • A POST request is sent to the token endpoint with appropriate parameters in the body.
    • The response typically returns an access token in JSON format.
  2. Secure API Request:
    • Include the token in the Authorization header as Bearer <token> in subsequent requests to the secured API.
  3. Error Handling:
    • If the token request or API request fails, handle the error gracefully (e.g., retry or log).

6. Security Tip

  • Never hardcode client_id or client_secret in your code. Store them securely (e.g., environment variables or a secrets manager).
  • If you handle sensitive data, ensure your OAuth2 provider supports HTTPS.

How do I log request and response details using Java 11 HttpClient?

If you are using Java 11’s HttpClient and want to log request and response details, you can achieve this by implementing a custom utility. Java 11’s HttpClient provides flexibility to log and inspect both requests and responses using its APIs. Here’s how you can log them:

Full Implementation for Logging Request and Response

Below is an example that demonstrates logging details such as HTTP headers, request body, response status, response headers, and response body.

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.time.Duration;

public class HttpClientLogger {

   public static void main(String[] args) {
      try {
         // Create a sample HTTP GET request
         HttpClient httpClient = HttpClient.newHttpClient();

         HttpRequest request = HttpRequest.newBuilder()
                 .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
                 .GET()
                 .header("Accept", "application/json")
                 .timeout(Duration.ofSeconds(10))
                 .build();

         // Log Request Details
         logRequestDetails(request);

         // Send the request and receive a response
         HttpResponse<String> response = httpClient.send(request,
                 HttpResponse.BodyHandlers.ofString());

         // Log Response Details
         logResponseDetails(response);

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

   private static void logRequestDetails(HttpRequest request) {
      System.out.println("---- HTTP Request Details ----");
      System.out.println("Method: " + request.method());
      System.out.println("URI: " + request.uri());
      System.out.println("Headers: " + request.headers().map());
      request.bodyPublisher().ifPresentOrElse(
              bodyPublisher -> {
                 System.out.println("Request Body: (BodyPublisher details not directly accessible, consider passing it explicitly)");
              },
              () -> System.out.println("Request Body: No body")
      );
      System.out.println("-------------------------------");
   }

   private static void logResponseDetails(HttpResponse<String> response) {
      System.out.println("---- HTTP Response Details ----");
      System.out.println("Status Code: " + response.statusCode());
      System.out.println("Headers: " + response.headers().map());
      System.out.println("Body: " + response.body());
      System.out.println("--------------------------------");
   }
}

Explanation of the Code

  1. Setting up HttpClient:
    • We create a HttpClient instance using HttpClient.newHttpClient().
  2. Building the HttpRequest:
    • Use the HttpRequest.Builder to construct the request. This includes setting the URI, method, headers, and a timeout.
  3. Logging HTTP Request Details:
    • Log details of the request by accessing:
      • HTTP Method: HttpRequest::method
      • URI: HttpRequest::uri
      • Headers: HttpRequest::headers
      • The request body can be logged if you manually supply it during the request creation since BodyPublisher doesn’t provide a content preview.
  4. Sending the Request:
    • Use HttpClient::send to perform the HTTP operation, and specify HttpResponse.BodyHandlers.ofString() to read the response as a string.
  5. Logging HTTP Response Details:
    • Log response information:
      • Status Code: HttpResponse::statusCode
      • Headers: HttpResponse::headers
      • Body: HttpResponse::body

Notes

  1. POST/PUT Requests with a Body:
    • If you are sending POST or PUT requests that include a body (e.g., JSON or a form), you should explicitly log the body content when building the request. Example:
      HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
            .POST(HttpRequest.BodyPublishers.ofString("{\"key\":\"value\"}"))
            .header("Content-Type", "application/json")
            .build();
      

      To log the request body, simply store it in a separate variable and print it in logRequestDetails.

  2. Production Logging:

    • Avoid directly printing details to the console in production.
    • Use proper logging libraries like SLF4J with an implementation (e.g., Logback or Log4j) to write logs at different levels like DEBUG, INFO, ERROR, etc.
  3. Sensitive Data:
    • Avoid logging sensitive details like authentication headers or personal data (e.g., Authorization tokens, passwords).

This approach provides a reusable template for logging HTTP requests and responses while using the Java 11+ HttpClient.

How do I Parse JSON Responses from Java 11 HttpClient Easily?

To parse JSON responses easily using Java 11’s HttpClient, you can use libraries like Jackson or Gson that help in converting a JSON string to Java objects (POJOs) without hassle.

Here’s a step-by-step guide:

Step 1: Create a Java 11 HttpClient request

  1. Use Java’s HttpClient to make an HTTP request and get the JSON response as a string.
  2. Use HttpRequest or HttpResponse as part of the Java 11 HttpClient API.

Step 2: Add Jackson or Gson to Parse JSON

  1. Jackson: Add the dependency to your pom.xml (for Maven):
    <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-databind</artifactId>
       <version>2.15.2</version> 
    </dependency>
    
  2. Gson: Add this dependency:
    <dependency>
       <groupId>com.google.code.gson</groupId>
       <artifactId>gson</artifactId>
       <version>2.10.1</version> 
    </dependency>
    

Step 3: Example Using Jackson

package org.kodejava.net.http;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

public class HttpClientJacksonExample {

    public static void main(String[] args) throws Exception {
        // 1. Create an HttpClient
        HttpClient client = HttpClient.newHttpClient();

        // 2. Create an HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://jsonplaceholder.typicode.com/posts/1")) // Example URL
                .GET()
                .build();

        // 3. Send the HttpRequest and get an HttpResponse
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // 4. Parse JSON String Response to a Java Object using Jackson
        ObjectMapper mapper = new ObjectMapper();
        Post post = mapper.readValue(response.body(), Post.class);

        // 5. Use the parsed object
        System.out.println("Post Title: " + post.getTitle());
    }

    // Sample POJO to match the JSON structure
    public static class Post {
        private int userId;
        private int id;
        private String title;
        private String body;

        // Getters and Setters
        public int getUserId() {
            return userId;
        }

        public void setUserId(int userId) {
            this.userId = userId;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getBody() {
            return body;
        }

        public void setBody(String body) {
            this.body = body;
        }
    }
}

Step 4: Example Using Gson

package org.kodejava.net.http;

import com.google.gson.Gson;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

public class HttpClientGsonExample {

    public static void main(String[] args) throws Exception {
        // 1. Create HttpClient
        HttpClient client = HttpClient.newHttpClient();

        // 2. Create HttpRequest
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://jsonplaceholder.typicode.com/posts/1")) // Example URL
                .GET()
                .build();

        // 3. Send HttpRequest and get HttpResponse
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        // 4. Parse JSON String response to Java Object using Gson
        Gson gson = new Gson();
        Post post = gson.fromJson(response.body(), Post.class);

        // 5. Use the parsed object
        System.out.println("Post Title: " + post.getTitle());
    }

    // Sample POJO class to match the JSON structure
    public static class Post {
        private int userId;
        private int id;
        private String title;
        private String body;

        // Getters and Setters
        public int getUserId() {
            return userId;
        }

        public void setUserId(int userId) {
            this.userId = userId;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getBody() {
            return body;
        }

        public void setBody(String body) {
            this.body = body;
        }
    }
}

Key Points:

  • Serialization and Deserialization: POJO structure must match your JSON’s keys.
  • Why Jackson or Gson?
    They are robust and simplify working with JSON (and even converting nested structures).

How do I Implement Connection Pooling in Java 11 HttpClient?

Java 11 introduced the HttpClient API as part of java.net.http. By default, the HttpClient implementation provides connection pooling, so you don’t usually need to manually enable or build it. However, you do need to configure it effectively to manage connection pooling and ensure it aligns with your performance and scalability requirements.

Here’s how you can implement and configure connection pooling in Java 11’s HttpClient:


1. Default Connection Pooling in Java 11 HttpClient

The HttpClient is built with connection pooling enabled by default. When you create an instance of HttpClient, it manages multiple connections automatically across requests to the same host. However, connection pooling behavior is controlled via the HttpClient.Builder.


2. Configure Connection Pooling Options

You can configure the pooling behavior through settings like timeouts and thread handling. Use the builder pattern to fine-tune:

Example:

package org.kodejava.net.http;

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;

public class HttpClientConnectionPoolingExample {

    public static void main(String[] args) throws Exception {
        // Create a custom HttpClient with connection pooling
        HttpClient httpClient = HttpClient.newBuilder()
                // Set connection timeout
                .connectTimeout(Duration.ofSeconds(10))
                // Use a custom Executor (optional - controls the threads for async requests)
                .executor(java.util.concurrent.Executors.newFixedThreadPool(10))
                // Enable HTTP/2 (default, but can be set explicitly)
                .version(HttpClient.Version.HTTP_2)
                .build();

        // Create an HTTP request
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://jsonplaceholder.typicode.com/posts"))
                .GET() // Optional, as GET is the default
                .build();

        // Perform the request
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        // Print the response
        System.out.println("Response Code: " + response.statusCode());
        System.out.println("Response Body: " + response.body());
    }
}

3. Key Concepts for Connection Pooling

  • Reusing HttpClient Instances
    Always reuse a single HttpClient instance across your application, especially for frequent HTTP calls. Each HttpClient has its own connection pool, so creating new instances unnecessarily can result in poor resource management and lack of reuse.
  • Setting a Custom Executor
    By default, the HttpClient uses a default Executor for asynchronous requests. You can configure a custom thread pool (using java.util.concurrent.Executors) for better control over the number of threads used by your application.
  • Testing and Handling Timeouts
    Always set reasonable timeouts for connections and requests to avoid blocking indefinitely when the server is slow or unreachable.
  • Setting Keep-Alive
    The connection pooling mechanism uses HTTP/1.1 or HTTP/2’s persistent connections to keep sockets alive. You don’t need to manually configure Keep-Alive as it’s handled internally by the HttpClient.

Example of Keep-Alive Headers:

If necessary, you can include a custom header in requests to explicitly control Keep-Alive durations:

HttpRequest request = HttpRequest.newBuilder()
        .uri(new URI("https://example.com"))
        .header("Connection", "keep-alive")
        .build();

4. Advanced: Tuning JVM System Properties

The HttpClient supports additional tuning using JVM system properties. For example, you can configure maximum connections per route, or total connections, if necessary.

Examples of commonly used JVM properties:

-Dsun.net.httpclient.defaultConnectTimeout=10000
-Dsun.net.httpclient.defaultReadTimeout=10000
-Djdk.httpclient.allowRestrictedHeaders=connection,keep-alive

5. Asynchronous Requests with Connection Pooling

If you perform a large number of requests and need high concurrency, leveraging asynchronous handling (sendAsync) is a good practice:

Example:

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.time.Duration;
import java.util.concurrent.CompletableFuture;

public class AsyncHttpClientDemo {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .executor(java.util.concurrent.Executors.newFixedThreadPool(10))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
                .GET()
                .build();

        // Send Async request
        CompletableFuture<HttpResponse<String>> responseFuture =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

        // Process the response asynchronously
        responseFuture.thenAccept(response -> {
            System.out.println("Status Code: " + response.statusCode());
            System.out.println("Response Body: " + response.body());
        }).join(); // Optional: Blocks until complete
    }
}

Summary

  • Java 11’s HttpClient comes with built-in support for connection pooling.
  • Always reuse HttpClient instances to automatically take advantage of the pool.
  • Tune parameters such as connectTimeout, executor, and HTTP versions.
  • Use asynchronous APIs (sendAsync) with custom executors for better concurrency.
  • Avoid creating too many HttpClient instances to prevent fragmentation.

This approach ensures that connection pooling is effectively implemented without the need for third-party libraries.