How do I use the Virtual Threads API Project Loom?

The Virtual Threads API is part of Project Loom in the Java platform. With Java 19, virtual threads became available as a preview feature, enabling the creation of lightweight threads that can run concurrently. They work similarly to traditional threads but are much cheaper in terms of memory and thread management because they are managed by the Java runtime, not the operating system. This makes it possible to scale the number of threads easily, even in the millions.

Here’s how you can use the Virtual Threads API:

1. Enable Virtual Threads

Virtual threads are available in Java 19+ as an incubating feature. To use them:

  • Ensure that you’re using a compatible version of Java (Java 19 or later).
  • Add the JVM flag --enable-preview to enable preview features when running your program.

2. Creating Virtual Threads

Java provides the java.lang.Thread class and the Executors utility to work with virtual threads. For example:

Creating a Virtual Thread

You can create and start a virtual thread like this:

Thread.startVirtualThread(() -> {
    System.out.println("This is a virtual thread!");
});

Using Virtual Threads with Executors

The Executors.newVirtualThreadPerTaskExecutor() method creates an ExecutorService that launches a new virtual thread for each task:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> System.out.println("Task on a virtual thread"));
    executor.submit(() -> System.out.println("Another virtual thread task"));
}

3. Advantages of Virtual Threads

  1. Lightweight: Virtual threads are less resource-intensive because they use the Java runtime scheduler rather than the OS scheduler. Millions of threads can be created.
  2. Non-blocking: Blocking operations in virtual threads don’t block OS resources, making them very efficient for I/O-intensive workloads like web servers or concurrent network communication.
  3. Easier Scaling: They simplify concurrent programming by allowing you to continue using the familiar thread-per-task model without worrying about resource limits.
  4. Works with Existing Code: Virtual threads integrate well with existing Java APIs like java.util.concurrent.

4. When to Use Virtual Threads

Virtual threads are ideal for:

  • Concurrent I/O tasks like HTTP servers or database connections.
  • High-concurrency environments where traditional threads might run out of OS resources.
  • Migrating legacy multithreaded code to take advantage of better scalability.

5. Example: HTTP Server with Virtual Threads

Here’s a minimal example showcasing how to use virtual threads for handling multiple HTTP requests:

import java.net.ServerSocket;
import java.net.Socket;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.PrintWriter;

public class VirtualThreadHttpServer {
    public static void main(String[] args) throws Exception {
        try (var serverSocket = new ServerSocket(8080)) {
            while (true) {
                Socket clientSocket = serverSocket.accept();
                Thread.startVirtualThread(() -> handleClient(clientSocket));
            }
        }
    }

    private static void handleClient(Socket clientSocket) {
        try (var in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             var out = new PrintWriter(clientSocket.getOutputStream(), true)) {
            out.println("Hello from the Virtual Thread server!");
            String input;
            while ((input = in.readLine()) != null) {
                System.out.println("Received: " + input);
                if ("exit".equalsIgnoreCase(input)) {
                    break;
                }
                out.println("You said: " + input);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

This makes use of virtual threads to handle each incoming socket connection, which scales efficiently for high-concurrency workloads.

6. Integration with Structured Concurrency

Virtual threads can be combined with structured concurrency (introduced in Java 21) for safer and more manageable multithreading. Structured concurrency allows parent threads to manage the lifecycle of child threads.

Example of Structured Concurrency:

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

public class StructuredConcurrencyExample {
    public static void main(String[] args) throws Exception {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            var future1 = executor.submit(() -> {
                Thread.sleep(500);
                return "Task 1 completed";
            });

            var future2 = executor.submit(() -> {
                Thread.sleep(300);
                return "Task 2 completed";
            });

            // Wait for results
            System.out.println(future1.get());
            System.out.println(future2.get());
        }
    }
}

Keynotes:

  • Virtual threads require no changes in application logic. Code written for traditional Thread can immediately benefit from using virtual threads.
  • They simplify thread management while maintaining excellent performance for non-blocking I/O operations.

Limitations:

  • Virtual threads won’t improve performance for CPU-bound tasks; you still need to consider the number of logical CPUs in your system.
  • JVM preview features need to be enabled since virtual threads are not yet finalized in the standard Java API. Check the latest Java release notes for updates.

How do I collect stream results into an immutable collection?

In Java, you can use the Stream API’s Collectors to gather stream results into an immutable collection. Since Java 10, you can use Collectors.toUnmodifiableList(), Collectors.toUnmodifiableSet(), and other similar methods to collect the results into unmodifiable collections.

Here’s how you can collect the stream results into an immutable collection:

1. Immutable List

To collect the results of a stream into an immutable list:

package org.kodejava.util.stream;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        List<String> immutableList = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableList());

        System.out.println(immutableList);

        // Attempting to modify the list will throw UnsupportedOperationException
        // immutableList.add("date"); // Throws UnsupportedOperationException
    }
}

2. Immutable Set

To collect the results into an immutable set:

package org.kodejava.util.stream;

import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        Set<String> immutableSet = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableSet());

        System.out.println(immutableSet);

        // Attempting to modify the set will throw UnsupportedOperationException
        // immutableSet.add("date"); // Throws UnsupportedOperationException
    }
}

3. Immutable Map

To collect results into an immutable map:

package org.kodejava.util.stream;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        Map<String, Integer> immutableMap = Stream.of("apple", "banana", "cherry")
                .collect(Collectors.toUnmodifiableMap(
                        fruit -> fruit,         // Key mapper: the fruit itself
                        fruit -> fruit.length() // Value mapper: the length of the fruit name
                ));

        System.out.println(immutableMap);

        // Attempting to modify the map will throw UnsupportedOperationException
        // immutableMap.put("date", 4); // Throws UnsupportedOperationException
    }
}

Notes:

  • Unmodifiable vs Immutable: Collections created with Collectors.toUnmodifiableList(), Collectors.toUnmodifiableSet(), and Collectors.toUnmodifiableMap() are unmodifiable. While they cannot be changed (add, remove, replace), immutability might imply further guarantees (e.g., deeply immutable objects inside the collection, which this does not enforce).
  • Introduced in Java 10: toUnmodifiableList(), toUnmodifiableSet(), and toUnmodifiableMap() were introduced in Java 10. If you’re using Java 8 or Java 9, you’ll need a custom approach for creating immutable collections (like Collections.unmodifiableList).

In Java 8:

If you’re stuck on Java 8, you can achieve something similar using Collections.unmodifiableList() or other Collections.unmodifiableXxx methods:

package org.kodejava.util.stream;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ImmutableCollectionExample {
    public static void main(String[] args) {
        List<String> immutableList = Collections.unmodifiableList(
                Stream.of("apple", "banana", "cherry").collect(Collectors.toList())
        );

        System.out.println(immutableList);
        // immutableList.add("date"); // Throws UnsupportedOperationException
    }
}

This approach, however, wraps an existing modifiable collection, so try to update your project to take advantage of Java 10+ features.

How do I use LinkedHashMap for predictable iteration order?

In Java, a LinkedHashMap is a subtype of HashMap that maintains a predictable iteration order. It uses a doubly linked list to store the entries in insertion order (or, optionally, access order). Here’s how you can use LinkedHashMap for predictable iteration order:

1. Maintaining Insertion Order

By default, a LinkedHashMap iterates its entries in the order they were inserted. This is useful when you want to retrieve elements in the same order you added them.

Here’s an example:

package org.kodejava.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        // Creating LinkedHashMap
        Map<String, Integer> map = new LinkedHashMap<>();

        // Adding elements (insertion order)
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);
        map.put("Four", 4);

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

One => 1
Two => 2
Three => 3
Four => 4

In this example, the elements are iterated in the same order they were inserted.


2. Maintaining Access Order

You can configure a LinkedHashMap to maintain access order, which means it reorders entries based on the most recent access. To enable access order, you must use the constructor that takes a boolean parameter for accessOrder.

Here’s an example:

package org.kodejava.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class AccessOrderExample {
    public static void main(String[] args) {
        // Creating LinkedHashMap with access-order
        Map<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);

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

        // Accessing some elements
        map.get("One");  // Access "One"
        map.get("Three"); // Access "Three"

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

Two => 2
One => 1
Three => 3

In this case:

  • Initially, the insertion order was One, Two, Three.
  • After accessing One and Three, they were moved to the end, making Two the first in the iteration order.

3. Removing the Oldest Entry with Access Order

If needed, you can use a LinkedHashMap in combination with its removeEldestEntry method to automatically remove the oldest entry (e.g., implementing a cache).

Here’s how:

package org.kodejava.util;

import java.util.LinkedHashMap;
import java.util.Map;

public class RemoveEldestExample {
    public static void main(String[] args) {
        // Create LinkedHashMap with override for removeEldestEntry
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>(3, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
                return size() > 3; // Remove oldest if size > 3
            }
        };

        // Adding elements
        map.put("One", 1);
        map.put("Two", 2);
        map.put("Three", 3);
        map.put("Four", 4); // "One" will be removed here

        // Accessing some elements
        map.get("Two");
        map.put("Five", 5); // "Three" will be removed here

        // Iterating through the map
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " => " + entry.getValue());
        }
    }
}

Output:

Four => 4
Two => 2
Five => 5

Explanation:

  1. The map was set to remove the eldest (first) entry when its size exceeds 3.
  2. When "Four" was added, "One" was removed because the size limit was exceeded.
  3. When "Five" was added, "Three" was removed, as it was now the eldest entry after accessing "Two".

Summary of Key Points:

  1. Insertion Order: By default, the iteration order matches the insertion order.
  2. Access Order: Can be enabled using the LinkedHashMap constructor with accessOrder = true.
  3. Custom Behavior: Override the removeEldestEntry method to create a fixed-size cache or similar functionality.

LinkedHashMap is handy when you need consistent iteration order (e.g., for caches, ordering-sensitive collections).

How to Use System.currentTimeMillis() for Performance Timing

In Java, System.currentTimeMillis() is commonly used as a simple way to measure the execution time of a block of code or a specific operation in terms of milliseconds. Here’s how you can effectively use it for performance timing:

Example Usage

package org.kodejava.lang;

public class PerformanceTimingExample {
    public static void main(String[] args) {
        // Record the start time
        long startTime = System.currentTimeMillis();

        // The code you want to measure
        performOperation();

        // Record the end time
        long endTime = System.currentTimeMillis();

        // Calculate the elapsed time
        long elapsedTime = endTime - startTime;

        // Print the result
        System.out.println("Execution time: " + elapsedTime + " milliseconds");
    }

    private static void performOperation() {
        try {
            // Simulate time-consuming task
            Thread.sleep(2000); // Sleep for 2 seconds
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Steps Explained

  1. Record Start Time: Use System.currentTimeMillis() before the block of code you want to measure.
  2. Execute Operation: Run the code or process whose performance you need to measure.
  3. Record End Time: Capture the time after the code execution using System.currentTimeMillis().
  4. Calculate Elapsed Time: Subtract the start time from the end time to get the elapsed time in milliseconds.
  5. Output Results: Display or log the elapsed time for performance analysis.

Things to Keep in Mind

  • Resolution: System.currentTimeMillis() measures the current time in milliseconds since the Unix epoch (January 1, 1970). However, its granularity may vary depending on the system, and it is not as precise as System.nanoTime() for very fine-grained measurements.
  • Avoid Garbage Collection Interference: When measuring performance, ensure that garbage collection has minimal impact by warming up the JVM and avoiding memory-intensive operations.
  • Use System.nanoTime() for Better Precision: If you need higher precision or want to avoid timer granularity issues, consider using System.nanoTime() instead. This measures elapsed time in nanoseconds and is suitable for shorter durations.

Example with System.nanoTime()

package org.kodejava.lang;

public class NanoTimingExample {
    public static void main(String[] args) {
        // Record the start time
        long startTime = System.nanoTime();

        // The code you want to measure
        performOperation();

        // Record the end time
        long endTime = System.nanoTime();

        // Calculate the elapsed time in milliseconds
        long elapsedTime = (endTime - startTime) / 1_000_000;

        // Print the result
        System.out.println("Execution time: " + elapsedTime + " milliseconds");
    }

    private static void performOperation() {
        try {
            // Simulate time-consuming task
            Thread.sleep(2000); // Sleep for 2 seconds
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Conclusion

System.currentTimeMillis() is a simple and effective method to time operations, especially those involving multiple seconds or milliseconds. However, for finer-grained timing or benchmarking (e.g., sub-millisecond accuracy), prefer System.nanoTime(). Always ensure that your measurements are consistent and unaffected by other system activities, such as garbage collection or OS-level processes.

How to Use StringBuilder for Efficient String Concatenation

In Java, using StringBuilder is a common way to handle efficient string concatenation, especially when working with loops or when you need to concatenate a large number of strings. Unlike String, which is immutable, StringBuilder is mutable and modifies its internal character array without creating new objects, hence improving performance.

Here’s how you can use StringBuilder for efficient string concatenation:

1. Creating a StringBuilder instance

You can create a new instance of StringBuilder using its constructor:

StringBuilder sb = new StringBuilder();

You can also initialize it with an existing string:

StringBuilder sb = new StringBuilder("Hello");

2. Appending Strings

Use the .append() method to concatenate strings:

StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
System.out.println(sb.toString()); // Output: "Hello World"

Here, the append() method modifies the existing StringBuilder instance.


3. Inserting Strings

To insert a string at a specific position, use the .insert() method:

StringBuilder sb = new StringBuilder("Hello World");
sb.insert(6, "Beautiful ");
System.out.println(sb.toString()); // Output: "Hello Beautiful World"

4. Replacing Part of the String

You can replace part of the string using .replace():

StringBuilder sb = new StringBuilder("Hello Java");
sb.replace(6, 10, "World");
System.out.println(sb.toString()); // Output: "Hello World"

5. Reversing the String

You can reverse the string using .reverse():

StringBuilder sb = new StringBuilder("abcd");
sb.reverse();
System.out.println(sb.toString()); // Output: "dcba"

6. Deleting Characters or Substrings

You can use .delete() or .deleteCharAt() to remove parts of the string:

StringBuilder sb = new StringBuilder("Hello World");
sb.delete(5, 11); // Remove characters from index 5 to 10
System.out.println(sb.toString()); // Output: "Hello"

sb.deleteCharAt(0); // Remove the character at index 0
System.out.println(sb.toString()); // Output: "ello"

7. Converting Back to a String

Once you are done building the string, convert it back to a String using .toString():

StringBuilder sb = new StringBuilder("Hello");
String result = sb.toString();
System.out.println(result); // Output: "Hello"

8. StringBuilder in Loops

It is particularly useful when appending strings in loops to avoid the overhead of creating multiple String instances:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
    sb.append("Number ").append(i).append(", ");
}
System.out.println(sb.toString());
// Output: "Number 0, Number 1, Number 2, Number 3, Number 4, "

Example: Complete Code

Here’s a complete example that combines multiple methods:

public class StringBuilderExample {
    public static void main(String[] args) {
        // Create a StringBuilder
        StringBuilder sb = new StringBuilder("Example");

        // Append strings
        sb.append(" of").append(" StringBuilder");

        // Insert a string
        sb.insert(8, " java");

        // Replace a substring
        sb.replace(0, 7, "Demo");

        // Delete part of the string
        sb.delete(5, 10);

        // Reverse the string
        sb.reverse();

        // Convert back to String
        System.out.println(sb.toString());
    }
}

Output:

redliuBgnirtS fo omeD

Performance Comparison: String vs StringBuilder

Here’s a quick comparison of the performance:

  • String: Creates a new object for each concatenation, which is inefficient in loops.
  • StringBuilder: Reuses the same object and modifies its internal buffer, which is much faster.

So, whenever you’re performing a lot of string manipulations, especially in loops, it’s highly recommended to use StringBuilder.