大众号「古时的风筝」,专注于后端技能,尤其是 Java 及周边生态。

大家好,我是风筝

之前写过 新项目为什么决定用 JDK 17了,还没过多久,JDK 21 立刻就出来了,看来 Java 这几年真的是长进了。

现在 Java 的最新稳定版是 JDK 20,但这是个过渡版,JDK21便是 LTS 版的了,也快要发布了,在本年9月份(也便是2023年9月)就要正式发布了。

可是,猜都不必猜,你必定还在用 Java 8 吧!

更丝滑的并发编程形式

假如说之前的 JDK17你还觉得没必要折腾,那 JDK21确实有必要重视一下了。由于 JDK21 引入了一种新型的并发编程形式。

当时 Java 中的多线程并发编程必定是另咱们都非常头疼的一部分,感觉便是学起来难啃,用起来难用。可是回头看看运用其他言语的朋友们,根本就没有这个烦恼嘛,比方 GoLang,感觉人家用起来就很丝滑呢。

JDK21 中就在这方面做了很大的改进,让Java并发编程变得更简单一点,更丝滑一点。确切的说,在 JDK19或JDK20中就有这些改进了。

那详细是什么呢?让咱们来详细来看一下。下面是JDK21的 Feature。

Java上进了,JDK21 要来了,并发编程再也不是噩梦了

其中Virtual ThreadsScoped ValuesStructured Concurrency便是针对多线程并发编程的几个功用。咱们今天也主要来说一下他们。

虚拟线程(Virtual Threads)

虚拟线程是根据协程的线程,它们与其他言语中的协程具有相似之处,但也存在一些不同之处。

虚拟线程是依附于主线程的,假如主线程毁掉了,那虚拟线程也不复存在。

相同之处:

  1. 虚拟线程和协程都是轻量级的线程,它们的创立和毁掉的开支都比传统的操作系统线程要小。
  2. 虚拟线程和协程都能够经过暂停和康复来完结线程之间的切换,从而防止了线程上下文切换的开支。
  3. 虚拟线程和协程都能够运用异步和非堵塞的办法来处理使命,进步应用程序的功用和响应速度。

不同之处:

  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的功用。

Java上进了,JDK21 要来了,并发编程再也不是噩梦了

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

Java上进了,JDK21 要来了,并发编程再也不是噩梦了

假如你用的是 IDEA ,那你的IDEA 版别最起码是2022.3 这个版别或许之后的,不然不支撑这么新的 JDK 版别。

假如你用的是 JDK19或许 JDK20的话,要在你的项目设置中将 language level设置为19或20的 Preview 等级,不然编译的时分会提示你无法运用预览版的功用,虚拟线程便是预览版的功用。

Java上进了,JDK21 要来了,并发编程再也不是噩梦了

假如你用的是 JDK21的话,将 language level 设置为 X -Experimental Features,别的,由于 JDK21不属于正式版别,所以需求到 IDEA 的设置中(留意是 IDEA 的设置,不是项目的设置了),将这个项目的 Target bytecode version手动修改为21,现在可选的最高便是20,也便是JDK20。设置为21之后,就能够运用 JDK21中的这些功用了。

Java上进了,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中呢,能够用结构化编程完结。

ShutdownOnSuccess捕获第一个成果并关闭使命范围以中止未完结的线程并唤醒调用线程。 适用于任意子使命的成果都能够直接运用,而且无需等候其他未完结使命的成果的状况。 它界说了获取第一个成果或在一切子使命失败时抛出反常的办法

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

履行多个使命,只需有一个失败(呈现反常或其他主动抛出反常状况),就中止其他未履行完的使命,运用scope.throwIfFailed捕捉并抛出反常。 假如一切使命均正常,则运用 Feture.get() 或*Feture.resultNow() 获取成果

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

Scoped Values 的比如

咱们必定都用过 ThreadLocal,它是线程本地变量,只需这个线程没毁掉,能够随时获取 ThredLocal 中的变量值。Scoped Values 也能够在线程内部随时获取变量,只不过它有个效果域的概念,超出效果域就会毁掉。

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

上面的比如模仿一个用户登录的进程,运用 ScopedValue.newInstance()声明晰一个 ScopedValue,用 ScopedValue.whereScopedValue设置值,而且运用 run 办法履行接下来要做的事儿,这样一来,ScopedValue就在 run() 的内部随时可获取了,在run办法中,模仿调用了一个service 的login办法,不必传递LoginUser这个参数,就能够直接经过LoginUser.get办法获取当时登录用户的值了。

也期望有收获的同学捧个场,转个发、点个在看,谢谢您嘞!咱们下期再见。 大众号「古时的风筝」,专注于后端技能,尤其是 Java 及周边生态。文章会收录在JavaNewBee中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里边。