3

使用Java虚拟线程时要避免的陷阱

 1 year ago
source link: https://www.jdon.com/66752.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

使用Java虚拟线程时要避免的陷阱

Java 虚拟线程是 JDK 19 提供的一项新功能。它有可能在减少内存消耗的基础上提高应用程序的可用性、吞吐量和代码质量。

在本文中,让我们了解从 Java 平台线程切换到虚拟线程时应避免的陷阱:

  1. 避免同步块/方法
  2. 避免线程池限制资源访问
  3. 减少ThreadLocal的使用

1.避免同步块/方法
 当一个方法在 Java 中被同步时,一次只允许一个线程进入该方法。让我们考虑以下示例:

 public synchronized void getData() {
   makeDBCall();
 }

在上面的代码片段中,'getData()'方法是'synchronized'。假设线程 1 尝试首先进入“ getData()”方法。当此线程 1 正在执行 getData() 方法时,线程 2 尝试执行此方法。由于“thread-1”当前正在执行“getData()”方法,因此“thread-2”将不允许执行。它将被置于 BLOCKED 状态。如果您在这种情况下使用虚拟线程,当线程移动到 BLOCKED 状态时,理想情况下它应该放弃其对底层 OS 线程的控制并移回堆内存。然而,由于当前虚拟线程实现的限制,当虚拟线程由于同步方法(或块)而被阻塞时,它不会放弃对底层操作系统线程的控制。因此,您不会获得切换到虚拟线程的好处。

在这种情况下,您应该考虑用“ ReentrantLock ”替换同步方法/块。可以使用“ReentrantLock”重写上面的示例代码同步的 getData()方法:

private ReentrantLock myLock = new ReentrantLock();

public void getData() {

 myLock.lock(); // acquire lock

   try {


      makeDBCall();

   } finally {


     myLock.unlock(); // release lock

   }

}

当你用 ReentrantLock 替换 synchronized 方法时,虚拟线程将放弃底层 OS 线程的控制,你可以享受虚拟线程的好处。

注意:虚拟线程在同步方法上工作时不释放底层操作系统线程,是 JDK 19 中的当前限制。它可以在未来的 Java 版本中解决。

2.避免线程池限制资源访问
有时,在我们的编程结构中,我们可能会使用线程池来限制对某些资源的访问。假设我们只想对后端系统进行 10 个并发调用,它可能已经使用线程池进行编程,如下所示:

 private ExecutorService BACKEND_THREAD_POOL = Executors.newFixedThreadPool(10);
 
   public <T> Future<T> queryBackend(Callable<T> query) {


  return BACKEND_THREAD_POOL.submit(query);

 }

在第 1 行中,您会注意到创建了一个包含 10 个线程的“BACKEND_THREAD_POOL” 。此线程池用于“queryBackend()”方法中以进行后端调用。该线程池将确保对后端系统的并发调用不超过 10 个。

在撰写本文时(2023 年 1 月),JDK 中没有可用的 API 来创建具有固定数量虚拟线程的执行器(即线程池)。下面是创建虚拟线程的所有 API 的列表。当您使用 Executor 时,您只能创建无限数量的虚拟线程。为了解决这个问题,您可以考虑将 Executor 替换为Semaphore。在上面的示例中,  可以使用“Semaphore”重写“queryBackend()”方法,如下所示:

 private static Semaphore BACKEND_SEMAPHORE = new Semaphore(10);

    public static <T> T queryBackend(Callable<T> query) throws Exception {

      BACKEND_SEMAPHORE.acquire(); // allow only 10 concurrent calls

        try {

           return query.call();

      } finally {

         BACKEND_SEMAPHORE.release();

      }

 }

如果您不熟悉信号量,您可以阅读这篇“ Java 信号量 - 简单介绍”帖子。如果您注意到第 1 行,我们正在创建一个具有 10 个许可的“BACKEND_SEMAPHORE”。

semaphore将只允许对后端系统进行 10 次并发调用。这是 Executor 的一个很好的替代品。 

3.减少ThreadLocal的使用
很少有应用程序倾向于使用ThreadLocal变量。如果你不熟悉ThreadLocal变量,你可以阅读这篇 "Java ThreadLocal--简单介绍 "的文章。但简而言之,Java ThreadLocal变量是在一个特定的线程范围内创建和存储的变量,它不能被其他线程访问。如果你的应用程序创建了数以百万计的虚拟线程,并且每个虚拟线程都有自己的ThreadLocal变量,那么它就会迅速消耗java堆的内存空间。因此,你要对存储为ThreadLocal变量的数据的大小保持谨慎。

你可能想知道为什么ThreadLocal变量在平台线程中没有问题。区别在于,在平台线程中,我们不会创建数以百万计的线程,而在虚拟线程中,我们会创建。数以百万计的线程,每个都有自己的ThreadLocal变量副本,可以迅速填满内存。俗话说,小水滴汇成大海。这句话在这里非常正确。

一般来说,Java ThreadLocal变量的管理和维护很棘手。它也可能导致讨厌的生产问题。因此,限制ThreadLocal变量的使用范围,可以使你的应用程序受益,特别是在使用虚拟线程时。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK