Java progresse, JDK21 arrive, la programmation concurrente n'est plus un cauchemar

Le compte public "Ancient Kite" se concentre sur la technologie back-end, en particulier Java et son écologie environnante.

Bonjour à tous, je suis Kite

J'ai écrit avant pourquoi le nouveau projet a décidé d'utiliser JDK 17. Peu de temps après, sortira bientôt JDK 21. Il semble que Java ait vraiment grandi ces dernières années.

À l'heure actuelle, la dernière version stable de Java est JDK 20, mais il s'agit d'une version de transition. JDK21 est la version LTS, et elle sortira bientôt. Elle sera officiellement publiée en septembre de cette année (c'est-à-dire en septembre 2023).

Mais, devinez quoi, vous devez toujours utiliser Java 8 !

Un modèle de programmation simultanée plus fluide

Si vous pensez toujours qu'il n'est pas nécessaire de vous soucier du JDK17 précédent, alors le JDK21 doit vraiment faire attention. Parce que JDK21 introduit un nouveau type de modèle de programmation concurrente.

La programmation simultanée multi-thread actuelle en Java est certainement une autre partie de nos maux de tête. On dirait qu'elle est difficile à apprendre et difficile à utiliser. Mais en regardant des amis qui utilisent d'autres langues, il n'y a pas du tout de problème de ce genre, comme GoLang, j'ai l'impression que les gens l'utilisent de manière très soyeuse.

JDK21 a apporté de grandes améliorations dans ce domaine, rendant la programmation simultanée Java un peu plus facile et plus fluide. Pour être précis, il y a ces améliorations dans JDK19 ou JDK20.

Qu'est-ce que c'est exactement ? Regardons de plus près. Ce qui suit est la fonctionnalité de JDK21.

Parmi elles Virtual Threads, Scoped Values, Structured Concurrencyse trouvent plusieurs fonctions de programmation concurrente multi-thread. Nous en parlerons aussi principalement aujourd'hui.

Fils virtuels

Les threads virtuels sont des threads basés sur des coroutines qui présentent des similitudes avec les coroutines d'autres langages, mais présentent également quelques différences.

Le thread virtuel est attaché au thread principal. Si le thread principal est détruit, le thread virtuel n'existera plus.

Similitudes:

  1. Les threads virtuels et les coroutines sont des threads légers, et leurs frais généraux de création et de destruction sont inférieurs à ceux des threads de système d'exploitation traditionnels.
  2. Les threads virtuels et les coroutines peuvent basculer entre les threads en suspendant et en reprenant, évitant ainsi la surcharge du changement de contexte de thread.
  3. Les threads virtuels et les coroutines peuvent traiter les tâches de manière asynchrone et non bloquante, améliorant ainsi les performances et la réactivité des applications.

不同之处:

  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中呢,可以用结构化编程实现。

ShutdownOnSuccessCapturez le premier résultat et fermez l'étendue de la tâche pour interrompre les threads en attente et réveiller le thread appelant. Un cas où les résultats de n'importe quelle sous-tâche sont disponibles directement sans attendre les résultats des autres tâches en attente. Il définit des méthodes pour obtenir le premier résultat ou lever une exception si toutes les sous-tâches échouent

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;
}

Arrêt en cas d'échec

Exécutez plusieurs tâches, tant qu'une échoue (une exception se produit ou d'autres exceptions actives sont levées), arrêtez d'autres tâches inachevées, utilisez scope.throwIfFailed pour intercepter et lever une exception. Si toutes les tâches sont OK, utilisez Feture.get() ou *Feture.resultNow() pour obtenir le résultat

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;
}

Exemples de valeurs délimitées

Nous devons l'avoir utilisé auparavant ThreadLocal. C'est une variable locale de thread. Tant que le thread n'est pas détruit, la valeur de la variable dans ThredLocal peut être obtenue à tout moment. Les valeurs portées peuvent également obtenir des variables à tout moment à l'intérieur du fil, mais elles ont un concept de portée et elles seront détruites lorsqu'elles dépasseront la portée.

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());
        }
    }
}

L'exemple ci-dessus simule un processus de connexion utilisateur, en utilisant ScopedValue.newInstance()pour déclarer un user ScopedValue, en utilisant ScopedValue.wherepour ScopedValuedéfinir une valeur et en utilisant la méthode run pour exécuter la prochaine chose à faire, de sorte qu'elle ScopedValuepuisse être obtenue à tout moment dans run(), dans Dans la méthode run, la méthode de connexion d'un service est simulée et LoginUser.getla valeur de l'utilisateur de connexion actuel peut être obtenue directement via la méthode sans passer le paramètre LoginUser.

J'espère aussi que les élèves qui ont gagné quelque chose rejoindront l'émission, retweeteront, cliqueront et regarderont, merci ! À la prochaine. Le compte public "Ancient Kite" se concentre sur la technologie back-end, en particulier Java et son écologie environnante. Des articles seront inclus dans  JavaNewBee  , et il y aura une carte de connaissances Java back-end, où la route de Xiaobai à Da Niu sera empruntée.

Guess you like

Origin juejin.im/post/7239715628172902437