How to Use Container-Aware JVM Features in Java 10 for Docker

Java 10 introduced new container-aware JVM features that greatly improve how Java applications run in Docker environments. These features provide enhanced automatic detection and utilization of container-based limits for memory and CPU resources, allowing Java applications to respect the constraints of containers better.

Here’s a step-by-step guide to using the container-aware JVM features in Java 10 for Docker:


1. Understand the Features

Before Java 10, the JVM didn’t recognize container resource limits (like those set by Docker). With Java 10, the JVM can now:

  • Detect container memory limits (e.g., --memory or -m in Docker).
  • Detect container CPU limits (e.g., --cpus in Docker).
  • Adjust garbage collection (GC) behavior based on allocated container resources.

2. Key JVM Options

Java 10 enables container awareness by default, but you can check and fine-tune these settings using certain JVM options:

  • -XX:MaxRAMPercentage
    Allows you to define the maximum available heap memory as a percentage of the container’s total memory limit (default: 25%).

  • -XX:InitialRAMPercentage
    Sets the initial heap size as a percentage of the container’s memory limit.

  • -XX:MinRAMPercentage
    Specifies the minimum heap size as a percentage of the container’s memory.

  • -XX:ActiveProcessorCount
    Lets you manually define the number of CPUs the JVM should consider if it doesn’t automatically detect container limits or you want to override them.


3. Check Container-Aware JVM Behavior

You can check if the JVM recognizes the container limits by running a simple Java program inside a Docker container. Below is an example:

Java Code:

public class ContainerAwarenessTest {
    public static void main(String[] args) {
        System.out.println("Available processors: " + Runtime.getRuntime().availableProcessors());
        System.out.println("Max memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB");
    }
}

4. Test in Docker

  1. Write a Dockerfile
    Create a Dockerfile using a Java 10 JDK image for testing:

    FROM openjdk:10-jdk
    COPY ContainerAwarenessTest.java /usr/src/myapp/
    WORKDIR /usr/src/myapp
    RUN javac ContainerAwarenessTest.java
    CMD ["java", "ContainerAwarenessTest"]
    
  2. Build and Run the Docker Container
    • Build the Docker image:
    docker build -t java-container-awareness .
    
    • Run the container with memory and CPU limits:
    docker run --memory="512m" --cpus="1" java-container-awareness
    
  3. Expected Output
    • The Runtime.getRuntime().maxMemory() will show 512 MB or close to it.
    • The Runtime.getRuntime().availableProcessors() will report 1 processor.

5. Fine-Tune with JVM Options

To customize the JVM’s behavior further using Java 10’s new options, add the JVM options with the java command. For example:

docker run --memory="1g" --cpus="2" java-container-awareness java \
 -XX:MaxRAMPercentage=50.0 \
 -XX:InitialRAMPercentage=25.0 \
 -XX:ActiveProcessorCount=1 \
 ContainerAwarenessTest

This manually adjusts:

  • The maximum heap to 50% of the container memory limit (1 GB).
  • The initial heap to 25% of the container memory limit.
  • The active processor count to override to only 1.

6. Verify

For detailed resource information, you can also enable verbose GC logging to monitor heap and memory usage in real-time:

docker run --memory="512m" --cpus="1" java-container-awareness java \
 -Xlog:gc \
 ContainerAwarenessTest

7. Move Beyond Java 10 [Optional]

If you’re using newer Java versions (like Java 11 or later), these container-aware features are still present, and additional enhancements have been made to how Java applications behave in containers. Make sure your base image and application are updated as needed.


By using these container-aware JVM features, your Java applications will better respect container resource constraints, leading to improved efficiency and performance in Dockerized environments.

How do I synchronize phases of execution with CyclicBarrier?

The CyclicBarrier class in Java allows you to synchronize phases or threads at a common point. It is particularly useful when you have multiple threads working on subtasks that need to wait for each other to proceed to the next phase of their work.

Key Features of CyclicBarrier

  • Reusable: The barrier can be reused once all threads have reached the barrier.
  • Action on Barrier Completion: You can specify a barrier action (a task to run only once by one of the threads) that gets executed when all threads reach the barrier.

How CyclicBarrier Works

  • A CyclicBarrier is initialized with a specific number of parties (threads) that must reach the barrier before they are permitted to proceed.
  • When a thread reaches the barrier, it calls the await() method.
  • The thread is blocked until all the required threads reach the barrier (i.e., call await()).
  • Once all threads reach the barrier:
    • Optionally, the barrier action (if defined) is executed by one thread.
    • All threads are released to continue execution.

Example: Synchronizing Multiple Threads with CyclicBarrier

Here’s an example of synchronizing threads using CyclicBarrier. In this case, multiple worker threads perform some task in phases, and all must wait for one another at the end of each phase before proceeding.

package org.kodejava.util.concurrent;

import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {

   public static void main(String[] args) {
      // Number of threads (parties) to synchronize
      int numThreads = 3;

      // Create a CyclicBarrier with a barrier action
      CyclicBarrier barrier = new CyclicBarrier(numThreads, () -> {
         System.out.println("All threads have reached the barrier. Proceeding to the next phase...");
      });

      // Create and start worker threads
      for (int i = 0; i < numThreads; i++) {
         new Thread(new Worker(barrier), "Thread " + (i + 1)).start();
      }
   }

   static class Worker implements Runnable {
      private final CyclicBarrier barrier;

      public Worker(CyclicBarrier barrier) {
         this.barrier = barrier;
      }

      @Override
      public void run() {
         try {
            System.out.println(Thread.currentThread().getName() + " is performing the first phase of task...");
            Thread.sleep((long) (Math.random() * 3000)); // Simulate work
            System.out.println(Thread.currentThread().getName() + " has finished the first phase. Waiting at the barrier...");
            barrier.await(); // Wait for other threads to reach the barrier

            System.out.println(Thread.currentThread().getName() + " is performing the second phase of task...");
            Thread.sleep((long) (Math.random() * 3000)); // Simulate work
            System.out.println(Thread.currentThread().getName() + " has finished the second phase. Waiting at the barrier...");
            barrier.await(); // Wait for other threads at the next barrier

            System.out.println(Thread.currentThread().getName() + " has completed all phases.");
         } catch (Exception e) {
            e.printStackTrace();
         }
      }
   }
}

Explanation of Code:

  1. Barrier Creation:
    • new CyclicBarrier(numThreads, action):
      • numThreads: Number of threads involved in synchronization.
      • action: A Runnable task that executes after all threads reach the barrier.
  2. Phase Execution:
    • Each thread performs its task and then calls barrier.await() to wait for others.
    • When all threads have called await(), the barrier opens, the optional action (if defined) executes, and threads proceed.
  3. Random Delays:
    • Simulated with Thread.sleep((long) (Math.random() * 3000)) to illustrate different thread run times.
  4. Multiple Phases:
    • The example includes two phases of execution, and the barrier synchronizes threads at the end of each phase.

Output (Example Output):

Thread 2 is performing the first phase of task...
Thread 1 is performing the first phase of task...
Thread 3 is performing the first phase of task...
Thread 2 has finished the first phase. Waiting at the barrier...
Thread 1 has finished the first phase. Waiting at the barrier...
Thread 3 has finished the first phase. Waiting at the barrier...
All threads have reached the barrier. Proceeding to the next phase...
Thread 2 is performing the second phase of task...
Thread 3 is performing the second phase of task...
Thread 1 is performing the second phase of task...
Thread 2 has finished the second phase. Waiting at the barrier...
Thread 3 has finished the second phase. Waiting at the barrier...
Thread 1 has finished the second phase. Waiting at the barrier...
All threads have reached the barrier. Proceeding to the next phase...
Thread 2 has completed all phases.
Thread 1 has completed all phases.
Thread 3 has completed all phases.

Keynotes:

  1. Thread Releasing:
    • All threads are released simultaneously when all of them reach the barrier.
  2. BarrierAction Execution:
    • The Runnable passed to the CyclicBarrier constructor (optional) is run by one of the threads before proceeding.
  3. Reuse:
    • The CyclicBarrier resets automatically after releasing the threads, so it can be reused for the next phase.
  4. Exceptions:
    • If one thread fails (e.g., throws an exception during await()), the barrier is broken, and other threads waiting at that barrier will also throw a BrokenBarrierException.

This implementation is widely used in parallel processing scenarios where tasks are executed in phases and synchronized at specific points.

A basic understanding of Java Classes and Interfaces

Java classes and interfaces are essential building blocks in Java programming. Here’s a simple explanation of both:


Java Classes

A class in Java is a blueprint or template used to create objects (instances). It contains:

  • Fields (Instance Variables): To store the state of objects.
  • Methods: To define behaviors or functionalities of the objects.
  • Constructors: To initialize objects.

Key Characteristics of a Class:

  1. It can extend (inherit from) another class (single inheritance).
  2. It can implement multiple interfaces.
  3. Commonly used for defining real-world entities with their properties and behaviors.

Example of a Class:

public class Animal {
    // Fields
    private String name;
    private int age;

    // Constructor
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Method
    public void speak() {
        System.out.println(name + " says hello!");
    }

    // Getter
    public String getName() {
        return name;
    }
}

How to use a class:

public class Main {
    public static void main(String[] args) {
        Animal dog = new Animal("Buddy", 3);
        dog.speak();  // Output: Buddy says hello!
    }
}

Java Interfaces

An interface in Java is a contract that defines a set of methods that a class must implement. Interfaces do not provide implementation but only the method declarations (method signatures).

Key Characteristics of Interfaces:

  1. A class that implements an interface must provide concrete implementations for all of its methods.
  2. A class can implement multiple interfaces (unlike inheritance where a class can extend only one class).
  3. Interfaces in Java 8+ can have:
    • Default Methods: Methods with a default implementation.
    • Static Methods: Methods that belong to the interface and can be invoked without an instance.

Example of an Interface:

public interface AnimalBehavior {
    void eat();  // Abstract method
    void sleep();
}

Implementing an Interface:

public class Dog implements AnimalBehavior {
    @Override
    public void eat() {
        System.out.println("The dog is eating.");
    }

    @Override
    public void sleep() {
        System.out.println("The dog is sleeping.");
    }
}

How to use the class with interface:

public class Main {
    public static void main(String[] args) {
        AnimalBehavior myDog = new Dog();
        myDog.eat();     // Output: The dog is eating.
        myDog.sleep();   // Output: The dog is sleeping.
    }
}

Differences Between Classes and Interfaces

Feature Class Interface
Purpose Blueprint for creating objects. Contract defining behavior (method signatures).
Inheritance Supports single inheritance. Can be implemented by multiple classes.
Access Modifiers Methods can have different access modifiers. Methods are public by default (abstract).
Implementation Contains method implementations. No method implementations (except default/static in Java 8+).
Usage Used for defining states and behaviors. Provides abstraction and enforces contract.

Summary:

  • Use classes to define what something is and its behavior.
  • Use interfaces to define what something can do (define a contract for behavior).

How to Work with the Root Certificates Included in Java 10

In Java 10, root certificates are included as part of the cacerts file in the Java Runtime Environment (JRE) to establish trust for security protocols like TLS/SSL. Java includes a default set of trusted Certificate Authorities (CAs) in this file. Here’s how you can work with the root certificates in Java 10:


Accessing the Root Certificates

Root certificates in Java 10 are found in the cacerts file, which is located in the lib/security directory in your JRE or JDK installation:

  • Path for JDK: <JAVA_HOME>/lib/security/cacerts
  • Path for JRE: <JAVA_HOME>/jre/lib/security/cacerts (if the setup includes a separate JRE)

Managing Root Certificates Using the keytool Utility

Java provides the keytool command-line utility to manage keystores such as cacerts. You can use it to list, add, or remove root certificates. Here’s how:

1. List Certificates

To view the existing certificates in the cacerts keystore, use the following command:

keytool -list -keystore <JAVA_HOME>/lib/security/cacerts

By default, the password for the cacerts keystore is changeit.

2. Import a New Root Certificate

If you have a custom root certificate (e.g., mycert.crt) that needs to be trusted by Java, import it as follows:

keytool -import -trustcacerts -file mycert.crt -keystore <JAVA_HOME>/lib/security/cacerts -alias myalias
  • Replace mycert.crt with the file path of your certificate.
  • Replace myalias with a unique alias for the certificate.
  • Note: If no password change has been applied, the default password is changeit.

3. Remove a Certificate

If you need to remove a root certificate from the cacerts keystore:

keytool -delete -alias myalias -keystore <JAVA_HOME>/lib/security/cacerts

Replace myalias with the alias of the certificate you want to remove.

4. Change Keystore Password

To change the default password (changeit) for the keystore:

keytool -storepasswd -keystore <JAVA_HOME>/lib/security/cacerts

Exporting Certificates

To export a certificate from the keystore:

keytool -export -alias myalias -file mycert.crt -keystore <JAVA_HOME>/lib/security/cacerts

Troubleshooting and Tips

  1. Backup Before Modifying: Always create a backup of the cacerts file before making changes. If something goes wrong, you can restore the original file.
    cp <JAVA_HOME>/lib/security/cacerts <JAVA_HOME>/lib/security/cacerts.bak
    
  2. Certificate Format: Ensure that the certificates you are working with are in the correct format. Java usually requires certificates in PEM or DER format.

  3. Java Home Environment Variable: Ensure the JAVA_HOME environment variable is set correctly to point to your Java 10 installation.

  4. Truststore for Applications: Applications that need a specific set of certificates can use a custom keystore/truststore by specifying the following JVM arguments:

    -Djavax.net.ssl.trustStore=/path/to/custom/truststore
       -Djavax.net.ssl.trustStorePassword=yourpassword
    

Switching to a Custom Truststore

If you prefer to use a custom truststore instead of altering the cacerts file:

  1. Create a new keystore file:
    keytool -genkey -alias myalias -keyalg RSA -keystore mytruststore.jks
    
  2. Add certificates to this custom truststore using the steps outlined above.

  3. Point the application or JVM to your custom truststore using the -Djavax.net.ssl.trustStore parameter.

Conclusion

Working with the root certificates in Java 10 provides more control over establishing trust with certificate authorities. Tools like keytool simplify this management process, whether you’re adding, removing, or listing certificates in the cacerts keystore. Always follow security best practices when modifying trust settings, and ensure critical backups are in place.

How do I control access to resources using Semaphore?

To control access to shared resources in a multithreaded environment, a Semaphore is frequently used, which is part of the java.util.concurrent package. A semaphore manages a set number of permits that control how many threads can access a shared resource simultaneously. Threads acquire permits before accessing the resource and release the permits after they are done, ensuring controlled and synchronized access.

Here’s how you can use a semaphore to control access to resources:

1. Key Points About Semaphore:

  • Permits: The semaphore holds a set number of permits, which represent the number of threads that can access the resource concurrently.
  • Acquire/Release:
    • A thread must acquire a permit using the acquire() method to access the resource.
    • It must release the permit using release() after finishing its access to the resource.
  • Blocking Behavior: If no permits are available, the acquiring thread will block until a permit is released by another thread.

  • Fairness: You can construct a semaphore in a fair mode to ensure that waiting threads acquire permits in the order they requested them.

2. Example: Semaphore with Limited Access to Resources

Here is a simple example where a semaphore is used to control access to a shared resource (e.g., a connection pool or a printer):

package org.kodejava.util.concurrent;

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

public class SemaphoreExample {

    // Semaphore initialized with 2 permits (only 2 threads can access simultaneously).
    private static final Semaphore semaphore = new Semaphore(2);

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

        // Simulate 5 threads trying to access the shared resource
        for (int i = 1; i <= 5; i++) {
            final int threadId = i;
            executorService.submit(() -> {
                try {
                    // Try to acquire a permit
                    System.out.println("Thread " + threadId + " is trying to acquire a permit.");
                    semaphore.acquire();  // Blocks if no permit is available

                    // Access the shared resource
                    System.out.println("Thread " + threadId + " has acquired a permit.");
                    Thread.sleep(2000);  // Simulate the resource usage

                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // Release the permit after use
                    System.out.println("Thread " + threadId + " is releasing the permit.");
                    semaphore.release();
                }
            });
        }

        executorService.shutdown();
    }
}

3. How It Works:

  1. Initialization: The semaphore is initialized with a number of permits (new Semaphore(2)), allowing only 2 threads to access the resource concurrently.
  2. Acquire: A thread attempts to access the resource by calling semaphore.acquire(). If permits are unavailable, the thread is blocked until a permit is released by another thread.
  3. Critical Section: Once the permit is acquired, it enters the critical section and uses the resource.
  4. Release: After the thread is done using the resource, it calls semaphore.release() to return a permit, allowing other threads to acquire it.

4. Output Example:

When you run the example above, you might see an output like this, showing how only 2 threads can access the resource simultaneously:

Thread 1 is trying to acquire a permit.
Thread 1 has acquired a permit.
Thread 2 is trying to acquire a permit.
Thread 2 has acquired a permit.
Thread 3 is trying to acquire a permit.
Thread 1 is releasing the permit.
Thread 3 has acquired a permit.
Thread 4 is trying to acquire a permit.
Thread 2 is releasing the permit.
Thread 4 has acquired a permit.
Thread 5 is trying to acquire a permit.
Thread 3 is releasing the permit.
Thread 5 has acquired a permit.
Thread 4 is releasing the permit.
Thread 5 is releasing the permit.

Here, only 2 threads are allowed to acquire permits at a time, while others are blocked until permits are released.

5. Fair Ordering:

If you want the semaphore to provide fairness (FIFO order), you can use the constructor:

Semaphore semaphore = new Semaphore(2, true);

The second argument (true) enables fair ordering, making sure the threads acquire permits in the order they requested them.

6. Use Cases:

  • Database Connection Pools: Managing the number of simultaneous connections to a database.
  • Printers: Limiting how many jobs can access a shared printer.
  • Rate Limiting: Throttling the number of threads processing tasks in high-volume systems.