Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=8dd3d94d-830f-4e04-b803-89b1f835b6db

Hilos virtuales en Java: la revolución del rendimiento en la plataforma Java

Imagen ornamental

A la hora de escalar una aplicación web, el principal cuello de botella suelen ser los hilos. Los hilos nativos del sistema operativo, para ser más concretos.

El número de hilos disponibles en el sistema es limitado y son costosos de crear y de gestionar, y al utilizarlos tienen que esperar por muchas cosas: que termine una llamada a una API o la obtención de datos desde una base de datos, o se encuentran bloqueados mientras esperan a que terminen otros hilos de los que dependen.

Para solucionar este problema se han intentado diversas cosas, siendo lo más popular el uso de extensiones o frameworks reactivos, como ReactiveX (la original y mítica creada, por cierto, por Microsoft) o Project Reactor, que forma la base de la parte reactiva de otros frameworks como Spring. Pero estas técnicas son complejas y además difíciles de mantener.

En Java 21 se introdujeron los hilos virtuales. Gracias a ellos, es posible ejecutar millones de hilos virtuales utilizando por debajo tan solo unos pocos hilos del sistema operativo.

Es más, ni siquiera es necesario modificar el código Java multihilo que ya tuviésemos. Todo lo que necesitamos es indicarle a nuestro código que debe utilizar hilos virtuales en lugar de hilos nativos.

Nota: en realidad los hilos virtuales se introdujeron como preview en Java 19. Eran lo que se conocía como Proyecto Loom. Estuvieron un año (dos versiones) en ese estado y, en septiembre de 2023, se lanzaron como definitivas junto con Java 21.

Hilos virtuales de Java en la práctica

Vamos a verlo con un ejemplo sencillo...

Para ello crearemos un pequeño programa que lanzará 1.000 tareas en paralelo. Cada una de ellas esperará sin hacer nada durante 1 segundo (simulando alguna tarea que tarde bastante, como una llamada a una API no muy rápida) y devolviendo un resultado, un número aleatorio en este caso (el ejemplo lo he sacado de aquí):

public class Task implements Callable<Integer> {

  private final int number;

  public Task(int number) {
    this.number = number;
  }

  @Override
  public Integer call() {
    System.out.printf(
        "Hilo %s - Tarea %d esperando...%n", Thread.currentThread().getName(), number);

    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      System.out.printf(
          "Hilo %s - Tarea %d cancelada.%n", Thread.currentThread().getName(), number);
      return -1;
    }

    System.out.printf(
        "Hilo %s - Tarea %d terminada.%n", Thread.currentThread().getName(), number);
    return ThreadLocalRandom.current().nextInt(100);
  }
}

Ahora medimos cuánto tiempo lleva ejecutar estas 1.000 tareas en un conjunto de 100 hilos del sistema operativo:

try (ExecutorService executor = Executors.newFixedThreadPool(100)) {
  List<Task> tasks = new ArrayList<>();
  for (int i = 0; i < 1_000; i++) {
    tasks.add(new Task(i));
  }

  long time = System.currentTimeMillis();

  List<Future<Integer>> futures = executor.invokeAll(tasks);

  long sum = 0;
  for (Future<Integer> future : futures) {
    sum += future.get();
  }

  time = System.currentTimeMillis() - time;

  System.out.println("sum = " + sum + "; time = " + time + " ms");
}

Como era de esperar, el programa tarda un poco más de 10 segundos: 1.000/100 = 10 tareas por hilo, o sea, cada hilo ejecuta 10 tareas en secuencia.

Vale. Ahora vamos a hacer lo mismo pero con hilos virtuales.

☕ ¿Quieres aprender Java en serio, y no con "recetas", sin entender nada en realidad? Pues tenemos el curso que necesitas 👉🏻 Desarrollo de aplicaciones con la plataforma Java.

Lo bonito es que solamente necesitamos hacer un cambio: en la primera línea de ejecución, en el try, sustituimos el ejecutor con los 100 hilos por un ejecutor virtual (newVirtualThreadPerTaskExecutor()), quedando así:

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    //Exactamente el mismo código
    ...
}

Este ejecutor crea un nuevo hilo virtual por cada tarea, por lo que se ejecutan todas en paralelo. El tiempo de ejecución es un poquito más de un segundo: el tiempo de espera de un solo hilo.

¿Y si cambiamos el número en el bucle para que sean 10.000 tareas en vez de tan solo 1.000? Apenas notaremos la diferencia: 1 segundo para lograrlo.

Y así podremos escalarlo hasta que las limitaciones de hardware entren en juego, que dependiendo de tu equipo será un número mayor o menor de hilos virtuales.

En cualquier caso las mejoras son exponenciales también: con el mismo hardware pero sin hilos virtuales el tiempo de ejecución aumenta y aumenta sin parar, en varios órdenes de magnitud. Con los hilos virtuales esto no ocurre, manteniendo siempre los tiempos de ejecución mucho más bajos.

Este solo es un modo de utilizarlos. También podemos crearlos sin un ejecutor, para usarlas de manera individual, utilizando los métodos Thread.startVirtualThread() o Thread.ofVirtual().start(), a los que pasaremos el código a ejecutar en forma de una función lambda (anónima):

Thread.startVirtualThread(() -> {
  //Código a ejecutar en el nuevo hilo virtual
});

Thread.ofVirtual().start(() -> {
  //Código a ejecutar en el nuevo hilo virtual
});

Podemos averiguar si nuestro código se está ejecutando dentro de un hilo virtual mediante el método Thread.currentThread().isVirtual(), que nos devolverá un booleano para indicarlo.

Ventajas de los hilos virtuales en Java

Los hilos virtuales ofrecen grandes ventajas y cambian el panorama de programación en Java, ya que:

  • Son muy ligeros y rápidos de crear (1 µs, un microsegundo, la millonésima parte de un segundo: en 1s podrías crear 1 millón)
  • Requieren mucha menos memoria: unas 1.000 veces menos que un hilo nativo (en pilas de llamada planas). 1 KB de los hilos virtuales frente a 1MB aproximadamente para cada hilo nativo.
  • Los bloqueos se efectúan en el espacio de usuario y no en el kernel del S.O., por lo que los cambios de contexto son muy rápidos.
  • Apenas es necesario cambiar el código respecto al uso de hilos nativos para poder ejecutarlas.

No están, ni de lejos, a la altura de la facilidad de las instrucciones async-await de C# o de JavaScript (sí), pero es uno de los grandes avances de la plataforma Java en los últimos años.

Puedes leer la documentación oficial sobre hilos virtuales en Java para conocer más detalles sobre su funcionamiento.

José Manuel Alarcón Fundador de campusMVP, es ingeniero industrial y especialista en consultoría de empresa. Ha escrito diversos libros, habiendo publicado hasta la fecha cientos de artículos sobre informática e ingeniería en publicaciones especializadas. Microsoft lo ha reconocido como MVP (Most Valuable Professional) en desarrollo web desde el año 2004 hasta la actualidad. Puedes seguirlo en Twitter en @jm_alarcon o leer sus blog técnico o personal. Ver todos los posts de José Manuel Alarcón
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.