【Java 20】Scoped Value - Thread Local 之外的替代选择

本文介绍 Java 20 新增的孵化功能 Scoped Value。Scoped Value 的作用是在某些情况下作为 Thread Local 的替代。在同一线程上运行的不同代码可以通过 Scoped Value共享不可变的值。Scoped Value 主要是为了解决虚拟线程使用 Thread Local 时可能存在的一些问题。

Scoped Value是一个孵化功能。JDK 20 尚未发布。本文介绍的内容基于 2022 年 12 月 15 号发布的 JDK 20 Early-Access Build 28。

我们先从 Thread Local 说起。当不同代码运行在同一个线程上时,可以通过 Thread Local 来共享数据。Thread Local 变量一般声明为 static final,方便在代码中直接使用,并不需要通过方法调用时的参数来传递。一个典型的使用 Thread Local 的场景是表示当前认证用户信息的对象。与认证相关的代码负责设置该 Thread Local 对象的值,而其他代码则可以直接从 Thread Local 对象中获取值。

Thread Local 存在一些问题。首先是 Thread Local 变量是可变的,任何运行在当前线程中的代码都可以修改该变量的值,很容易产生一些难以调试的 bug。

其次是 Thread Local 变量的生命周期会很长。当使用 Thread Local 变量的 set 方法,为当前线程设置了值之后,这个值在线程的整个生命周期中都会保留,直到调用 remove 方法来删除。但是绝大部分开发人员不会主动调用 remove 来进行删除,这可能造成内存泄漏。

最后一个问题是 Thread Local 变量可以被继承。如果一个子线程从父线程中继承 Thread Local 变量,那么该子线程需要独立存储父线程中的全部 Thread Local 变量,这会产生比较大的内存开销。

虚拟线程同样可以使用 Thread Local,只不过所带来的问题不同。虚拟线程的特点是数量巨大,但是每个虚拟线程的生命周期较短,因此不容易产生内存泄漏问题。但是由于线程继承所带来的内存开销会更大。

由于 Thread Local 的这些问题,Java 20 引入了一种新的对象 Scoped Value,可以作为 Thread Local 之外的另外一种选择。Scoped Value 具备 Thread Local 的核心特征,也就是每个线程只有一个值。与 Thread Local 不同的是,Scoped Value 是不可变的,并且有确定的作用域,这也是名字中 scoped 的含义。

Scoped Value 对象用 jdk.incubator.concurrent 包中的ScopedValue 类来表示。使用 Scoped Value 的第一步是创建 ScopedValue 对象,通过静态方法 newInstance 来完成。ScopedValue 对象一般声明为 static final。

下一步是指定 ScopedValue 对象的值和作用域,通过静态方法 where 来完成。where 方法有 3 个参数:

  • 第一个参数是 ScopedValue 对象
  • 第二个参数是 ScopedValue 对象所绑定的值
  • 第三个参数是 RunnableCallable 对象,表示 ScopedValue 对象的作用域。

也就是说,在 RunnableCallable 对象执行过程中,其中的代码可以用 ScopedValue 对象的 get 方法获取到 where 方法调用时绑定的值。这个作用域是动态的,取决于 RunnableCallable 对象所调用的方法,以及这些方法所调用的其他方法。当 RunnableCallable 对象执行完成之后,ScopedValue 对象会失去绑定,不能再通过 get 方法获取值。在当前作用域中,ScopedValue 对象的值是不可变的,除非再次调用 where 方法绑定新的值。这个时候会创建一个嵌套的作用域,新的值仅在嵌套的作用域中有效。

下面的代码给出了 ScopedValue 对象的使用示例,VALUE 是一个 ScopedValue 对象。

import jdk.incubator.concurrent.ScopedValue;

public class WithScopedValue {
  private static final ScopedValue<String> VALUE = ScopedValue.newInstance();

  public void test(){
    ScopedValue.where(VALUE, "hello world", () -> {
      System.out.println(VALUE.get());
    });
  }
}

Scoped Value 的一个重要出发点是与虚拟线程一同使用,适用于 thread-per-request 的并发模式。在处理请求时,之前的代码可以指定 ScopedValue 的值,之后运行的代码则使用该值。

在下面的代码中,AuthService 负责处理用户认证,ScopedValue 对象 USER 表示当前认证用户,runWithUser 方法指定 USER 的值,并运行 Callable 对象。Callable 对象运行时的方法可以通过 USER 获取当前用户。

import java.util.UUID;
import java.util.concurrent.Callable;
import jdk.incubator.concurrent.ScopedValue;

public class AuthService {

  public static final ScopedValue<String> USER = ScopedValue.newInstance();

  public <V> V runWithUser(Callable<V> callable) throws Exception {
    return ScopedValue.where(USER, getCurrentUser(), callable);
  }

  private String getCurrentUser() {
    return "USER-" + UUID.randomUUID();
  }
}

当需要在不同的线程之间继承 Scoped Value 时,推荐的做法是使用结构化并发中 StructuredTaskScopeStructuredTaskScope 会创建一个作用域,刚好与 Scoped Value 的作用域相对应。下面的代码展示了 ScopedValueStructuredTaskScope 的用法。

ScopedValue.where(VALUE, "value", () -> {
  try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String>  user  = scope.fork(() -> findUser());          
    Future<Integer> order = scope.fork(() -> fetchOrder());        
    scope.join().throwIfFailed();
    return new Response(user.resultNow(), order.resultNow());
  }
});
版权所有 © 2024 灵动代码