Guía de Java Phaser

1. Información general

En este artículo, veremos la construcción Phaser del paquete java.util.concurrent . Es una construcción muy similar a CountDownLatch que nos permite coordinar la ejecución de subprocesos. En comparación con CountDownLatch , tiene algunas funciones adicionales.

El Phaser es una barrera en la que el número dinámico de subprocesos debe esperar antes de continuar con la ejecución. En CountDownLatch, ese número no se puede configurar dinámicamente y debe proporcionarse cuando estamos creando la instancia.

2. API de Phaser

El Phaser nos permite construir una lógica en la que los subprocesos deben esperar en la barrera antes de pasar al siguiente paso de ejecución .

Podemos coordinar múltiples fases de ejecución, reutilizando una instancia de Phaser para cada fase del programa. Cada fase puede tener un número diferente de subprocesos esperando avanzar a otra fase. Más adelante veremos un ejemplo de uso de fases.

Para participar en la coordinación, el hilo necesita registrarse () a sí mismo con la instancia de Phaser . Tenga en cuenta que esto solo aumenta el número de partes registradas, y no podemos verificar si el hilo actual está registrado; tendríamos que subclasificar la implementación para que admita esto.

El subproceso indica que llegó a la barrera llamando al método arrivalAndAwaitAdvance () , que es un método de bloqueo. Cuando el número de partidos llegados sea igual al número de partidos registrados, la ejecución del programa continuará y el número de fase aumentará. Podemos obtener el número de fase actual llamando al método getPhase () .

Cuando el hilo finaliza su trabajo, deberíamos llamar al método arrivalAndDeregister () para señalar que el hilo actual ya no debería ser contabilizado en esta fase en particular.

3. Implementación de lógica mediante la API de Phaser

Digamos que queremos coordinar múltiples fases de acciones. Tres subprocesos procesarán la primera fase y dos subprocesos procesarán la segunda fase.

Crearemos una clase LongRunningAction que implementa la interfaz Runnable :

class LongRunningAction implements Runnable { private String threadName; private Phaser ph; LongRunningAction(String threadName, Phaser ph) { this.threadName = threadName; this.ph = ph; ph.register(); } @Override public void run() { ph.arriveAndAwaitAdvance(); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } ph.arriveAndDeregister(); } }

Cuando se crea una instancia de nuestra clase de acción, nos registramos en la instancia de Phaser utilizando el método register () . Esto incrementará el número de subprocesos usando ese Phaser específico .

La llamada a arrivalAndAwaitAdvance () hará que el hilo actual espere en la barrera. Como ya se mencionó, cuando el número de partidos llegados sea igual al número de partidos registrados, la ejecución continuará.

Una vez finalizado el procesamiento, el subproceso actual se anulará el registro mediante una llamada al método ArrivalAndDeregister () .

Creemos un caso de prueba en el que iniciaremos tres subprocesos LongRunningAction y bloquearemos la barrera. A continuación, una vez finalizada la acción, crearemos dos subprocesos LongRunningAction adicionales que realizarán el procesamiento de la siguiente fase.

Al crear una instancia de Phaser desde el hilo principal, pasamos 1 como argumento. Esto es equivalente a llamar al método register () desde el hilo actual. Hacemos esto porque, cuando creamos tres subprocesos de trabajo, el subproceso principal es un coordinador y, por lo tanto, el Phaser necesita tener cuatro subprocesos registrados:

ExecutorService executorService = Executors.newCachedThreadPool(); Phaser ph = new Phaser(1); assertEquals(0, ph.getPhase());

La fase posterior a la inicialización es igual a cero.

La clase Phaser tiene un constructor en el que podemos pasarle una instancia principal. Es útil en casos en los que tenemos un gran número de partes que experimentarían costos masivos de contención de sincronización. En tales situaciones, se pueden configurar instancias de Phasers para que los grupos de sub-phasers compartan un padre común.

A continuación, comencemos tres subprocesos de acción LongRunningAction , que estarán esperando en la barrera hasta que llamemos al método arrivalAndAwaitAdvance () desde el subproceso principal.

Tenga en cuenta que hemos inicializado nuestro Phaser con 1 y hemos llamado a register () tres veces más. Ahora, tres subprocesos de acción han anunciado que han llegado a la barrera, por lo que se necesita una llamada más de arrivalAndAwaitAdvance () , la del subproceso principal:

executorService.submit(new LongRunningAction("thread-1", ph)); executorService.submit(new LongRunningAction("thread-2", ph)); executorService.submit(new LongRunningAction("thread-3", ph)); ph.arriveAndAwaitAdvance(); assertEquals(1, ph.getPhase());

Después de completar esa fase, el método getPhase () devolverá uno porque el programa terminó de procesar el primer paso de ejecución.

Digamos que dos subprocesos deben realizar la siguiente fase de procesamiento. Podemos aprovechar Phaser para lograr eso porque nos permite configurar dinámicamente la cantidad de subprocesos que deben esperar en la barrera. Estamos comenzando dos nuevos subprocesos, pero estos no procederán a ejecutarse hasta que se llame a arrivalAndAwaitAdvance () desde el subproceso principal (igual que en el caso anterior):

executorService.submit(new LongRunningAction("thread-4", ph)); executorService.submit(new LongRunningAction("thread-5", ph)); ph.arriveAndAwaitAdvance(); assertEquals(2, ph.getPhase()); ph.arriveAndDeregister();

Después de esto, el método getPhase () devolverá un número de fase igual a dos. Cuando queremos terminar nuestro programa, necesitamos llamar al método ArribayDeregister () ya que el hilo principal todavía está registrado en el Phaser. Cuando la cancelación del registro hace que el número de partidos registrados para convertirse en cero, la Phaser está terminada. Todas las llamadas a los métodos de sincronización ya no se bloquearán y volverán inmediatamente.

La ejecución del programa producirá la siguiente salida (el código fuente completo con las declaraciones de la línea de impresión se puede encontrar en el depósito de código):

This is phase 0 This is phase 0 This is phase 0 Thread thread-2 before long running action Thread thread-1 before long running action Thread thread-3 before long running action This is phase 1 This is phase 1 Thread thread-4 before long running action Thread thread-5 before long running action

Vemos que todos los hilos están esperando su ejecución hasta que se abre la barrera. La siguiente fase de la ejecución se realiza solo cuando la anterior finalizó con éxito.

4. Conclusión

En este tutorial, echamos un vistazo a la construcción Phaser de java.util.concurrent e implementamos la lógica de coordinación con múltiples fases usando la clase Phaser .

La implementación de todos estos ejemplos y fragmentos de código se puede encontrar en el proyecto GitHub; este es un proyecto de Maven, por lo que debería ser fácil de importar y ejecutar tal como está.