是否可以创建一个ThreadLocal作为Java虚拟线程的承载线程?

Mar*_*son 15 java caching thread-local virtual-threads

JEP-425:虚拟线程指出“应该为每个应用程序任务创建一个新的虚拟线程”,并两次提到在 JVM 中运行“数百万”虚拟线程的可能性。

相同的 JEP 意味着每个虚拟线程都可以访问其自己的线程本地值:

虚拟线程就像平台线程一样支持线程局部变量,因此它们可以运行使用线程局部变量的现有代码。

线程局部变量多次用于缓存非线程安全且创建成本昂贵的对象。JEP 警告:

但是,由于虚拟线程可能非常多,因此请在仔细考虑后使用线程局部变量。

确实很多!特别是考虑到虚拟线程不被池化(或者至少不应该被池化)。作为短期任务的代表,在虚拟线程中使用线程局部变量来缓存昂贵的对象似乎毫无意义。除非!我们可以从虚拟线程创建并访问绑定到其载体线程的线程局部变量

为了澄清起见,我想从这样的事情开始(当仅使用上限为池大小的本机线程时,这是完全可以接受的,但是当连续运行数百万个虚拟线程时,这显然不再是一个非常有效的缓存机制重新创建:

static final ThreadLocal<DateFormat> CACHED = ThreadLocal.withInitial(DateFormat::getInstance);
Run Code Online (Sandbox Code Playgroud)

为此(可惜这个类不是公共 API 的一部分):

static final ThreadLocal<DateFormat> CACHED = new jdk.internal.misc.CarrierThreadLocal();
// CACHED.set(...)
Run Code Online (Sandbox Code Playgroud)

在我们到达那里之前。人们必须问,这是一种安全的做法吗?

好吧,据我正确理解虚拟线程,它们只是在平台线程(又名“载体线程”)上执行的逻辑阶段,能够卸载而不是被阻塞等待。所以我假设 - 如果我错了,请纠正我 - 1)虚拟线程永远不会被同一载体线程上的另一个虚拟线程交错或重新安排在另一个载体线程上,除非代码否则会阻塞,因此,如果 2 )我们对缓存对象调用的操作永远不会阻塞,那么任务/虚拟线程将简单地在同一载体上从头到尾运行,所以是的,将对象缓存在平台线程本地上是安全的。

冒着回答我自己的问题的风险,JEP-425 表明这是不可能的:

载体的线程局部变量对于虚拟线程不可用,反之亦然。

我找不到公共 API 来获取载体线程或在平台线程上显式分配线程局部变量(从虚拟线程),但这并不是说我的研究是确定的。也许有办法吗?

然后我读了JEP-429: Scoped Values,乍一看似乎是 Java 之神试图完全摆脱它ThreadLocal,或者至少为虚拟线程提供替代方案。事实上,JEP 使用了诸如“迁移到作用域值”之类的措辞,并表示它们“优于线程局部变量,尤其是在使用大量虚拟线程时”。

对于 JEP 中讨论的所有用例,我只能同意。但在本文档的底部,我们还发现了这一点:

有一些场景有利于线程局部变量。一个示例是缓存创建和使用成本高昂的对象,例如 java.text.DateFormat 的实例。众所周知,DateFormat 对象是可变的,因此如果没有同步,就无法在线程之间共享它。通过在线程生命周期内持续存在的线程局部变量为每个线程提供自己的 DateFormat 对象通常是一种实用的方法。

根据前面讨论的内容,使用线程本地可能是“实用的”,但不是很理想。事实上,JEP-429 本身实际上是从一个非常有说服力的评论开始的:“如果一百万个虚拟线程中的每一个都有可变的线程局部变量,那么内存占用可能会很大”。

总结一下:

您是否找到了从虚拟线程在载体线程上分配线程局部变量的方法?

如果不是,那么可以肯定地说,对于使用虚拟线程的应用程序,在线程本地缓存对象的做法已经死亡,并且必须实现/使用不同的方法,例如并发缓存/映射/池/其他方法?

Hol*_*ger 15

你写了

\n
\n

所以我假设 - 如果我错了,请纠正我 - 那

\n
    \n
  1. 虚拟线程永远不会被同一载体线程上的另一个虚拟线程交错,或者在另一个载体线程上重新调度,除非代码否则会阻塞,因此,如果
  2. \n
  3. 我们对缓存对象调用的操作永远不会阻塞,然后任务/虚拟线程将简单地在同一载体上从头到尾运行,所以是的,将对象缓存在平台线程本地上是安全的。
  4. \n
\n
\n

Loom 的 State of Loom文件指出:

\n
\n

您不得对今天\xe2\x80\x99s 线程的调度点位置做出任何假设。即使没有强制抢占,您调用的任何 JDK 或库方法都可能会引入阻塞,从而导致任务切换点。

\n
\n

并进一步

\n
\n

为此,我们计划让虚拟机支持尝试在任何安全点强制抢占执行的操作。该功能将如何向调度程序公开尚待确定,并且可能不会出现在第一个预览版中。

\n
\n

所以

\n
    \n
  1. 虚拟线程仅在即将被阻塞时释放载体线程的假设仅适用于当前预览。虚拟线程之间的抢占式切换是允许的,甚至是未来计划的。

    \n
  2. \n
  3. 即使我们假设虚拟线程在执行阻塞操作时只能释放载体线程,我们也无法预测何时可能发生阻塞操作。

    \n
      \n
    • 我们无法控制的操作的一个例子是类加载。加载类数据是一个阻塞操作,并且对于常见的 JVM 来说,类加载是延迟实现的。甚至有可能,一个被多次调用的方法突然执行一个不常见的路径,该路径使用了一个以前没有使用过的类。

      \n
    • \n
    • 另一个例子是资源加载。即使是像您这样简单的示例也DateFormat已经涉及以未指定方式组织的资源,例如时区数据或本地化的月份和工作日名称。

      \n
    • \n
    \n
  4. \n
\n

因此,\xe2\x80\x99s 无法拥有安全工作的运营商本地缓存,并且您认为使用线程局部变量(或类似的)进行缓存的假设确实是正确的。您可以改用对象池,但由于这意味着某种同步,您不妨考虑仅使用单个DateFormat\xc2\xb9 并对其进行同步。这将实现您最初的想法,即在使用对象期间不释放载体线程。

\n

当然,在这个特定的示例中,更好的选择是使用线程安全的DateTimeFormatterAPI java.time,因此允许所有线程共享单个实例。

\n

\xc2\xb9 或多个之一,以不涉及同步的方式选择

\n