Executor

Executor making it easier to manage thread execution. Instead of manage thread manually, you submit tasks to Executor, the tasks will be run concurrently within a pool of threads.

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

public class Main {

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

        executor.submit(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        });

        executor.submit(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        });

        executor.shutdown();
    }
}

The output shoul look like following:

pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-1 5
pool-1-thread-2 0
pool-1-thread-1 6
pool-1-thread-2 1
pool-1-thread-2 2
pool-1-thread-2 3
pool-1-thread-2 4
pool-1-thread-2 5
pool-1-thread-2 6
pool-1-thread-2 7
pool-1-thread-2 8
pool-1-thread-2 9
pool-1-thread-1 7
pool-1-thread-1 8
pool-1-thread-1 9

Submit more tasks:

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

public class Main {

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

        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        executor.submit(task);
        executor.submit(task);
        executor.submit(task);

        executor.shutdown();
    }
}

Still two threads running at the same time:

pool-1-thread-2 0
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-2 1
pool-1-thread-2 2
pool-1-thread-1 2
pool-1-thread-2 3
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-2 4
pool-1-thread-2 0
pool-1-thread-2 1
pool-1-thread-2 2
pool-1-thread-2 3
pool-1-thread-2 4

Virtual Threads

Using virtual threads via Executors.newVirtualThreadPerTaskExecutor():

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Instant startTime = Instant.now(); 
        // ExecutorService executor = Executors.newCachedThreadPool();
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        AtomicLong memUsage = new AtomicLong(0L);

        for (int i = 0; i < 100_000; i++) {
            executor.submit(() -> {
                try {
                    Thread.sleep(100);
                    Long usage = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
                    if (memUsage.get() < usage) {
                        memUsage.set(usage);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);

        System.out.println("Execution time: " + Duration.between(startTime, Instant.now()) + " Memory: " + memUsage.get());
    }
}

It's obvious that virtual threads are much more efficient than the platform thread:

Execution time: PT4.35937S Memory: 1876
Execution time: PT0.410156S Memory: 203