Java avanza, llega JDK21, la programación concurrente ya no es una pesadilla

La cuenta pública "Ancient Kite" se centra en la tecnología de back-end, especialmente Java y su ecología circundante.

Hola a todos, soy Kite

Escribí antes por qué el nuevo proyecto decidió usar JDK 17. No mucho después, saldrá pronto JDK 21. Parece que Java realmente ha crecido en los últimos años.

Actualmente, la última versión estable de Java es JDK 20, pero esta es una versión de transición. JDK21 es la versión LTS y se lanzará próximamente. Se lanzará oficialmente en septiembre de este año (es decir, septiembre de 2023).

Pero, adivina qué, ¡todavía debes estar usando Java 8!

Un modelo de programación concurrente más fluido

Si todavía sientes que no hay necesidad de hablar sobre el JDK17 anterior, entonces JDK21 realmente necesita prestar atención. Porque JDK21 introduce un nuevo tipo de modelo de programación concurrente.

La programación concurrente actual de subprocesos múltiples en Java es definitivamente otra parte de nuestros dolores de cabeza, parece que es difícil de aprender y de usar. Pero mirando hacia atrás a los amigos que usan otros idiomas, no hay tal problema en absoluto, como GoLang, siento que la gente lo usa de manera muy sedosa.

JDK21 ha realizado grandes mejoras en esta área, haciendo que la programación concurrente de Java sea un poco más fácil y fluida. Para ser precisos, existen estas mejoras en JDK19 o JDK20.

¿Qué es exactamente? Miremos más de cerca. La siguiente es la característica de JDK21.

Entre ellos Virtual Threads, hay varias funciones para la programación concurrente Scoped Valuesde Structured Concurrencysubprocesos múltiples. También hablaremos principalmente de ellos hoy.

Subprocesos virtuales

Los subprocesos virtuales son subprocesos basados ​​en rutinas que tienen similitudes con las corrutinas en otros idiomas, pero también tienen algunas diferencias.

El subproceso virtual se adjunta al subproceso principal. Si se destruye el subproceso principal, el subproceso virtual ya no existirá.

similitudes:

  1. Tanto los subprocesos virtuales como las corrutinas son subprocesos livianos, y sus gastos generales de creación y destrucción son más pequeños que los subprocesos tradicionales del sistema operativo.
  2. Tanto los subprocesos virtuales como las corrutinas pueden cambiar entre subprocesos mediante la suspensión y la reanudación, evitando así la sobrecarga del cambio de contexto de subprocesos.
  3. Tanto los subprocesos virtuales como las corrutinas pueden procesar tareas de forma asincrónica y sin bloqueos, lo que mejora el rendimiento y la capacidad de respuesta de las aplicaciones.

不同之处:

  1. 虚拟线程是在 JVM 层面实现的,而协程则是在语言层面实现的。因此,虚拟线程的实现可以与任何支持 JVM 的语言一起使用,而协程的实现则需要特定的编程语言支持。
  2. 虚拟线程是一种基于线程的协程实现,因此它们可以使用线程相关的 API,如 ThreadLocalLockSemaphore。而协程则不依赖于线程,通常需要使用特定的异步编程框架和 API。
  3. 虚拟线程的调度是由 JVM 管理的,而协程的调度是由编程语言或异步编程框架管理的。因此,虚拟线程可以更好地与其他线程进行协作,而协程则更适合处理异步任务。

总的来说,虚拟线程是一种新的线程类型,它可以提高应用程序的性能和资源利用率,同时也可以使用传统线程相关的 API。虚拟线程与协程有很多相似之处,但也存在一些不同之处。

虚拟线程确实可以让多线程编程变得更简单和更高效。相比于传统的操作系统线程,虚拟线程的创建和销毁的开销更小,线程上下文切换的开销也更小,因此可以大大减少多线程编程中的资源消耗和性能瓶颈。

使用虚拟线程,开发者可以像编写传统的线程代码一样编写代码,而无需担心线程的数量和调度,因为 JVM 会自动管理虚拟线程的数量和调度。此外,虚拟线程还支持传统线程相关的 API,如 ThreadLocalLockSemaphore,这使得开发者可以更轻松地迁移传统线程代码到虚拟线程。

虚拟线程的引入,使得多线程编程变得更加高效、简单和安全,使得开发者能够更加专注于业务逻辑,而不必过多地关注底层的线程管理。

结构化并发(Structured Concurrency)

结构化并发是一种编程范式,旨在通过提供结构化和易于遵循的方法来简化并发编程。使用结构化并发,开发人员可以创建更容易理解和调试的并发代码,并且不容易出现竞争条件和其他与并发有关的错误。在结构化并发中,所有并发代码都被结构化为称为任务的定义良好的工作单元。任务以结构化方式创建、执行和完成,任务的执行总是保证在其父任务完成之前完成。

Structured Concurrency(结构化并发)可以让多线程编程更加简单和可靠。在传统的多线程编程中,线程的启动、执行和结束是由开发者手动管理的,因此容易出现线程泄露、死锁和异常处理不当等问题。

使用结构化并发,开发者可以更加自然地组织并发任务,使得任务之间的依赖关系更加清晰,代码逻辑更加简洁。结构化并发还提供了一些异常处理机制,可以更好地管理并发任务中的异常,避免因为异常而导致程序崩溃或数据不一致的情况。

除此之外,结构化并发还可以通过限制并发任务的数量和优先级,防止资源竞争和饥饿等问题的发生。这些特性使得开发者能够更加方便地实现高效、可靠的并发程序,而无需过多关注底层的线程管理。

作用域值(Scoped Values)

作用域值是JDK 20中的一项功能,允许开发人员创建作用域限定的值,这些值限定于特定的线程或任务。作用域值类似于线程本地变量,但是设计为与虚拟线程和结构化并发配合使用。它们允许开发人员以结构化的方式在任务和虚拟线程之间传递值,无需复杂的同步或锁定机制。作用域值可用于在应用程序的不同部分之间传递上下文信息,例如用户身份验证或请求特定数据。

试验一下

进行下面的探索之前,你要下载至少 JDK19或者直接下载 JDK20,JDK 20 目前(截止到2023年9月份)是正式发布的最高版本,如果你用 JDK 19的话,没办法体验到Scoped Values的功能。

或者是直接下载 JDK 21 的 Early-Access Builds(早期访问版本)。在这个地址下载 「jdk.java.net/21/」,下载对应的版…

如果你用的是 IDEA ,那你的IDEA 版本最起码是2022.3 这个版本或者之后的,否则不支持这么新的 JDK 版本。

如果你用的是 JDK19或者 JDK20的话,要在你的项目设置中将 language level设置为19或20的 Preview 级别,否则编译的时候会提示你无法使用预览版的功能,虚拟线程就是预览版的功能。

如果你用的是 JDK21的话,将 language level 设置为 X -Experimental Features,另外,因为 JDK21不属于正式版本,所以需要到 IDEA 的设置中(注意是 IDEA 的设置,不是项目的设置了),将这个项目的 Target bytecode version手动修改为21,目前可选的最高就是20,也就是JDK20。设置为21之后,就可以使用 JDK21中的这些功能了。

虚拟线程的例子

我们现在启动线程是怎么做的呢?

先声明一个线程类,implementsRunnable,并实现 run方法。

public class SimpleThread implements Runnable{

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

然后就可以使用这个线程类,然后启动线程了。

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

中规中矩,没毛病。

有了虚拟线程之后呢,怎么实现呢?

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

下面是几种使用虚拟线程的方式。

1、直接启动一个虚拟线程

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

2、使用 ofVirtual(),builder 方式启动虚拟线程,可以设置线程名称、优先级、异常处理等配置

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

3、使用 Factory 创建线程

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

4、使用 Executors 方式

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

结构化编程的例子

想一下下面这个场景,假设你有三个任务要同时进行,只要任意一个任务执行完成并返回结果了,那就可以直接用这个结果了,其他的两个任务就可以停止了。比如说一个天气服务,通过三个渠道获取天气情况,只要有一个渠道返回就可以了。

这种场景下, 在 Java 8 下应该怎么做呢,当然也可以了。

// 执行任务并返回 Future 对象列表
List<Future<String>> futures = executor.invokeAll(tasks);

// 等待任一任务完成并获取结果
String result = executor.invokeAny(tasks);

使用 ExecutorServiceinvokeAllinvokeAny实现,但是会有一些额外的工作,在拿到第一个结果后,要手动关闭另外的线程。

而 JDK21中呢,可以用结构化编程实现。

ShutdownOnSuccessCapture el primer resultado y cierre el alcance de la tarea para interrumpir los subprocesos pendientes y activar el subproceso que llama. Un caso donde los resultados de cualquier subtarea están disponibles directamente sin esperar los resultados de otras tareas pendientes. Define métodos para obtener el primer resultado o lanzar una excepción si fallan todas las subtareas

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 + "个任务:" + s);
  return s;
}

ShutdownOnFailure

Ejecute varias tareas, siempre que una falle (ocurre una excepción o se lanzan otras excepciones activas), detenga otras tareas inconclusas, use scope.throwIfFailed para capturar y lanzar una excepción. Si todas las tareas están bien, use Feture.get() o *Feture.resultNow() para obtener el resultado

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(); //或 res1.get()
    System.out.println(s);
    String result = Stream.of(res1, res2,res3)
      .map(Future::resultNow)
      .collect(Collectors.joining());
    System.out.println("直接结果:" + result);
  } catch (Exception e) {
    e.printStackTrace();
    //throw new RuntimeException(e);
  }
}

// 有一定几率发生异常
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 + "个任务:" + s);
  return s;
}

Ejemplos de valores de ámbito

Debemos haberlo usado antes ThreadLocal, es una variable local de hilo, mientras no se destruya el hilo, el valor de la variable en ThredLocal se puede obtener en cualquier momento. Scoped Values ​​también puede obtener variables en cualquier momento dentro del hilo, pero tiene un concepto de alcance, y se destruirá cuando exceda el alcance.

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

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

        Thread.sleep(2000);
    }

    static class Service {
        void login(){
            System.out.println("当前登录用户是:" + LoginUser.get());
        }
    }
}

El ejemplo anterior simula un proceso de inicio de sesión de usuario, usando ScopedValue.newInstance()para declarar un usuario ScopedValue, usando ScopedValue.wherepara ScopedValueestablecer un valor y usando el método de ejecución para ejecutar lo siguiente que se debe hacer, de modo que se ScopedValuepueda obtener en cualquier momento dentro de run(), en En el método de ejecución, se simula el método de inicio de sesión de un servicio y LoginUser.getel valor del usuario de inicio de sesión actual se puede obtener directamente a través del método sin pasar el parámetro LoginUser.

También espero que los estudiantes que hayan ganado algo se unan al programa, retuiteen, hagan clic y miren, ¡gracias! Nos vemos la próxima vez. La cuenta pública "Ancient Kite" se centra en la tecnología de back-end, especialmente Java y su ecología circundante. Los artículos se incluirán en  JavaNewBee  , y habrá un mapa de conocimiento de back-end de Java, donde se tomará el camino de Xiaobai a Da Niu.

Supongo que te gusta

Origin juejin.im/post/7239715628172902437
Recomendado
Clasificación