avataromgzui

Summary

Java 21 introduces new features such as Virtual Threads, Structured Concurrency, and Scoped Values to improve concurrent programming.

Abstract

Java 21 has made significant improvements in the area of concurrent programming, making it easier and smoother. The new features introduced in JDK21 include Virtual Threads, Structured Concurrency, and Scoped Values. Virtual Threads are coroutine-based threads that have similarities to coroutines in other languages but also have some differences. Structured Concurrency is a programming paradigm that aims to simplify concurrent programming by providing a structured and easy-to-follow approach. Scoped Values are a feature in JDK 20 that allow developers to create scoped values that are restricted to a specific thread or task.

Opinions

  • Virtual Threads can make multi-threaded programming easier and more efficient.
  • Structured Concurrency can make multi-threaded programming easier and more reliable.
  • Scoped Values can be used to pass contextual information between different parts of the application.
  • The introduction of these features in JDK21 can improve applications' performance and resource utilization, while also using traditional thread-related APIs.
  • These features can help developers focus more on business logic without paying too much attention to underlying thread management.

Java 21 in Practice, Virtual Threads, Structured Concurrency, and Scoped Values

Photo by Clément Hélardot on Unsplash

If you still think that there is no need to toss about the previous JDK17, then JDK21 needs to pay attention. Because JDK21 introduces a new type of concurrent programming model.

The current multi-threaded concurrent programming in Java is another part of our headaches. It feels like it is difficult to learn and difficult to use. But looking back at friends who use other languages, there is no such trouble at all, such as GoLang, it feels very silky to use.

JDK21 has made great improvements in this area, making Java concurrent programming a little easier and smoother. To be precise, there are these improvements in JDK19 or JDK20.

Among them, Virtual Threads, Scoped Values, and Structured Concurrency are several functions for multi-threaded concurrent programming.

1. Virtual Threads

Virtual threads are coroutine-based threads that have similarities to coroutines in other languages but also have some differences.

The virtual thread is attached to the main thread. The virtual thread will no longer exist if the main thread is destroyed.

Similarities:

  • Both virtual threads and coroutines are lightweight, and their creation and destruction overheads are smaller than traditional operating system threads.
  • Both virtual threads and coroutines can switch between threads by suspending and resuming, thus avoiding the overhead of thread context switching.
  • Both virtual threads and coroutines can process tasks in an asynchronous and non-blocking manner, improving application performance and responsiveness.

The difference:

  • Virtual threads are implemented at the JVM level, while coroutines are implemented at the language level. Therefore, the implementation of virtual threads can be used with any language that supports the JVM, while the implementation of coroutines requires specific programming language support.
  • Virtual threads are a thread-based implementation of coroutines, so they can use thread-related APIs such as ThreadLocal, Lock, and Semaphore. Coroutines do not depend on threads and usually require specific asynchronous programming frameworks and APIs.
  • The scheduling of virtual threads is managed by the JVM, while the scheduling of coroutines is managed by the programming language or asynchronous programming framework. Therefore, virtual threads can better cooperate with other threads, while coroutines are better for handling asynchronous tasks.

In general, virtual threads are a new thread type that can improve applications' performance and resource utilization, while also using traditional thread-related APIs. Virtual threads have many similarities to coroutines, but there are also some differences.

Virtual threads can indeed make multithreaded programming easier and more efficient. Compared with traditional operating system threads, the overhead of creating and destroying virtual threads is smaller, and the overhead of thread context switching is also smaller, so resource consumption and performance bottlenecks in multi-threaded programming can be greatly reduced.

Using virtual threads, developers can write code like writing traditional thread code without worrying about the number and scheduling of threads, because the JVM will automatically manage the number and scheduling of virtual threads. In addition, virtual threads also support traditional thread-related APIs, such as ThreadLocal, Lock, and Semaphore, which makes it easier for developers to migrate traditional thread code to virtual threads.

The introduction of virtual threads makes multi-thread programming more efficient, simpler, and safer, allowing developers to focus more on business logic without paying too much attention to underlying thread management.

2. Structured Concurrency

Structured Concurrency is a programming paradigm that aims to simplify concurrent programming by providing a structured and easy-to-follow approach. Using structured concurrency, developers can create concurrent code that is easier to understand and debug, and less prone to race conditions and other concurrency-related bugs. In structured concurrency, all concurrent code is structured into well-defined units of work called tasks. Tasks are created, executed, and completed in a structured manner, and the execution of a task is always guaranteed to complete before its parent task completes.

Structured Concurrency can make multi-threaded programming easier and more reliable. In traditional multi-threaded programming, the startup, execution, and termination of threads are manually managed by developers, so problems such as thread leaks, deadlocks, and improper exception handling are prone to occur.

Using structured concurrency, developers can organize concurrent tasks more naturally, making the dependencies between tasks clearer and the code logic more concise. Structured concurrency also provides some exception-handling mechanisms to better manage exceptions in concurrent tasks and avoid program crashes or data inconsistencies caused by exceptions.

In addition, structured concurrency can also prevent resource competition and starvation by limiting the number and priority of concurrent tasks. These features make it easier for developers to implement efficient and reliable concurrent programs without paying too much attention to underlying thread management.

3. Scoped Values

Scoped values are a feature in JDK 20 that allow developers to create scoped values that are restricted to a specific thread or task. Scoped values are similar to thread-local variables, but are designed to work with virtual threads and structured concurrency. They allow developers to pass values between tasks and virtual threads in a structured manner, without complex synchronization or locking mechanisms. Scope values can be used to pass contextual information between different parts of the application, such as user authentication or requesting specific data.

4. Practice

Before proceeding with the following exploration, you need to download at least JDK19 or directly download JDK20. JDK 20 is currently (as of September 2023) the highest version officially released. If you use JDK 19, you cannot experience the Scoped Values function.

Or simply download the Early-Access Builds of JDK 21.

If you are using IDEA, then your IDEA version must be at least version 2022.3 or later, otherwise, such a new JDK version will not be supported.

If you are using JDK19 or JDK20, you should set the language level to 19 or 20 in your project settings. Otherwise, you will be prompted that you cannot use the preview version function when compiling. Virtual thread is the function of the preview version.

If you are using JDK21, set the language level to X -Experimental Features. In addition, because JDK21 is not an official version, you need to go to the IDEA settings (note that it is the IDEA settings, not the project settings), and set this The Target bytecode version of the project is manually changed to 21. Currently, the highest option is 20, which is JDK20. After setting it to 21, you can use these functions in JDK21.

4.1 Virtual Threads

How do we start the thread now?

First, declare a thread class, implements from Runnable, and implement the run method.

public class SimpleThread implements Runnable{

    @Override
    public void run() {
        System.out.println("name:" + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Then you can use this thread class and start the thread.

Thread thread = new Thread(new SimpleThread());
thread.start();

After having a virtual thread, how to achieve it?

Thread.ofPlatform().name("thread-test").start(new SimpleThread());

Below are several ways to use virtual threads.

1. Start a virtual thread directly

Thread thread = Thread.startVirtualThread(new SimpleThread());

2. Use ofVirtual(), builder mode to start virtual threads, you can set thread name, priority, exception handling, and other configurations

Thread.ofVirtual()
                .name("thread-test")
                .start(new SimpleThread());
// Or
Thread thread = Thread.ofVirtual()
  .name("thread-test")
  .uncaughtExceptionHandler((t, e) -> {
    System.out.println(t.getName() + e.getMessage());
  })
  .unstarted(new SimpleThread());
thread.start();

3. Use Factory to create threads

ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(new SimpleThread());
thread.setName("thread-test");
thread.start();

4. Use Executors

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
Future<?> submit = executorService.submit(new SimpleThread());
Object o = submit.get();

4.2 Structured Concurrency

Think about the following scenario. Suppose you have three tasks to be performed at the same time. As long as any one of the tasks is completed and returns a result, the result can be used directly, and the other two tasks can be stopped. For example, a weather service obtains weather conditions through three channels, as long as one channel returns it.

In this scenario, what should be done under Java 8, of course, is also possible.

List<Future<String>> futures = executor.invokeAll(tasks);

String result = executor.invokeAny(tasks);

Use the invokeAll and invokeAny implementations of ExecutorService, but there will be some extra work. After getting the first result, you need to manually close another thread.

In JDK21, it can be implemented with structured programming.

ShutdownOnSuccess captures the first result and closes the task scope to interrupt outstanding threads and wake up the calling thread.

A case where the results of any subtask are available directly without waiting for the results of other outstanding tasks.

It defines methods to get the first result or throw an exception if all subtasks fail

public static void main(String[] args) throws IOException {
  try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
    Future<String> res1 = scope.fork(() -> runTask(1));
    Future<String> res2 = scope.fork(() -> runTask(2));
    Future<String> res3 = scope.fork(() -> runTask(3));
    scope.join();
    System.out.println("scope:" + scope.result());
  } catch (ExecutionException | InterruptedException e) {
    throw new RuntimeException(e);
  }
}

public static String runTask(int i) throws InterruptedException {
  Thread.sleep(1000);
  long l = new Random().nextLong();
  String s = String.valueOf(l);
  System.out.println(i + "task:" + s);
  return s;
}

ShutdownOnFailure

Execute multiple tasks, as long as one fails (an exception occurs or other active exceptions are thrown), stop other unfinished tasks, and use scope.throwIfFailed to catch and throw an exception.

If all tasks are OK, use Feture.get() or *Feture.resultNow() to get the result

public static void main(String[] args) throws IOException {
  try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> res1 = scope.fork(() -> runTaskWithException(1));
    Future<String> res2 = scope.fork(() -> runTaskWithException(2));
    Future<String> res3 = scope.fork(() -> runTaskWithException(3));
    scope.join();
    scope.throwIfFailed(Exception::new);

    String s = res1.resultNow();
    System.out.println(s);
    String result = Stream.of(res1, res2,res3)
      .map(Future::resultNow)
      .collect(Collectors.joining());
    System.out.println("result:" + result);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

public static String runTaskWithException(int i) throws InterruptedException {
  Thread.sleep(1000);
  long l = new Random().nextLong(3);
  if (l == 0) {
    throw new InterruptedException();
  }
  String s = String.valueOf(l);
  System.out.println(i + "task:" + s);
  return s;
}

4.3 Scoped Values

We must have used ThreadLocal, which is a thread-local variable, as long as the thread is not destroyed, the variable value in ThredLocal can be obtained at any time. Scoped Values can also obtain variables at any time inside the thread, but it has a concept of scope and will be destroyed when it exceeds the scope.

public class ScopedValueExample {
    final static ScopedValue<String> LoginUser = ScopedValue.newInstance();

    public static void main(String[] args) throws InterruptedException {
        ScopedValue.where(LoginUser, "Tom")
                .run(() -> {
                    new Service().login();
                });

        Thread.sleep(2000);
    }

    static class Service {
        void login(){
            System.out.println("user:" + LoginUser.get());
        }
    }
}

The above example simulates a user login process, uses ScopedValue.newInstance() to declare a ScopedValue, uses ScopedValue.where to set a value for the ScopedValue, and uses the run method to execute the next thing to be done so that the ScopedValue is in The inside of run() can be obtained at any time. In the run method, the login method of service is simulated. Without passing the parameter LoginUser, the value of the currently logged-in user can be obtained directly through the LoginUser.get method.

Finally

Thanks for reading. I am looking forward to your following and reading more high-quality articles.

Java
Spring Boot
Backend
Software Development
Programming
Recommended from ReadMedium