0% encontró este documento útil (0 votos)
4 vistas103 páginas

Java Concurrency

La programación concurrente en Java permite la ejecución paralela de múltiples hilos para optimizar el uso de recursos y tiempo. Java proporciona clases e interfaces, como Thread y Runnable, para facilitar la implementación de esta programación, así como el paquete java.util.concurrent a partir de JDK 1.5 para mejorar su manejo. Además, se introducen interfaces como Executor y Callable para gestionar tareas concurrentes de manera más eficiente.

Cargado por

mrcoar
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
4 vistas103 páginas

Java Concurrency

La programación concurrente en Java permite la ejecución paralela de múltiples hilos para optimizar el uso de recursos y tiempo. Java proporciona clases e interfaces, como Thread y Runnable, para facilitar la implementación de esta programación, así como el paquete java.util.concurrent a partir de JDK 1.5 para mejorar su manejo. Además, se introducen interfaces como Executor y Callable para gestionar tareas concurrentes de manera más eficiente.

Cargado por

mrcoar
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Lenguaje de programación JAVA: Programación

Concurrente

Marco Araneda Soto


Qué es la programación concurrente
 La programación concurrente se define como la
codificación de una o varias simples línea de código (en
adelante SLOC) que será(n) ejecutada(s) en paralelo al
menos dos veces.
 Este tipo de programación tiene la ventaja de
optimización de tiempo en manejo de recursos como
registros de bases de datos, archivos, etc.
Qué NO es la programación concurrente
 La programación concurrente NO es un paradigma de programación, sino
que es una práctica transversal entre todos los paradigmas existentes.
 De hecho, los fundamentos de la programación concurrente existen desde tiempos
en los que el paradigma predominante de programación era el paradigma
estructurado.
 La programación concurrente NO es un patrón de diseño, sino que es una
práctica transversal entre todos los patrones de diseño existentes.
 De hecho, los fundamentos de la programación concurrente existen desde tiempos
en los que los programas se desarrollaban sin patrones de diseño.
 NO es la solución al problema de reserva de memoria.
 Tener múltiples procesos en paralelo no es sinónimo de optimización de uso de
memoria, sino, en el mejor caso, optimización de tiempo de ejecución.
Programación concurrente en JAVA
 A través de la especificación JSE, JAVA proporciona
clases e interfaces para definición y ejecución de
múltiples hebras o hilos de ejecución en paralelo (cada
hebra representa un proceso hijo creado a partir del
proceso principal) y para la ejecución de uno o varios
SLOC en paralelo.
 Al ser parte de la especificación JSE, no se requiere de
librerías externas adicionales para aplicar programación
concurrente a una aplicación.
Manejo de múltiples hebras
 Java proporciona, en todas sus versiones, la clase Thread, en el paquete
[Link], para poder definir una simple hebra.
 Para crear una simple hebra utilizando solamente la clase Thread, se debe
hacer lo siguiente:
 Definir una clase que sea extensión de Thread.
 Sobrescribir en esa clase el método public void run()
 Desde cualquier clase donde se desee crear la hebra, se debe crear una
instancia de la clase creada y luego llamar al método public void start de esa
instancia.
 El método start automáticamente llama al método run.
 Que esa clase tenga o no tenga constructores, atributos o métodos adicionales
depende de las necesidades del programador.
Ejemplo clase Thread
¿Qué puede tener una hebra en Java?
 Una hebra puede tener:
 Un nombre, ya sea asignado por el programador por medio de uno de los
constructores de Thread o en su defecto uno generado por la JVM (véase la imagen).
 Una ID designada por la JVM, equivalente a los PID (identificadores de proceso)
manejados por el sistema operativo.
 Una bandera booleana para indicar si la hebra fue interrumpida.
 Una prioridad de ejecución (véase más adelante)
 Una bandera booleana para indicar si la hebra corresponde a un daemon (proceso
ejecutado en segundo plano) o a un proceso de usuario.
 Se puede obtener el nombre, la ID y la prioridad de la hebra en formato
String con el método public String toString()
Operaciones con objetos Thread
 Java permite las siguientes operaciones a una simple hebra definida con la clase Thread o con
cualquiera de sus subclases:
 Chequear si la hebra actual tiene acceso a la hebra definida con el método public void checkAccess(). En
caso de no tener acceso, se lanzará un SecurityException.
 Interrumpir la hebra con el método public void interrupt(). Este método internamente llamará a
checkAccess() antes de interrumpir la hebra. Además, al interrumpir la hebra, si esa hebra estaba en
pausa debido al método wait (proporcionado por Object) o pausaba la aplicación con el método join (véase
más adelante), se lanzará un InterruptedException desde esa hebra.
 Obligar a la aplicación, desde esa hebra, a pausar su ejecución con el método join. Este método tiene dos
sobrecargas:
 public void join(long millis): Pone en pausa la aplicación hasta que la hebra termine su ejecución o hasta que ésta haya
sido interrumpida o hasta haber transcurrido la cantidad de milisegundos especificada, lo que venga primero. Si la
cantidad especificada es cero (0), la aplicación puede esperar hasta que la hebra termine, sin importar cuánto se
demore, o hasta que sea interrumpida.
 Public void join(long millis, long nanos): Igual que el anterior, pero especificando también los nanosegundos adicionales
a esperar.
 public void join(): Equivale a llamar al método join pasando como parámetro un cero.
Operaciones con objetos Thread
 Java permite las siguientes operaciones a una simple hebra definida con la clase Thread o
con cualquiera de sus subclases:
 Obtener o establecer una prioridad a la hebra (con respecto a cualquier otra hebra definida) con
los métodos public int getPriority() y public void setPriority(int priority).
 El valor retornado por getPriority y el argumento de setPriority puede y debe ser el de cualquiera de las
siguientes constantes definidas en la clase Thread. Cualquier valor distinto de esos pasado como parámetro
a setPriority provocará que se lance un IllegalArgumentException:
 MIN_PRIORITY: Prioridad baja
 NORM_PRIORITY: Prioridad normal. Es la prioridad por defecto
 MAX_PRIORITY: Prioridad alta.
 Para el caso de setPriority, este llamará a checkAccess antes de intentar establecer la nueva prioridad.
 Chequear si la hebra fue interrumpida con el método public boolean isInterrupted()
 Chequear o establecer que la hebra corresponda a un proceso daemon o uno de usuario con los
métodos public boolean isDaemon() y public void setDaemon(boolean flag).
 Chequear que la hebra permanezca en ejecución con el método public boolean isAlive().
Operaciones con objetos Thread
 Java permite las siguientes operaciones a una simple hebra definida con la
clase Thread o con cualquiera de sus subclases:
 Obtener la traza de errores de una o todas las hebras:
 Para una hebra, el método es public StackTraceElement[] getStackTrace()
 Para todas las hebras, el método es public static Map<Thread, StackTraceElement[]>
getAllStackTraces.
 En cada caso, para una hebra, si esa hebra no se ha iniciado o si ha terminado con
éxito, se obtendrá un arreglo vacío.
 Mostrar en el error estándar la traza de errores de una hebra con el método
public void dumpStack()
 Obtener o establecer el nombre de la hebra con los métodos public String
getName() y public void setName(String name)
Operaciones adicionales con Thread
 Además de todo lo anterior, la clase Thread también
permite pausar la aplicación completa una cantidad de
milisegundos y/o nanosegundos con los métodos
estáticos public static void sleep(long millis) y public
static void sleep(long millis, long nanos).
La interfaz Runnable
 La gran limitación al utilizar únicamente la clase Thread para definir
múltiples hebras es la impuesta por las limitantes de las herencias de
clases.
 La interfaz Runnable es la solución a la limitación descrita previamente.
 Esta interfaz tiene un método llamado public void run(), el cual debe
definir qué es lo que la hebra debe ejecutar.
 A pesar de que Runnable existió desde la primera versión de Java, por
el solo hecho de tener un único método abstracto, el método run, se
considera una interfaz funcional, por lo que, a partir de Java 8, se
pueden crear objetos Runnable mediante expresiones lambda.
Cómo definir una hebra con Runnable
 Se deben seguir los siguientes pasos:
 Crear un objeto Runnable. La forma de hacerlo depende de la versión del JDK que
se está utilizando:
 Para todas las versiones, crear una clase que implemente la interfaz Runnable, o bien
instanciar un Runnable definiendo inmediatamente el comportamiento del método run.
 Para Java 8 en adelante, utilizar expresiones lambda para generar un objeto Runnable.
 Crear un objeto Thread utilizando uno de los siguientes constructores. Para ambos
casos, el valor de runObj debe ser el del objeto creado en el paso anterior:
 public Thread(Runnable runObj) para asignar un nombre por defecto
 public Thread(Runnable runObj, String name) para asignar un nombre arbitrario
 Llamar al método start del objeto Thread creado:
 El método start, en vez de llamar al método run del propio objeto Thread, llama al método
run del objeto Runnable.
Ejemplo Runnable
Manejo de concurrencia (JDK 1.5 en adelante)
 A partir de JDK 1.5, Java proporciona el paquete
[Link] para implementar de mejor manera
la programación concurrente y evitar limitar al
programador a usar solamente Thread y Runnable.
 El paquete contiene muchas clases e interfaces, por lo
que solo se verán aquellas que son fundamentales.
La interfaz Executor
 Esta interfaz permite ejecutar objetos Runnable sometidos.
 La ventaja de usar Executor es que permite desacoplar la ejecución del objeto
Runnable del real mecanismo de ejecución del mismo. Esto, debido a que Executor
permite ejecutar el objeto Runnable en la hebra actual (llamando a su método run) o
en una nueva hebra (creando un objeto Thread a partir del objeto Runnable),
ejecutarlo dos o más veces en paralelo, o simplemente no ejecutarlo en lo absoluto,
dependiendo de las necesidades del programador.
 Executor tiene un único método (razón por la cual se pueden generar objetos
Executor mediante expresiones lambda a partir de Java 8) llamado public void
execute(Runnable r) para ejecutar el objeto Runnable.
 El argumento no puede ser nulo, o se lanzará un NullPointerException
 Si el argumento no es aceptado para ejecución, se lanzará un RejectedExecutionException.
Ejemplo de Executor
La interfaz ExecutorService
 Un simple objeto Executor no puede ser apagado ni tampoco sirve, por sí solo, para ejecutar un simple
Runnable en dos o más hebras.

 Java proporciona una extensión de Executor llamada ExecutorService, que permite, además de ejecutar
un objeto Runnable del mismo modo que Executor, más cosas tales como:
 Esperar que todas las tareas asociadas a un ExecutorService terminen, o la hebra actual haya sido interrumpida o la cantidad
de tiempo (time) en la unidad especificada por unit haya transcurrido (lo que venga primero) con el método public boolean
awaitTermination(long time, TimeUnit unit).
 TimeUnit es una enumeración que tiene los siguientes valores posibles: DAYS (días), HOURS (horas), MICROSECONDS
(microsegundos), MILISECONDS (milisegundos), MINUTES (minutos), NANOSECONDS (nanosegundos) y SECONDS (segundos)

 Chequear si el ejecutor terminó su ejecución con el método public boolean isShutdown()

 Chequear si todas las tareas asociadas fueron completadas antes de detener el ejecutor con el método public boolean
isTerminated()

 Parar la ejecución del ejecutor. Esto es posible de una de dos formas posibles:
 public void shutdown() para un apagado ordenado, esperando que todas las tareas asociadas se apaguen y no aceptando más tareas
que ejecutar
 public List<Runnable> shutdownNow() para interrumpir todas las tareas en espera o en ejecución antes de detener el ejecutor. El
valor de retorno es una lista de todos los objetos Runnable asociados que se encontraban en espera al momento de llamar a este
método.

 No se recomienda implementar esta interfaz uno mismo. Java proporciona métodos que devuelven
implementaciones de esta interfaz. La forma de crear objetos ExecutorService y ejemplos de su uso se
verán más adelante.
La interfaz ScheduledExecutorService
 Al usar Executor y/o ExecutorService, se asume que las tareas a ejecutar no tienen una demora artificial y no
existe periodicidad en las tareas a ejecutar (es decir, se ejecuta una única vez y no se vuelve a ejecutar cada
cierto tiempo).

 Java proporciona una extensión de ExecutorService llamada ScheduledExecutorService que permite, en adición
a todo lo que se puede hacer con Executor y ExecutorService, más cosas tales como:
 Ejecutar una tarea una única vez después de un tiempo de latencia en una unidad de tiempo especificada con public
ScheduledFuture<?> schedule(Runnable task, long time, TimeUnit unit). Llamar al método get del objeto retornado (véase más
adelante) devolverá null.

 Con public ScheduledFuture<V> schedule(Callable<V> call, long time, TimeUnit unit) se hace lo mismo que el método anterior,
con la diferencia de que el resultado de procesar el objeto call es un objeto que es instancia de V que debe ser devuelto al
llamar al método get del método retornado. Callable se verá en la siguiente diapositiva.

 Ejecutar una tarea periódicamente cada cierto tiempo (period), luego de un tiempo de latencia inicial (delay), con ambos tiempos
expresados en la misma unidad de tiempo, con public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long delay,
long period, TimeUnit unit)

 Con public ScheduledFuture<?> scheduleWithFixedDelay(RunnableTask, long delay, long period, TimeUnit unit) se logra lo
mismo que el método anterior, con la diferencia de que con este método, se espera a que la ejecución anterior de la tarea
termine para aplicar la latencia especificada en period y posteriormente volver a ejecutar la tarea.

 Los ejemplos de uso de ScheduledExecutorService se verán más adelante


La interfaz Callable<V>
 La interfaz Callable<V> es similar en funcionalidad a la interfaz Runnable, con
la diferencia de que con Runnable, se asume que la tarea ejecutada desde el
método run no devuelve (directamente) un resultado y no existe una excepción
posible a manejar.
 Pero sí se puede devolver un resultado si éste fue declarado como atributo en la
clase que implementa la interfaz Runnable, modificado dentro del método run y
obtenible directamente o por medio de un método.
 Pero Callable<V> proporciona un único método llamado call que debe devolver
un objeto que es instancia de V (V es una clase cualquiera) o lanzar una
excepción en caso de error.
 En consecuencia, a partir de Java 8, se pueden crear objetos Callable usando
expresiones lambda.
Ejemplo de ScheduledExecutorService con
Callable y Future
Las interfaces Future, Delayed y
ScheduledFuture
 Los objetos devueltos por todos los métodos de la interfaz
ScheduledExecutorService son instancias de la interfaz ScheduledFuture<?>.
 Esta interfaz no tiene métodos propios. Sin embargo, heredó directamente los
métodos de las interfaces Delayed y Future<V> e, indirectamente, de la interfaz
Comparable<Delayed>
 Delayed es utilizada para establecer latencia en la ejecución de una tarea. Objetos Delayed
se pueden generar con expresiones lambda a partir de Java 8.
 Future<V> es utilizada para representar el resultado de ejecuciones asíncronas de una
tarea. V representa una clase o interfaz cualquiera de la cual debe ser instancia el
resultado de la ejecución. En algunos casos, la tarea asociada a un objeto Future solo se
ejecutará cuando se intente obtener el resultado.
 ScheduledFuture heredó de Delayed el método public long getDelay(TimeUnit unit)
que devuelve el tiempo de latencia asociado en la unidad de tiempo especificada.
Las interfaces Future, Delayed y
ScheduledFuture
 La interfaz Future<V> proporciona los siguientes métodos:
 public boolean cancel(boolean interrumpir) para intentar cancelar la ejecución de la tarea asociada. El
argumento sirve para indicar si la hebra asociada a la tarea debe interrumpirse inmediatamente (valor true)
o se debe esperar a que se termine. El método devuelve false si la tarea no pudo cancelarse (ya sea
porque fue completada o previamente cancelada) o true en caso contrario.
 public boolean isCanceled(): devuelve true si la tarea asociada fue cancelada exitosamente.
 public boolean isDone(): devuelve true si la tarea asociada fue completada, cancelada o hubo una
excepción.
 public V get(): ejecuta la tarea, espera, de ser necesario, que se complete la tarea y devuelve el resultado.
 public V get(long time, TimeUnit unit): Ejecuta la tarea, espera que se complete la tarea o transcurra el
tiempo expresado en la unidad de tiempo especificada. Luego devuelve el resultado si es que está
disponible.
 Para los dos métodos anteriores, se puede lanzar una de las siguientes excepciones:
 CancellationException: Si la tarea fue cancelada
 ExecutionException: Si la tarea lanzó una excepción
 InterruptedException: Si la tarea fue interrumpida
 TimeoutException (sólo el segundo método) si expiró el tiempo especificado en los argumentos.
La interfaz ExecutorService (segunda parte)
 Ahora que vimos la funcionalidad de las interfaces Callable y Future, vemos otros
métodos de ExecutorService que son utilizados en ejecuciones asíncronas de tareas sin
latencia ni periodicidad:
 El método public List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) llama a
todos los métodos call de la lista de Callable pasada como parámetro. Cada resultado es
guardado en una lista de Future que es devuelta cuando todas las tareas hayan sido completadas.
 El método public List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long
timeout, TimeUnit unit) se comporta de igual manera que el método anterior con la diferencia de
que la lista es retornada cuando todas las tareas se hayan completado o cuando haya expirado el
tiempo en la unidad especificada, lo que venga primero. En caso de un timeout, las tareas que
faltaban por completarse se marcarán como incompletas. Este método y el anterior pueden lanzar
un InterruptedException si la operación fue interrumpida, un NullPointerException si la lista o al
menos uno de sus elementos es nulo, o un RejectedExecutionException si al menos una de las
tareas no puede ser ejecutada.
La interfaz ExecutorService (segunda parte)
 Ahora que vimos la funcionalidad de las interfaces Callable y Future, vemos otros métodos de
ExecutorService que son utilizados en ejecuciones asíncronas de tareas sin latencia ni
periodicidad:
 El método public T invokeAny(Collection<? extends Callable<T>> tasks) invoca todas las tareas pasadas
como parámetro, pero solo devuelve el valor devuelto por el método call del objeto Callable
correspondiente a la primera tarea que terminó exitosamente.
 El método public T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) se
comporta de igual manera que el método anterior, con la diferencia de que este método espera a la
primera tarea que se complete o a que haya expirado el tiempo de espera, lo que venga primero. Este
método y el anterior pueden lanzar una de las siguientes excepciones:
 NullPointerException si la lista o al menos uno de sus elementos es nulo
 IllegalArgumentException (solo para la sobrecarga del método que no considera timeout) si la lista está vacía
 TimeoutException (solo para la sobrecarga del método que considera timeout) si el tiempo de espera especificado
expiró.
 InterruptedException si la operación de este método fue interrumpida, en cuyo caso todas las tareas faltantes se marcan
como canceladas.
 ExecutionException si ninguna tarea terminó exitosamente.
 RejectedExecutionException si alguna tarea no fue aceptada para su ejecución.
La interfaz ExecutorService (segunda parte)
 Ahora que vimos la funcionalidad de las interfaces Callable y Future, vemos
otros métodos de ExecutorService que son utilizados en ejecuciones
asíncronas de tareas sin latencia ni periodicidad:
 El método public Future<T> submit(Callable<T> task) ejecuta de manera asíncrona
la tarea (a diferencia de execute, que no lo hace así). Al llamar al método get del
objeto devuelto, en caso de éxito, debe devolver el valor devuelto por el método call
del parámetro.
 El método public Future<?> submit(Runnable task, T result) se comporta del igual
manera que el anterior, con la diferencia de que, como el método run del primer
parámetro no devuelve valores, al llamar al método get del objeto devuelto, se debe
devolver el valor del segundo parámetro.
 El método public Future<T> submit(Runnable task) equivale a llamar al método
anterior con el segundo parámetro igual a null.
Ejemplos de ExecutorService con Future
La interfaz ThreadFactory
 Esta interfaz permite crear instancias de Thread a partir de
objetos Runnable de manera personalizada, en vez de simple
y solamente crear el objeto Runnable y pasarlo como
parámetro al constructor de Thread.
 Esta interfaz tiene un único método llamado public Thread
newThread(Runnable r) para tal fin.
 Esta interfaz, debido a lo anterior, se considera interfaz
funcional a partir de Java 8, por lo que se pueden crear
objetos ThreadFactory a partir de expresiones lambda.
La clase Executors
 Tal como se mencionó anteriormente, no se recomienda crear uno
mismo clases que implementen interfaces como ExecutorService,
ScheduledExecutorService, ScheduledFuture y Future, a menos
que el programador sepa exactamente lo que quiere y debe hacer.
 Para eso, Java proporciona la clase Executors, que contiene
métodos públicos y estáticos que crean internamente objetos que
implementan alguna de estas interfaces para uso futuro.
 A través de Executors, el programador puede determinar cuántas
veces simultáneas se debe ejecutar una tarea.
Executors con Callable
 Executors permite obtener objetos Callable a partir de los siguientes métodos:
 public static Callable<T> callable(Runnable task, T result): permite crear un objeto
Callable a partir de la tarea especificada. El método call del objeto devuelto, al ser
llamado, retornará el valor del segundo parámetro.
 public static Callable<Object> callable(Runnable task): Equivale a llamar al método
anterior con el segundo parámetro igual a null.
 public static Callable<Object> callable(PrivilegedAction<?> action) y public static
Callable<Object> callable(PrivilegedExceptionAction<?> action): permite crear un
objeto Callable a partir de una acción privilegiada especificada. El método call del
objeto devuelto, al ser llamado, retornará el valor devuelto por el método run del
parámetro. La diferencia entre ambos métodos es que el segundo cubre el hecho de
que en el transcurso de la acción se puede lanzar una excepción. Véase el Anexo 1
para más detalles.
Executors con Callable
 Executors permite obtener objetos Callable a partir de los
siguientes métodos:
 public static Callable<T> privilegedCallable(Callable<T> c) y
public static Callable<T>
privilegedCallableUsingCurrentClassLoader(Callable<T> c):
ambos métodos permiten crear un objeto Callable a partir de
otro, de manera tal que solo pueda ser utilizado bajo el actual
contexto de control de acceso. El segundo de estos métodos
lanzará un AccessControlException si dicho contexto no tiene
permisos para leer o modificar el class loader actual.
Executors con ThreadFactory
 Executors permite obtener una fábrica de hebras por
defecto (es decir, que solamente se dedique a crear los
objetos Thread a partir de un Runnable sin alguna
acción adicional) con el método public static
ThreadFactory defaultThreadFactory().
 También permite obtener una fábrica con los mismos
permisos que la hebra actual con el método public static
ThreadFactory privilegedThreadFactory().
Executors con ExecutorService
 La clase Executors permite obtener objetos ExecutorService
con cualquiera de los siguientes métodos:
 public static ExecutorService
newCachedThreadPool(ThreadFactory factory): El objeto devuelto
permite crear nuevas hebras cuando sea necesario, utilizando el
parámetro para crear nuevas hebras, y reutilizar hebras existentes
cuando estén disponibles.
 public static ExecutorService newCachedThreadPool(): Llamar a
este método es equivalente a llamar al anterior pasando como
parámetro el objeto devuelto por [Link]()
Executors con ExecutorService
 La clase Executors permite obtener objetos ExecutorService con cualquiera de los
siguientes métodos:
 public static ExecutorService newFixedThreadPool(int n, ThreadFactory factory): El objeto
devuelto permite crear nuevas hebras cuando sea necesario, permitiendo ejecutar
simultáneamente hasta un máximo de n hebras, utilizando el segundo parámetro para
crear nuevas hebras. Cualquier hebra que se intente ejecutar habiendo alcanzado n hebras
en ejecución permanecerá en espera hasta que una de esas n hebras termine su ejecución.
 public static ExecutorService newFixedThreadPool(int n): Llamar a este método es
equivalente a llamar al anterior pasando como segundo parámetro el objeto devuelto por
[Link]()
 public static ExecutorService newSingleThreadExecutor(ThreadFactory factory): Llamar a
este método es equivalente a llamar a [Link](1, factory).
 public static ExecutorService newSingleThreadExecutor(): Llamar a este método es
equivalente a llamar a [Link](1)
Executors con ExecutorService
 La clase Executors permite obtener objetos ExecutorService con
cualquiera de los siguientes métodos:
 public static ExecutorService newWorkStealingPool(int n): El objeto devuelto
permite un pool de hebras usando n procesadores, donde n representa la
cantidad máxima de hebras que se pueden ejecutar en paralelo. Se le llama
“work-stealing” porque si un procesador se queda sin hebras que ejecutar, le
quita una hebra a otro procesador que aun tenga hebras que ejecutar (para
más detalles, visitar [Link]
 public static ExecutorService newWorkStealingPool(): Llamar a este método es
equivalente a llamar al método anterior pasando como parámetro el valor
devuelto por [Link]().availableProcessors(), es decir, el
número de procesadores disponibles durante la ejecución actual de la
aplicación.
Executors con ExecutorService
 La clase Executors permite obtener objetos
ExecutorService con cualquiera de los siguientes
métodos:
 public static ExecutorService
unconfigurableExecutorService(ExecutorService e): Permite
crear un objeto ExecutorService a partir de otro para
delegarle todos sus métodos definidos, pero no métodos que
pudieran ser llamados haciéndole un casting al objeto.
Executors con ScheduledExecutorService
 Executors permite obtener objetos que implementan la interfaz
ScheduledExecutorService por medio de: public static
ScheduledExecutorService newScheduledThreadPool(int n, ThreadFactory
factory), public static ScheduledExecutorService newScheduledThreadPool(int
n), public static ScheduledExecutorService
newSingleThreadScheduledExecutor(ThreadFactory factory), public static
ScheduledExecutorService newSingleThreadScheduledExecutor() y public
static ScheduledExecutorService
unconfigurableScheduledExecutorService(ScheduledExecutorService e).
Estos métodos son equivalentes a [Link](n, factory),
[Link](n),
[Link](factory),
[Link]() y
[Link](e), respectivamente, para el caso de
ejecutores sincronizados y/o con latencia.
Sincronización
 Hasta este punto, se tiene asumido que cada proceso trabaja con recursos (variables, objetos, archivos,
bases de datos, etc.) diferentes entre sí.

 Pero la realidad es que existe la posibilidad de que al menos dos procesos trabajan con un mismo
recurso.

 Por defecto, cuando un método o bloque de código es ejecutado en paralelo más de una vez, cada
llamada es asincrónica.
 En consecuencia, si dos o más procesos trabajan con un mismo objeto en esas circunstancias, el recurso será
manejado por el primer proceso que llegue a ocuparlo.
 En el peor caso, puede causar en los demás procesos un comportamiento incorrecto debido al estado del recurso:
 Se puede obtener un valor inesperado del recurso manejado por los procesos
 Se puede lanzar una excepción desde un proceso debido a un valor o estado indebido del recurso tras ser manipulado
por otro proceso.
 Se puede obtener un deadlock (espera infinita) en los demás procesos debido a que el proceso que está trabajando con
el recurso se está demorando “una eternidad”.

 Esto provoca lo que se conoce en la informática como un Problema de la Sección Crítica, donde, en
este caso, la sección crítica corresponde a las SLOC que trabajan con el recurso dentro de cada
proceso.
Ejemplo de operación asíncrona errónea
(fuente: [Link]
 Supongamos que existe una clase como la que sigue:

 Para el ejemplo, asumamos que el método getSum() existe y funciona correctamente,


devolviendo el valor del atributo sum.
Ejemplo de operación asíncrona errónea
(fuente: [Link]
 Luego, en otra clase, se tiene el siguiente método, que funciona en la versión 8 o posterior
de Java. Se debe asumir para el ejemplo que todos los métodos existen y funcionan en la
misma clase que la de la imagen:
Ejemplo de operación asíncrona errónea
(fuente: [Link]
 Lo que hace el método es definir un objeto ExecutorService llamado service para ejecutar hebras de manera tal
que solo se puedan ejecutar un máximo de 3 a la vez.

 El método crea un IntStream para hacer 1001 iteraciones (entre 0 y 1000) y por cada una:
 Se crea un objeto Runnable utilizando la expresión ::
 Cuando se desea crear una instancia de una interfaz por medio de una expresión lambda, si el contenido de esa expresión es
únicamente la llamada a un método y los parámetros de ese método son los mismos del método de la interfaz (la misma cantidad, en
el mismo orden y con los mismos tipos para cada parámetro) o ninguno de estos dos métodos tienen parámetros, y además, ambos
métodos devuelven el mismo tipo de dato o son void, en vez de usar el operador flecha (->) para definir una expresión lambda, se
puede usar el operador :: para llamar al método a ser llamado desde la expresión lambda.
 En el ejemplo de la diapositiva anterior, la línea [Link](summation::calculate) equivale a
[Link](()→[Link]()) porque tanto el método calculate como el método run no poseen argumentos y ambos son
void.

 El objeto service es utilizado para obtener una instancia de Future<?> a partir del objeto Runnable creado llamando
al método submit.

 En consecuencia, habrán 1001 llamadas asíncronas de manera tal que se ejecuten hasta 3 a la vez.

 El método esperará hasta que todas las ejecuciones realizadas con las llamadas a summit se completen, o hasta
haber pasado 1000 milisegundos.

 Finalmente, comparará la suma obtenida con getSum() para determinar si es igual a 1000.
Ejemplo de operación asíncrona errónea
(fuente: [Link]
 El resultado esperado es que el valor devuelto por
getSum() después de todas las ejecuciones sea igual a
1000. Sin embargo, como las llamadas son asíncronas,
el resultado en el caso promedio es obtener un valor
inferior a 1000, fallando la validación al final del método.
Soluciones de sincronización
 En la práctica, la mejor solución para este problema es
evitar ejecuciones asíncronas. En otras palabras, las
ejecuciones se deben realizar de manera tal que el recurso
sea manejado por un proceso y solo un proceso a la vez.
 Con la sincronización se evita que dos o más métodos o
bloques de código a la vez utilicen el mismo recurso.
 Java proporciona diversas soluciones para obligar a que
las llamadas en paralelo sean sincronizadas.
La palabra reservada synchronized
 Esta palabra reservada, disponible desde la primera versión
de Java, es utilizada para forzar a un método o bloque de
código a ser invocado solo una vez por cada proceso.
 Esto significa que, sin importar cuántos procesos se ejecuten
a la vez ni el contenido de cada uno, si desde un proceso se
está llamando a un método o bloque de código sincronizado,
ningún otro puede llamarlo hasta que la ejecución del método
haya finalizado
 Java permite métodos estáticos sincronizados.
Ejemplo de synchronized con métodos
Blóques de código sincronizados
 Tal como se dijo antes, synchronized permite sincronizar bloques de código en vez de un
método completo.
 Al hacerlo de esta manera, es obligatorio especificar un objeto monitor (y solo uno).
 El efecto de esto es que mientras el objeto monitor esté siendo utilizado dentro de un
bloque sincronizado de un proceso, ningún otro proceso puede utilizarlo.
 Si el bloque sincronizado está dentro de un método no estático, se puede utilizar la
palabra reservada this para indicar que el mismo objeto será el monitor.
 Si el bloque sincronizado está dentro de un método estático, se puede utilizar la instancia
de Class obtenida a partir de la clase a la que pertenece el método para indicar que la
misma clase será el objeto monitor.
 Java permite bloques sincronizados anidados y, para estos casos, también permite que un
mismo objeto sea monitor para dos o más bloques.
Ejemplos de synchronized con bloques de código
Clases sincronizadoras
 La única gran limitación de la palabra reservada synchronized es que
no permite sincronizaciones personalizadas, ya sea cantidad de
procesos simultáneos a sincronizar, recurso(s) a compartir y
condiciones para bloquear cada proceso.
 Si el programador necesita tales personalizaciones, Java proporciona,
a partir de su versión “1.5”, dentro de [Link], un grupo de
clases conocidas como Clases Sincronizadoras.
 La elección de clase(s) sincronizadora(s) a utilizar en la aplicación
depende de los requerimientos de la aplicación y/o de las necesidades
del programador.
La clase CountdownLatch
 Otro problema con las ejecuciones de hebras en
paralelo es que no se sabe cuando terminarán todas las
hebras, por lo que no se garantiza que cualquier línea
definida después de la ejecución de las hebras se
ejecute exactamente después de la ejecución de la
última hebra.
 Java proporciona la clase sincronizadora
CountdownLatch para evitar este problema.
Cómo funciona CountdownLatch
 Primero se debe definir cuántas hebras se desea generar y ejecutar.

 Crear uno o más objetos Thread que se deseen ejecutar.


 Si lo que se crea son subclases de Thread, estas deben tener un atributo que es instancia de CountdownLatch
 Si lo que se crea son implementaciones de Runnable, estas deben tener un atributo que es instancia de
CountdownLatch.
 En cada caso, dentro del método run, luego de hacer todas las operaciones deseadas, se debe llamar al método
public void countdown() del atributo CountdownLatch.

 Instanciar un objeto CountdownLatch en el método desde donde se ejecutan las hebras. El constructor
de esa clase admite un único argumento entero indicando la cantidad inicial de contadores utilizados
para el bloqueo.

 Pasar el objeto CountdownLatch instanciado como parámetro al constructor de la clase que sea
extensión de Thread o que implemente Runnable al momento de crear las hebras. En ese constructor,
el parámetro CountdownLatch debe ser asignado al atributo que sea instancia de esa clase.

 Después de ejecutar las hebras, se debe llamar al método public void await() para llevar a cabo el
bloqueo.
Cómo funciona CountdownLatch
 El efecto de lo anterior es el que sigue:
 Luego de ejecutar todas las hebras, la aplicación estará bloqueada por tiempo indefinido.
 Durante ese tiempo, cada vez que una hebra llame al método countdown(), se disminuirá en 1 la cantidad de
contadores en el objeto CountdownLatch.
 Cuando esa cantidad llegue a cero, el bloqueo termina y la aplicación continúa su ejecución.

 Se debe tener cuidado al definir la cantidad de hebras y la cantidad de contadores:


 Si la cantidad de contadores es menor a la de hebras, el resto de las hebras se ejecutara asíncronamente.
 Si la cantidad de contadores es mayor a la de las hebras, el bloqueo será permanente.
 Para evitar la situación del punto anterior, se puede llamar a await pasando como parámetros una variable long
indicando el tiempo de espera y un objeto TimeUnit indicando la unidad en que debe ser expresado el tiempo

 Java permite bloquear una aplicación con múltiples CountdownLatch, respetando las consideraciones
de esta diapositiva y la anterior.
 La clase que es subclase de Thread o que implemente Runnable debe tener la cantidad deseada de atributos
CountdownLatch. Esa misma cantidad de objetos debe ser instanciada desde el método donde se ejecuten las
hebras.
Ejemplo de CountdownLatch

La salida del método


de la derecha es una
lista conteniendo 5
repeticiones de
“Counted down” y
después “Latch
released”
La clase CyclicBarrier
 Una alternativa a CountdownLatch es la clase
CyclicBarrier.
 Esta clase permite implementar una “condición barrera”
que consiste en que dos o más procesos dentro de la
barrera deben esperar entre sí hasta que todos hayan
terminado su ejecución.
 Opcionalmente, también permite ejecutar una tarea
adicional una vez que la última hebra en la barrera haya
terminado su ejecución.
Cómo funciona la clase CyclicBarrier. Caso
normal
 Primero se debe definir cuántas hebras se desea generar y ejecutar.

 Crear uno o más objetos Thread que se deseen ejecutar.

 Si lo que se crea son subclases de Thread, estas deben tener un atributo que es instancia de CyclicBarrier
 Si lo que se crea son implementaciones de Runnable, estas deben tener un atributo que es instancia de
CyclicBarrier.
 En cada caso, dentro del método run, luego de hacer todas las operaciones deseadas, se debe llamar al método
public void await() del atributo CyclicBarrier.

 Instanciar un objeto CyclicBarrier en el método desde donde se ejecutan las hebras. El constructor de esa clase
admite, por defecto, un único argumento entero indicando la cantidad de procesos que se desea colocar en la
barrera.

 Pasar el objeto CyclicBarrier instanciado como parámetro al constructor de la clase que sea extensión de Thread
o que implemente Runnable al momento de crear las hebras. En ese constructor, el parámetro CyclicBarrier debe
ser asignado al atributo que sea instancia de esa clase.

 Las llamadas al método start de todas las hebras debe estar dentro de un if que tenga como condición que la
barrera no se haya roto. Para chequear esto, se debe llamar al método public boolean isBroken() de CyclicBarrier.
De lo contrario, se corre el riesgo de que se lance un BrokenBarrierException al momento de ejecutar las hebras.
Cómo funciona la clase CyclicBarrier. Caso
normal
 El efecto de lo anterior es el siguiente:
 Una vez ejecutada la cantidad de hebras especificada en el
constructor de CyclicBarrier, cada hebra permanecerá
bloqueada al momento de llamar a await.
 Cuando la última de las hebras en la barrera haya terminado
su ejecución, la barrera es removida y el resto de la
aplicación se ejecuta normalmente.
Ejemplo de CyclicBarrier, caso normal
Cómo funciona la clase CyclicBarrier. Caso
especial
 Opcionalmente, al momento de instanciar el objeto CyclicBarrier, se le puede pasar
un segundo parámetro, que debe ser un objeto que sea instancia de cualquier clase
que implemente la interfaz Runnable.
 Se recomienda que no sea un objeto utilizado por una hebra.
 Si eso ocurre, cuando la última hebra en la barrera haya terminado su ejecución, se
ejecutará el método run del objeto del segundo parámetro automáticamente.
 Si se desea que todos los objetos Runnable trabajen con los mismos recursos,
páselos como atributo a todas las clases deseadas que implementen Runnable o
bien define todas esas clases como clases internas dentro de la clase cuyo método
propio define, ejecute y bloquee las hebras
 Un ejemplo de lo anterior está disponible en
[Link]
CyclicBarrier v/s CountdownLash
 Aunque ambas clases tienen la misma finalidad, tienen las siguientes
diferencias principales:
 Ámbito de la clase:
 CountdownLash no trabaja directamente con las hebras, sino con las tareas definidas para cada
hebra.
 CyclicBarrier sí trabaja directamente con las hebras.
 Reusabilidad:
 CountdownLash, por definición, al momento de disminuir su contador interno, este no volverá a
incrementarse, razón por la que si hay más procesos que número de contadores, los demás
procesos se ejecutarán asíncronamente.
 Para CyclicBarrier, por el contrario, una vez que su contador interno disminuya, solo se
incrementará si un proceso termina su ejecución. En consecuencia:
 Si hay más procesos creados que la cantidad máxima definida en la barrera, los procesos restantes
permanecerán en espera hasta que uno de ellos termine su ejecución o haya sido interrumpido.
 Si hay menos procesos que la cantidad definida, solo los procesos existentes se incluirán en la barrera.
Semáforos en Java
 El paquete [Link] contiene una
implementación del algoritmo del semáforo, la clase
Semaphore.
 Esta clase no solo permite adquirir un acceso de un
proceso a su sección crítica, sino también permite
chequear si el acceso desde un proceso es posible y
cuantos procesos pueden acceder a su sección crítica.
Semáforos en Java
 La clase Semaphore tiene un constructor que admite una variable de tipo entero para indicar la cantidad
máxima de procesos a poder entrar en su sección crítica.

 Un objeto Semaphore debe ser definido como atributo en cualquier clase que se desee ser utilizada
como recurso compartido por los procesos.

 Para que un proceso acceda a su sección crítica, debe existir un método en la clase que tiene el
semáforo que llame a uno de los siguientes métodos:
 public void acquire(): Con este método, el proceso que intente entrar en su sección crítica entrará
inmediatamente si hay espacio disponible. De lo contrario, permanecerá en espera hasta que otro proceso
salga de su sección crítica.
 public boolean tryAcquire(): A diferencia del método anterior, si hay espacio disponible para más procesos,
este método llamará a acquire y devolverá true. En caso contrario, devolverá false.
 Una vez que el proceso termine su ejecución, desde el objeto de la clase que tiene el semáforo, debe
existir una llamada al método public void release() para emitir la señal de liberación.

 Opcionalmente, se puede chequear cuántos procesos pueden entrar en su sección crítica con el método
public int availablePermits().
Ejemplo de semáforo en Java
La interfaz BlockingQueue<T>
 Esta interfaz es una extensión directa de [Link]<T> y una indirecta de [Link]<E> (razón por
la cual es considerada una colección de multiobjetos) para implementar colas de bloqueo que serán utilizadas
para meter o sacar recursos concurrentemente.

 Los objetos que son instancias de esta clase son utilizados principalmente para implementaciones de soluciones
en Java a otro problema de programación concurrente conocido como “El Problema del Productor-Consumidor”.

 Java proporciona implementaciones propias de esta interfaz.

 Ni BlockingQueue ni las clases que lo implementan permiten que se inserten objetos que no son instancias de T
o de alguna de sus subclases, o que se eliminen o busquen objetos que no sean instancias de T o de alguna de
sus subclases. El intentar hacer cualquiera de estas cosas provoca que se lance un ClassCastException.

 A diferencia de otras colecciones de multiobjetos:

 Una vez definido el largo de un objeto BlockingQueue, este no se puede modificar.


 Cualquier intento por insertar un elemento en una cola llena puede implicar una pausa, la devolución de un valor
para indicar fracaso o el lanzamiento de una excepción dependiendo del método llamado por el programador para la
inserción.
 Ni BlockingQueue ni las clases que lo implementan permiten que se inserten elementos nulos en las colas. Intentar
hacerlo provoca que se lance un NullPointerException
La interfaz BlockingQueue<T>
 Esta interfaz posee, entre otros métodos, los siguientes:
 Para meter recursos al buffer:
 public boolean offer(T elem): Si la cola está llena, devuelve false. En caso contrario,
inserta el recurso y devuelve true.
 public boolean offer(T elem, long time, TimeUnit unit): Igual que el anterior con la
diferencia de que si la cola está llena, pausa la aplicación durante el tiempo
especificado por time expresado en la unidad especificada por unit y devuelve false si
el tiempo de espera expiró.
 public boolean add(T elem): Este método, presumiblemente, llama a offer para
intentar insertar el recurso. Si el valor retornado por offer es false, se lanzará un
IllegalStateException.
 public void put(T elem): Si la cola está llena, pausa el proceso hasta que haya al
menos un espacio disponible en el buffer.
La interfaz BlockingQueue<T>
 Esta interfaz posee, entre otros métodos, los siguientes:

 Para sacar recursos del buffer


 public boolean remove(Object obj): Intenta remover el elemento especificado por obj de la cola, utilizando el
método equals de T para determinar si obj es igual a uno de los elementos de la cola. Si no lo encuentra o la
cola está vacía, devuelve false. En caso contrario, devuelve true. Si no es posible hacer un casting a T de obj,
se lanzará un ClassCastException.
 public T take(): Intenta remover el elemento al frente de la cola (por ejemplo, si se insertó “Hola” y después se
insertó “Adiós”, al llamar a este método por primera vez, el valor devuelto es “Hola”). Si la cola está vacía,
pausará el proceso hasta que un elemento se haya insertado en esta.
 public T poll(long time, TimeUnit unit): Igual que take, con la diferencia de que el proceso permanecerá en
pausa el tiempo especificado por time expresado en la unidad especificada por unit. Si el tiempo de espera
expira antes de haber un espacio en la cola, se devuelve null.
 public int drainTo(Collection<? super T> col): Remueve todos los elementos de la cola y los inserta en col y
devuelve la cantidad de elementos transferidos. Si col es instancia de una clase que implementa Collection
que no soporta inserción de elementos, se lanzará un UnsupportedOperationException. Si el valor de col es
la misma cola desde donde se sacaron los elementos, se lanzará un IllegalArgumentException.
 public int drainTo(Collection<? super T> col, int max): Igual que el anterior, con la diferencia de que solo se
transfieren, como máximo, los primeros max elementos disponibles de la cola.
La interfaz BlockingQueue<T>
 Java posee las siguientes implementaciones de BlockingQueue<T>:
 ArrayBlockingQueue<T>: La cola es implementada internamente como una FIFO (First In, First Out) usando un
arreglo. Se puede fijar su capacidad utilizando uno de sus constructores, que requiere de un entero como parámetro.
 DelayedQueue<T>:
 La cola no puede tener un largo fijo. Su largo debe ser el máximo valor posible para una variable de tipo entero, es decir,
el valor de la constante Integer.MAX_INT.
 La cola solo puede contener objetos que son instancias de cualquier clase que implemente la interfaz Delayed (véase
Las Interfaces Future, Delayed y ScheduledFuture para más detalles).
 Esta clase tiene dos constructores: uno permite crear una cola vacía y otro permite crear una a partir de los elementos
de otra colección de elementos de tipo T.
 LinkedBlockingQueue<T>: La cola es implementada internamente como una FIFO usando una lista enlazada simple.
Esta clase tiene 3 constructores: El primero genera una cola vacía permitiendo establecer su largo. El segundo
genera una cola vacía con el máximo largo posible. El tercero genera una cola a partir de los elementos de otra
colección de elementos de tipo T.
 LinkedBlockingDeque<T>: La cola es implementada usando una lista enlazada doble, lo que le permite agregar o
quitar elementos al principio y al final de la cola. Para ello, esta clase también implementa la interfaz
BlockingDeque<T>, la cual es extensión de la interfaz Deque<T>. Esta clase tiene 3 constructores similares a los de
LinkedBlockingQueue.
La interfaz BlockingQueue<T>
 Java posee las siguientes implementaciones de BlockingQueue<T>:

 LinkedTransferQueue<T>:
 Igual que LinkedBlockingQueue, pero obligando a las colas a tener un máximo de Integer.MAX_INT elementos.
 Esta clase implementa la interfaz TransferQueue<T> que es una extensión de BlockingQueue<T>
 TransferQueue es utilizada especialmente para implementar soluciones al Problema del Productor-Consumidor (esto no impide usar otra
implementación de BlockingQueue para tal fin). Para ello, proporciona los métodos transfer(), transfer(long, TimeUnit) y tryTransfer() para transferir
recursos a un proceso consumidor.
 Esta clase tiene 2 constructores. El primero permite generar una cola vacía. El segundo permite generar una a partir de los elementos de otra
colección con elementos de tipo T.

 PriorityBlockingQueue<T>:
 Los elementos son automáticamente ordenados por prioridad.
 Para esta clase, al igual que para la clase PriorityQueue, la prioridad puede ser definida utilizando cualquier clase que implemente la interfaz
Comparator<T> o bien usar el orden por defecto dependiendo de T.
 Esta clase tiene 4 constructores, de los cuales 3 funcionan de manera análoga a los constructores de LinkedBlockingQueue. El cuarto permite generar
una cola vacía con un largo inicial y un objeto Comparator<T> para el ordenamiento de los elementos de la cola.

 SynchronousQueue<T>:
 Las colas deben tener un máximo de Integer.MAX_INT elementos.
 Cada vez que se intente insertar un recurso a la cola, el proceso que lo inserta debe permanecer en pausa hasta que otro proceso quite un recurso de
ésta y viceversa.
 Se puede crear una cola con esta clase utilizando su constructor por defecto.
Ejemplo del Problema del Productor-Consumidor
 El siguiente ejemplo utiliza la clase LinkedBlockingQueue<Integer> para implementar un
problema de Productor-Consumidor para 4 productores, una cantidad de consumidores igual a la
cantidad de “procesadores” posibles y un buffer con un máximo de 10 recursos.
 Cada productor, una vez ejecutado, produce una cantidad de recursos de tipo Integer igual a
100 números escogidos al azar entre 0 y 99 (con repetición) más una cantidad de “píldoras
venenosas” (también de tipo Integer, con valor igual a Integer.MAX_INT) igual al cuociente
entero entre la cantidad de consumidores y la de productores.
 Cada consumidor está infinitamente tomando elementos del buffer. Si el número tomado no
corresponde a una “píldora venenosa”, despliega su valor en pantalla.
 Existe un proceso principal que crea una hebra por cada productor y una por cada consumidor.
 Al final del proceso principal se agrega una hebra para un quinto productor que generará 100
números escogidos al azar entre 0 y 99 (con repetición) más una cantidad de “píldoras
venenosas” iguales a la suma entre el cuociente y el resto de la división entera entre la cantidad
de consumidores y la de los productores definidos inicialmente.
Ejemplo de Problema de Productor-Consumidor
usando BlockingQueue: Clase productora
Ejemplo de Problema de Productor-Consumidor
usando BlockingQueue: Clase consumidora
Ejemplo de Problema de Productor-Consumidor
usando BlockingQueue: Proceso principal
Manejo de procesos con
Justicia (Fairness)
 La justicia (o fairness como se menciona en inglés) en términos de programación
concurrente se refiere a la forma en que dos o más procesos son manejados en el sentido
de a qué proceso permitir entrar en su sección crítica.

 Una aplicación o sistema operativo aplica un 100% de justicia cuando al momento de


designar el siguiente proceso a entrar a su sección crítica, se designa el proyecto que más
tiempo ha estado esperando.

 Una aplicación o sistema operativo aplica on 0% de justicia cuando al momento de


designar el siguiente proceso, se selecciona el primero disponible.

 Un mal manejo de procesos en términos de justicia puede provocar lo que se conoce en


informática como “Hambruna de procesos” (en inglés, “starvation”), que es cuando un
proceso se queda sin recursos para completarse y se queda en espera de que otro
proceso libere tales recursos, lo cual, en el peor caso, puede nunca ocurrir. Esto último es
conocido en informática como un “deadlock”.
Manejo de Fairness en Java
 En Java, es imposible alcanzar un 100%
de justicia.
 Sin embargo, permite que cualquier
aplicación con múltiples hebras aplique
justicia durante su ejecución tanto como
se pueda.
Manejo de Fairness en Java
 Las siguientes clases, de las vistas anteriormente en esta presentación, permiten
aplicar justicia en Java:
 ArrayBlockingQueue: Esta clase permite aplicar justicia mediante 2 de sus 3 constructores:
 public ArrayBlockingQueue(int largo, boolean fairness): permite generar una cola vacía con largo
establecido, aplicando justicia si el valor de fairness es true.
 public ArrayBlockingQueue(int largo, boolean fairness, Collection<? extends T> c). Igual que el
anterior, pero poblando la cola con el contenido de c en el orden en que cada uno de sus elementos
fue insertado.

 SynchronousQueue: Esta clase permite aplicar justicia mediante uno de sus dos
constructores, que requiere de un único parámetro booleano para indicar si se aplica
justicia o no.

 Semaphore: Esta clase permite aplicar justicia mediante uno de sus dos constructores, que
admite dos parámetros. El primero es la cantidad máxima de permisos que se puedan
adquirir desde el semáforo y el segundo es una bandera booleana para indicar si se
aplicará justicia o no.
Cerrojos: Lock API
 En detalle, el uso de la palabra reservada
synchronized posee las siguientes
limitaciones:
 No permite aplicar justicia a los procesos
 Se corre riesgo de deadlock si un proceso no
puede acceder al bloque sincronizado
 Cualquier proceso esperando entrar a un bloque
sincronizado no puede interrumpirse.
Cerrojos: Lock API
 Lock API es un conjunto de clases e
interfaces para manejo de cerrojos.
Dichas clases e interfaces se encuentran
en el paquete [Link]
 Lock API representa una alternativa más
flexible que el uso de synchronized
Cerrojos: Lock API
 ¿Cómo funciona?
 Se crea un objeto que es una instancia de la interfaz Lock.
 Se llama al método lock o a tryLock del objeto Lock.
 Posteriormente se define un bloque try/finally:
 En el bloque try se debe colocar las SLOC que deben ser
sincronizadas.
 En el bloque finally se debe llamar al método unlock del objeto Lock
para poner fin al bloque sincronizado.
 No es necesario utilizar catch a menos que se tenga que manejar una
excepción.
La interfaz Lock
 Lock proporciona los siguientes métodos utilizados para el manejo de un cerrojo:
 public void lock(): Esta línea da inicio al bloque sincronizado. Al llamar a este método desde un
proceso, si el cerrojo está disponible, el acceso al bloque es otorgado al proceso. En caso
contrario, el proceso es bloqueado hasta que el cerrojo sea liberado.

 public void lockInterruptibly(): Similar a lock(), con la diferencia de que con este método permite a
un proceso bloqueado ser interrumpido

 public boolean tryLock(): Intenta adquirir inmediatamente el acceso al bloque sincronizado y


devuelve true si lo logra o false en caso contrario.

 public boolean tryLock(long time, TimeUnit unit): Igual al anterior, con la diferencia de que este
método espera la cantidad de tiempo en la unidad de tiempo especificados.

 public void unlock(): Esta línea pone fin al bloque sincronizado y libera el cerrojo para que el
bloque sea llamado desde otro proceso.

 Opcionalmente permite emular una clase monitor con el método newCondition(), que devuelve
una instancia de la interfaz Condition.
La interfaz Condition
 Esta interfaz, que pertenece a LockAPI permite que
cualquier instancia de la clase que la implemente
haga las veces de objeto monitor cuando se están
utilizando cerrojos para la sincronización.
 Aunque es posible crear propias implementaciones
de Condition, para obtener un objeto Condition, se
recomienda llamar al método newCondition() de la
interfaz Lock.
La interfaz Condition
 Cómo funciona:
 El o los objetos Condition deben crearse a partir de un objeto Lock existente y
definido.
 Luego se debe dar inicio al bloque sincronizado llamado al método lock del objeto
Lock.
 Definir los bloques try/finally del mismo modo como si se utilizaran cerrojos sin
condiciones con las siguientes dos diferencias:
 Se debe definir, dentro del bloque try, un ciclo que se repita mientras se cumpla una
condición booleana cualquiera. Dentro de ese bloque se debe llamar al método public void
await() del objeto Condition que se desee utilizar en el bloque, obligando al proceso actual a
estar en espera indefinidamente.
 Opcionalmente, después del ciclo, si se trabaja con dos o más objetos Condition y cada
condición está en un bloque sincronizado diferente y se desea avisar a otro objeto Condition
que su proceso actual deje de esperar, se debe llamar al método public void signal() de ese
otro objeto Condition.
Ejemplo de Lock y Condition
La interfaz Condition
 Métodos de espera:
 El método await, mencionado previamente, hace que el proceso actual espere
indefinidamente hasta una llamada al método signal, o hasta que el proceso sea
interrumpido.

 Para hacer que el proceso espere indefinidamente prohibiendo cualquier interrupción


posible, se utiliza el método awaitUninterruptibly

 Para hacer que el proceso espere un lapso fijo de tiempo antes de interrumpirse
automáticamente, se utiliza el método await pasando como parámetros una variable long
con el tiempo de espera y un objeto TimeUnit con la unidad en la que se expresa el tiempo.

 El método anterior puede sustituirse por el método awaitNanos(long) si se desea que el


tiempo de espera esté expresado en nanosegundos.

 Si se desea hacer que el proceso espere hasta una fecha específica, se utiliza el método
awaitUntil(Date)

 Todos los métodos mencionados son de tipo void.


La interfaz Condition
 Métodos de emisión de señales:
 El método signal, mencionado previamente, solo
permite reanudar la ejecución de una hebra. Cuál
hebra reanudar depende de si al objeto Lock asociado
al objeto Condition creado se le aplica o no justicia.
 Para poder reanudar la ejecución de todas las hebras
en espera, se utiliza el método signalAll()
 Ambos métodos son de tipo void.
La clase ReentrantLock
 Java proporciona la clase ReentrantLock, que implementa la interfaz Lock. Esta clase
posee el comportamiento más cercano posible a la palabra reservada synchronized más
algunas extensiones como las de las implementaciones de los métodos de Lock para
personalizar la sincronización.
 Esta clase tiene dos constructores: el primero admite un parámetro booleano para indicar si se
aplica justicia al cerrojo y el segundo, sin parámetros, para indicar que no se aplicará justicia.

 También contiene métodos booleanos para:


 chequear si se aplica justicia al cerrojo (isFair()),
 si el cerrojo está siendo utilizado por una hebra (isLocked()),
 si posee hebras en espera (hasQueuedThreads()),
 si una hebra en específico está en espera (hasQueuedThread(Thread t))
 si existe al menos una hebra en espera dada una condición específica (hasWaiters(Condition c))

 También permite obtener al proceso propietario del cerrojo con public Thread getOwner(). Si el
cerrojo no tiene propietario, el método devuelve null.
La Interfaz ReadWriteLock
 Esta interfaz permite obtener dos cerrojos, uno
para operaciones de lectura y uno para
operaciones de escritura.
 La interfaz posee dos métodos que devuelven
un objeto Lock cada uno: uno para lectura
(public Lock readLock()) y uno para escritura
(public Lock writeLock()).
La clase
ReentrantReadWriteLock
 Esta clase proporcionada por Java implementa la
interfaz ReadWriteLock para obtener
implementaciones propias de Lock tanto para la
lectura como para la escritura.
 Esta clase posee dos constructores que tienen la
misma funcionalidad que los constructores de
ReentrantLock. Además, posee los mismos
métodos propios de esa clase.
Ejemplo de
ReentrantReadWriteLock
La clase StampedLock (Java 8
en adelante)
 Esta clase es una alternativa a
ReadWriteLock para el manejo de
cerrojos de lectura y escritura.
 Esta clase NO implementa la interfaz Lock.
La clase StampedLock (Java 8
en adelante)
 Cómo funciona:
 Se debe crear una instancia de StampedLock utilizando el constructor por defecto

 Al momento de implementar un cerrojo de lectura:


 Antes del bloque try, se debe declarar una variable long asignándole lo que retorna el método public long
readLock(). A esa variable le llamamos stamp.
 Dentro del bloque finally, se debe llamar al método public void unlockRead(long stamp) pasándole como
parámetro el valor obtenido de stamp.
 La adquisición del cerrojo no es exclusiva.

 Al momento de implementar un cerrojo de escritura:


 Antes del bloque try, se debe declarar una variable long asignándole lo que retorna el método public long
writeLock().
 Dentro del bloque finally, se debe llamar al método public void unlockWrite(long stamp) pasándole como
parámetro el valor obtenido del método anterior.
 La adquisición del cerrojo es exclusiva.

 Opcionalmente se puede determinar si el estampado asociado al cerrojo es válido con el método


public boolean validate().
La clase StampedLock (Java 8
en adelante)
 Alternativas para adquirir cerrojos:
 Al momento de implementar un cerrojo de lectura, se puede sustituir el
método readLock por cualquiera de los siguientes métodos que devuelven
un valor long:
 readLockInterruptibly(): igual que readLock, con la diferencia de que readLock no
libera del bloqueo al proceso actual en caso de ser interrumpido.
 tryReadLock(): igual que readLock con la diferencia de que readLock espera
indefinidamente hasta que el cerrojo esté disponible para adquisición, mientras que
tryReadLock devuelve un cero si el cerrojo no está disponible.
 tryReadLock(long time, TimeUnit unit): Igual que el anterior, con la diferencia de que
este método espera la cantidad de tiempo especificada en time expresada en la
unidad de tiempo especificada por unit. Si el tiempo expira, el método devuelve un
cero.
 tryOptimisticRead(): “Devuelve un estampado que puede ser validado después, o un
cero si el cerrojo es exclusivo”.
La clase StampedLock (Java 8
en adelante)
 Alternativas para adquirir cerrojos:
 Al momento de implementar un cerrojo de escritura, se puede sustituir el
método writeLock por cualquiera de los siguientes métodos que devuelven
un valor long:
 writeLockInterruptibly(): igual que writeLock, con la diferencia de que writeLock
no libera del bloqueo al proceso actual en caso de ser interrumpido.
 tryWriteLock(): igual que writeLock con la diferencia de que writeLock espera
indefinidamente hasta que el cerrojo esté disponible para adquisición, mientras
que tryWriteLock devuelve un cero si el cerrojo no está disponible.
 tryWriteLock(long time, TimeUnit unit): Igual que el anterior, con la diferencia de
que este método espera la cantidad de tiempo especificada en time expresada
en la unidad de tiempo especificada por unit. Si el tiempo expira, el método
devuelve un cero.
La clase StampedLock (Java 8
en adelante)
 Alternativas para liberar cerrojos:
 Al momento de liberar un cerrojo de lectura, se puede
sustituir unlockRead por el método public boolean
tryUnlockRead(). Este método devuelve true si logra liberar
un cerrojo de lectura sin tener que especificar su estampado
asociado.
 Al momento de liberar un cerrojo de escritura, se puede
sustituir unlockWrite por el método public boolean
tryUnlockWrite(). Este método devuelve true si logra liberar
un cerrojo de escritura sin tener que especificar su
estampado asociado.
La clase StampedLock (Java 8
en adelante)
 Paso de lectura a escritura y viceversa:
 A veces, al terminar de trabajar en la lectura de un recurso, se desea pasar
inmediatamente a la escritura y viceversa.
 Para ello, StampedLock proporciona los siguientes métodos que devuelven un valor long y
admiten otro valor long como parámetro:
 tryConvertToReadLock: Si el parámetro constituye un estampado asociado a un cerrojo de escritura,
lo libera y devuelve un nuevo estampado asociado a un cerrojo de lectura. En caso contrario,
devuelve cero.
 tryConvertToWriteLock: Lo opuesto del método anterior (obtiene un estampado de lectura a partir de
uno de escritura).
 tryConvertToOptimisticRead: Igual que tryConvertToReadLock pero para lectura optimista.

 StampedLock también proporciona métodos para chequear si actualmente se está


aplicando un cerrojo de lectura o uno de escritura al proceso usando, respectivamente, los
métodos booleanos isReadLocked() e isWriteLocked()
La clase StampedLock (Java 8
en adelante)
 Paso de StampedLock a ReadWriteLock:
 A veces, por un tema de migración y/o compatibilidad, cuando
en una aplicación inicialmente se trabaja con ReadWriteLock,
se desea implementar cerrojos de lectura y escritura con
StampedLock de manera tal que se modifique el código lo
menos posible.
 Para ello, StampedLock proporciona el método
asReadWriteLock()
 También es posible obtener un objeto Lock a partir de
StampedLock con los métodos asReadLock() y asWriteLock()
Ejemplo de StampedLock
Anexo 1: Acciones privilegiadas
 Cuando se trabaja con recursos críticos de sistema en aplicaciones Java, a veces, se necesita restringir
ciertas acciones de manera tal que solo los usuarios con privilegios las puedan ejecutar.

 Para ello, Java proporciona dos interfaces pertenecientes al paquete [Link]: PrivilegedAction<T>
y PrivilegedExceptionAction<T>.
 Ambas tienen en común un único método llamado run, que devuelve una instancia de T, donde T es una clase
cualquiera.
 Por este motivo, a partir de Java 8, estas interfaces se consideran funcionales y sus objetos se pueden crear mediante
expresiones lambda.
 La única diferencia es que PrivilegedAction es utilizado cuando el método run no debe lanzar una excepción, o al
menos no debe obligar al programador a lanzar una, mientras que en PrivilegedExceptionAction, el metodo run debe
lanzar una excepción en caso de una falla.

 Para un objeto que es instancia de cualquiera de estas interfaces, el método run no debe ser llamado
manualmente por el programador, sino que debe ser pasado como el primer parámetro de cualquiera de
las sobrecargas del método estático doPrivileged, o de doPrivilegedWithCombiner, ambos
pertenecientes a la clase AccessController, del mismo paquete. Ambos métodos devuelven el valor
devuelto por el método run del objeto pasado como parámetro.
Anexo 2: Problema de la sección crítica
 También conocido como el Problema de exclusión mutua.
 En este problema, cada proceso que comparte un recurso, durante
su ejecución, llega a un punto en el que el recurso es utilizado
directamente. A esta porción del proceso se le conoce como
sección crítica.
 Al compartir recursos de este modo, se corre el riesgo de que el
estado del recurso al final de la ejecución de un proceso x no sea
reconocido o sea tomado como inválido por un proceso x+1, o bien
que los procesos esperen “eternamente” a que un proceso sea
liberado.
Anexo 2: Problema de la sección crítica
 En la década de 1960, el famoso científico computacional e informático
holandés Edsger W. Dijkstra (1930 – 2002), autor del algoritmo para recorrido
de grafos que lleva su nombre, resumió estos problemas en 4 condiciones,
conocidas como “Las cuatro condiciones de Dijkstra”, que indican qué debe
pasar con cada proceso en cuanto a la sección crítica para evitar estos
problemas:
 Un proceso no debe utilizar funciones especiales de hardware, sino solamente la
memoria compartida.
 Si un proceso está en su sección crítica, ninguna otra puede estarlo.
 Si un proceso no está en su sección crítica, no debe prohibir a otros procesos que no
están en su sección crítica entrar a la suya.
 Un proceso no debe estar esperando indefinidamente a que otro salga de su sección
crítica.
Anexo 2: Problema de la sección crítica
 Luego de varios intentos fallidos de solución, respetando esas 4
condiciones, se llegó a una solución para dos procesos creada
por el matemático holandés Theodorus Joseph Dekker (1927 -
) y publicada por Dijkstra en 1965 bajo el nombre de
“Algoritmo de Dekker”.
 Luego, en 1981, basándose en el algoritmo de Dekker, Gary L.
Peterson creó el algoritmo que lleva su nombre, una versión
más simplificada, originalmente para dos procesos, pero que
con el tiempo se demostró servir también para una cantidad
mayor.
Anexo 2: Problema de la sección crítica
Pseudocódigo Algoritmo de Dekker
Anexo 2: Problema de la sección crítica
Pseudocódigo Algoritmo de Peterson para más de 2 procesos
Anexo 3: Algoritmo del Semáforo
 Inventado por Dijkstra entre 1962 y 1963.
 Soluciona el problema de la sección crítica para dos o más procesos, ignorando la
condición de no requerir funciones especiales de hardware.
 Utiliza un contador booleano (para dos procesos) o entero (para más de dos
procesos) llamado I indicando cuántos procesos están en ejecución, dada una
cantidad máxima de procesos (S). Si se llega a esa cantidad, los demás procesos
deberán esperar. Luego, cuando un proceso salga de su sección crítica, debe avisar
a los procesos que están en espera para que uno de ellos entre a su sección crítica.
 Consiste en dos funciones:
 wait (V): para efectuar la espera de los procesos
 signal (S): para indicar desde un proceso que este ya salió de su sección crítica.
Anexo 3: Algoritmo del Semáforo
 Pseudocódigo de las funciones wait y signal
Anexo 4: Problema del Productor-Consumidor
 Este problema de programación concurrente consiste en la siguiente situación:
 Se tiene un proceso que genera recursos que son guardados en un buffer compartido. Este
proceso es conocido como el productor.
 Se tiene otro proceso que toma los recursos generados por el productor. Este proceso es
conocido como el consumidor.
 El buffer compartido tiene un largo máximo arbitrario.
 Si el productor no puede agregar más recursos al buffer por encontrarse este lleno, permanecerá
en pausa hasta que al menos un recurso del buffer haya sido tomado por el consumidor.
 Si el consumidor no puede tomar más recursos del buffer por encontrarse este vacío,
permanecerá en pausa hasta que el productor agregue al menos un recurso al buffer.
 En la práctica, puede existir más de un productor, más de un consumidor y más de un
buffer.
 Se tienen varias ideas de solución (incluyendo uso de semáfotos). Sin embargo, si la idea
utilizada no es la correcta, se corre el riesgo de caer en un deadlock.

También podría gustarte