concurrency java semaphore
Este tutorial discutirá los componentes del paquete java.util.concurrent como Java Semaphore, Executor Framework, ExecutorService para implementar Concurrency en Java:
De nuestros tutoriales de Java anteriores, sabemos que la plataforma Java admite la programación concurrente desde cero. La unidad básica de concurrencia es un hilo y hemos discutido en detalle los hilos y el multihilo en Java.
Desde Java 5 en adelante, se agregó un paquete llamado 'java.util.concurrent' a la plataforma Java. Este paquete contiene un conjunto de clases y bibliotecas que facilitan al programador el desarrollo de aplicaciones concurrentes (multiproceso). Con este paquete, no tenemos que escribir clases complejas ya que tenemos implementaciones listas para la mayoría de los conceptos concurrentes.
=> Consulte TODOS los tutoriales de Java aquí.
En este tutorial, discutiremos los diversos componentes del paquete java.util.concurrent relacionados con la concurrencia y el multiproceso en Java.
Lo que vas a aprender:
java.util.concurrent Package
A continuación se enumeran los diversos componentes del paquete java.util.concurrent relacionados con la concurrencia y el subproceso múltiple en Java. Exploremos cada componente en detalle con la ayuda de ejemplos de programación simples. Algunos de los componentes que haremos
discutir son:
- Marco del ejecutor
- ExecutorService
- ThreadPool
- Invocable
- Cerraduras - ReentrantLock
- Semáforo
- HorquillaUnirsePiscina
Executor Framework en Java
Executor Framework en Java se lanzó con la versión JDK 5. Executor Framework (java.util.concurrent.Executor) es un marco que consta de componentes que nos ayudan a manejar de manera eficiente múltiples subprocesos.
Usando Executor Framework, podemos ejecutar objetos que son Ejecutables reutilizando los subprocesos ya existentes. No necesitamos crear nuevos subprocesos cada vez que necesitamos ejecutar objetos.
La API del ejecutor separa o desacopla la ejecución de una tarea de la tarea real mediante un Ejecutor . Un ejecutor se centra en la interfaz del ejecutor y tiene subinterfaces, es decir, ExecutorService y la clase ThreadPoolExecutor.
Por lo tanto, usando Executor, solo tenemos que crear objetos Runnable y luego enviarlos al ejecutor que los ejecuta.
Algunas de las mejores prácticas que se deben seguir al utilizar el marco del Ejecutor son,
- Debemos realizar una verificación cruzada y planificar un código para revisar las listas principales de modo que podamos detectar tanto interbloqueo como livelock en el código.
- El código Java siempre debe ejecutarse con herramientas de análisis estático. Ejemplos de las herramientas de análisis estático son FindBugs y PMD.
- No solo deberíamos detectar las excepciones, sino también los errores en los programas de subprocesos múltiples.
Ahora analicemos los componentes de Executor Framework en Java.
Ejecutor
El ejecutor se puede definir como una interfaz utilizada para representar un objeto que ejecuta las tareas que se le asignan. Si la tarea se ejecutará en el hilo actual o en el nuevo, depende del punto desde donde se inició la invocación, lo que además depende de la implementación.
Entonces, usando Executor, podemos desacoplar las tareas de la tarea real y luego ejecutarlas de forma asincrónica.
Sin embargo, no es necesario que la ejecución de la tarea mediante Executor sea asincrónica. Los ejecutores también pueden invocar la tarea instantáneamente usando un hilo de invocación.
A continuación se muestra un ejemplo de código para crear una instancia de Ejecutor:
|_+_|Una vez que se crea el invocador, como se muestra arriba, podemos usarlo para ejecutar la tarea de la siguiente manera.
|_+_|Tenga en cuenta que si el Ejecutor no acepta la tarea, arroja RejectedExecutionException.
ExecutorService
Un ExecutorService (java.util.concurrent.ExecutorService) programa las tareas enviadas según la disponibilidad de subprocesos y también mantiene una cola de memoria. ExecutorService actúa como una solución completa para el procesamiento asincrónico de tareas.
Para usar ExecutorService en el código, creamos una clase Runnable. ExecutorService mantiene un grupo de subprocesos y también asigna las tareas a los subprocesos. Las tareas también pueden ponerse en cola en caso de que el hilo no esté disponible.
A continuación se muestra un ejemplo simple de ExecutorService.
|_+_|Producción
En el programa anterior, creamos una instancia simple de ExecutorService con un grupo de subprocesos que consta de 10 subprocesos. Luego se asigna a la instancia Runnable y se ejecuta para imprimir el mensaje anterior. Después de imprimir el mensaje, ExecutorService se cierra.
Grupo de hilos
Un grupo de subprocesos en Java es un grupo de subprocesos de trabajo que se pueden reutilizar muchas veces y asignar trabajos.
Un grupo de subprocesos contiene un grupo de subprocesos de tamaño fijo. Cada subproceso se extrae del grupo de subprocesos y el proveedor de servicios le asigna una tarea. Una vez que se completa el trabajo asignado, el subproceso se vuelve a entregar al grupo de subprocesos.
El grupo de subprocesos es ventajoso ya que no tenemos que crear un nuevo subproceso cada vez que la tarea está disponible, por lo que se mejora el rendimiento. Se usa en aplicaciones en tiempo real que usan Servlet y JSP donde se usan grupos de subprocesos para procesar solicitudes.
En aplicaciones multiproceso, Thread Pool ahorra recursos y ayuda a contener el paralelismo dentro de límites predefinidos.
El siguiente programa de Java demuestra el grupo de subprocesos en Java.
|_+_|Producción
En los programas anteriores, hay un grupo de subprocesos de 5 subprocesos que se crean utilizando el método 'newFixedThreadPool'. Luego, los subprocesos se crean y se agregan al grupo y se asignan al ExecutorService para su ejecución.
Invocable en Java
Ya sabemos que podemos crear hilos usando dos enfoques. Un enfoque consiste en extender la clase Thread, mientras que el segundo enfoque consiste en implementar una interfaz Runnable.
Sin embargo, los subprocesos creados utilizando la interfaz Runnable carecen de una característica, es decir, no devuelven un resultado cuando el subproceso se termina o run () completa la ejecución. Aquí es donde entra en juego la interfaz Callable.
Usando una interfaz invocable, definimos una tarea para que devuelva un resultado. También puede generar una excepción. La interfaz invocable es parte del paquete java.util.concurrent.
La interfaz Callable proporciona un método call () que está en las líneas similares al método run () proporcionado por la interfaz Runnable con la única diferencia de que el método call () devuelve un valor y arroja una excepción marcada.
El método call () de la interfaz Callable tiene el siguiente prototipo.
|_+_|Dado que el método call () devuelve un objeto, el hilo principal debe ser consciente de esto.
Por lo tanto, el valor de retorno debe almacenarse en otro objeto conocido por el hilo principal. Este propósito se cumple mediante el uso de un objeto 'Futuro'. Un objeto Future es un objeto que contiene el resultado devuelto por un hilo. O en otras palabras, mantendrá el resultado cuando Callable regrese.
Invocable encapsula una tarea que debería ejecutarse en otro hilo. Un objeto Future almacena el resultado devuelto por un hilo diferente.
No se puede utilizar una interfaz invocable para crear un hilo. Necesitamos Runnable para crear un hilo. Luego, para almacenar el resultado, se requiere un objeto Future. Java proporciona un tipo concreto llamado 'FutureTask' que combina la funcionalidad implementando Runnable y Future.
Creamos una FutureTask proporcionando un constructor con Callable. Este objeto FutureTask luego se entrega al constructor de la clase Thread para crear un objeto Thread.
A continuación se muestra un programa Java que demuestra la interfaz invocable y el objeto Future. También usamos el objeto FutureTask en este programa.
Como ya se mencionó, en el programa creamos una clase que implementa una interfaz invocable con un método call () anulado. En el método principal, creamos 10 objetos FutureTask. Cada constructor de objetos tiene un objeto de clase invocable como argumento. Luego, el objeto FutureTask se asocia con una instancia de subproceso.
Por lo tanto, indirectamente creamos un hilo usando un objeto de interfaz invocable.
|_+_|Producción
Como se muestra en el programa anterior, el método call () de Callable que se anula en la clase que implementa Callable genera números aleatorios. Una vez que se inicia el hilo, muestra estos números aleatorios.
Además, usamos objetos FutureTask en la función principal. Como implementa la interfaz Future, no necesitamos almacenar los resultados en los objetos Thread. De manera similar, podemos cancelar la tarea, verificar si está ejecutándose o completada y también obtener el resultado usando el objeto FutureTask.
ReentrantLock en Java
Hemos discutido la sincronización de subprocesos usando la palabra clave sincronizada en detalle en nuestro último tutorial. El uso de la palabra sincronizada para la sincronización de subprocesos es el método básico y es algo rígido.
Con la palabra clave sincronizada, un hilo solo se puede bloquear una vez. Además, después de que un subproceso sale del bloque sincronizado, el siguiente subproceso toma el bloqueo. No hay cola de espera. Estos problemas pueden provocar la inanición de algún otro hilo, ya que es posible que no pueda acceder a los recursos durante mucho tiempo.
Para abordar estos problemas, necesitamos un método flexible para sincronizar los hilos. 'Reentrant Locks' es este método en Java que proporciona sincronización con mucha mayor flexibilidad.
La clase 'ReentrantLock' implementa bloqueos Reentrant y es parte del paquete 'import java.util.concurrent.locks'. La clase ReentrantLock proporciona la sincronización de métodos para acceder a los recursos compartidos. Las clases también tienen los métodos de bloqueo y desbloqueo para bloquear / desbloquear recursos cuando se accede a ellos por hilos.
Una característica peculiar de ReentrantLock es que el hilo puede bloquear el recurso compartido más de una vez usando ReentrantLock. Proporciona un recuento de retención que se establece en uno cuando el hilo bloquea el recurso.
El hilo puede volver a ingresar y acceder al recurso antes de desbloquearse. Cada vez que el subproceso accede al recurso mediante el bloqueo Reentrant, el recuento de retención se incrementa en uno. Para cada desbloqueo, el recuento de retención se reduce en uno.
Cuando el recuento de retención llega a 0, el recurso compartido se desbloquea.
La clase ReentrantLock también proporciona un parámetro de equidad que es un valor booleano que se puede pasar con el constructor del bloqueo. Cuando el parámetro de equidad se establece en verdadero, siempre que un subproceso libera el bloqueo, el bloqueo se pasa al subproceso de espera más largo. Esto evita el hambre.
Las cerraduras reentrantes se pueden utilizar de la siguiente manera:
|_+_|Tenga en cuenta que la declaración de desbloqueo para ReentrantLock siempre está en el bloque finalmente. Esto garantiza que el bloqueo se libere incluso si se lanza una excepción.
Implementemos un programa Java para comprender ReentrantLock.
|_+_|Producción
En el programa anterior, hemos creado un hilo y usamos ReentrantLock para ello. Con ReentrantLock se puede acceder al recurso compartido.
Semáforo en Java
El siguiente método de sincronización de subprocesos es mediante Semaphore. Usando esta construcción llamada semáforo, el acceso a un recurso compartido se controla a través de un contador. Las señales se envían entre los subprocesos para que podamos proteger la sección crítica y también evitar señales perdidas.
Un semáforo se puede definir como una variable que se utiliza para gestionar procesos concurrentes sincronizando estos procesos. Los semáforos también se utilizan para sincronizar el acceso al recurso compartido y así evitar una condición de carrera. El permiso otorgado a un hilo para acceder al recurso compartido por semáforo también se denomina permiso.
Dependiendo de las funciones que realicen, los semáforos se pueden dividir en dos tipos:
# 1) Semáforo binario: Se utiliza un semáforo binario para sincronizar procesos concurrentes e implementar la exclusión mutua. Un semáforo binario asume solo dos valores, es decir, 0 y 1.
# 2) Contando el semáforo: El semáforo de conteo tiene un valor que indica la cantidad de procesos que pueden ingresar a la sección crítica. En cualquier punto, el valor indica el número máximo de procesos que ingresan a la sección crítica.
Entonces, ¿cómo funciona un semáforo?
El funcionamiento de un semáforo se puede resumir en los siguientes pasos:
- Si el recuento de semáforos es> 0, significa que el hilo tiene un permiso para acceder a la sección crítica, y luego el recuento se reduce.
- De lo contrario, el hilo se bloquea hasta que se obtenga el permiso.
- Cuando el hilo termina con el acceso al recurso compartido, el permiso se libera y el conteo de semáforos se incrementa para que otro hilo pueda repetir los pasos anteriores y adquirir el permiso.
Los pasos anteriores del funcionamiento de los semáforos se pueden resumir en el siguiente diagrama de flujo.
En Java, no necesitamos implementar nuestro semáforo, pero proporciona una Semáforo clase que implementa la funcionalidad del semáforo. La clase Semaphore es parte de la java.util.concurrent paquete.
La clase Semaphore proporciona los siguientes constructores con los que podemos crear un objeto semáforo:
|_+_|Aquí,
num_value => valor inicial del recuento de permisos que determina el número de subprocesos que pueden acceder al recurso compartido.
cómo => establece el orden en el que se otorgarán permisos a los hilos (cómo = verdadero). Si cómo = falso, entonces no se sigue ese orden.
Ahora implementaremos un programa Java que demostrará el semáforo que se utiliza para administrar el acceso a recursos compartidos y evitar la condición de carrera.
|_+_|Producción
Este programa declaró una clase para el recurso compartido. También declara una clase de subproceso en la que tenemos una variable de semáforo que se inicializa en el constructor de la clase.
En el método run () anulado de la clase Thread, el procesamiento de la instancia del hilo se realiza en el que el hilo adquiere el permiso, accede a un recurso compartido y luego libera el permiso.
En el método principal, declaramos dos instancias de hilo. Ambos subprocesos se inician y luego esperan utilizando el método de unión. Finalmente, se muestra el recuento, es decir, 0, lo que indica que ambos hilos han terminado con el recurso compartido.
Bifurcar y unirse en Java
El marco de bifurcación / unión se introdujo por primera vez en Java 7. Este marco consta de herramientas que pueden acelerar el procesamiento paralelo. Utiliza todos los núcleos de procesador disponibles en el sistema y completa la tarea. El marco de bifurcación / unión utiliza el enfoque de dividir y conquistar.
La idea básica detrás del marco Fork / Join es que el primer marco 'Forks', es decir, divide recursivamente la tarea en subtareas individuales más pequeñas hasta que las tareas son atómicas para que puedan ejecutarse de forma asincrónica.
Después de hacer esto, las tareas se 'unen', es decir, todas las subtareas se unen de forma recursiva en una sola tarea o valor de retorno.
El marco de bifurcación / unión tiene un grupo de subprocesos conocido como 'ForkJoinPool'. Este grupo administra el tipo de subprocesos de trabajo “ForkJoinWorkerThread”, lo que proporciona un procesamiento paralelo eficaz.
ForkJoinPool administra los subprocesos de trabajo y también nos ayuda a obtener información sobre el rendimiento y el estado del grupo de subprocesos. ForkJoinPool es una implementación del 'ExecutorService' que discutimos anteriormente.
A diferencia de los subprocesos de trabajo, ForkJoinPool no crea un subproceso independiente para cada subtarea. Cada subproceso de ForkJoinPool mantiene su deque (cola de dos extremos) para almacenar tareas.
El deque actúa como el equilibrio de la carga de trabajo del hilo y lo hace con la ayuda de un 'algoritmo de robo de trabajo' que se describe a continuación.
Algoritmo de robo de trabajo
Podemos definir el algoritmo de robo de trabajo en palabras simples como 'Si un hilo está libre, 'roba' el trabajo de los hilos ocupados'.
Un hilo de trabajo siempre obtendrá las tareas de su deque. Cuando todas las tareas de la deque se agotan y la deque está vacía, el hilo de trabajo tomará una tarea de la cola de otra deque o de la 'cola de entrada global'.
De esta manera, se minimiza la posibilidad de que los hilos compitan por tareas y también se reduce el número de veces que el hilo tiene que buscar trabajo. Esto se debe a que el hilo ya tiene la mayor parte del trabajo disponible y lo ha terminado.
Entonces, ¿cómo podemos usar ForkJoinPool en un programa?
La definición general de ForkJoinPool es la siguiente:
|_+_|La clase ForkJoinPool es parte del paquete 'java.util.concurrent'.
En Java 8, creamos una instancia de ForkJoinPool usando su método estático 'common-pool ()' que proporciona una referencia al grupo común o al grupo de subprocesos predeterminado.
|_+_|En Java 7, creamos una instancia de ForkJoinPool y la asignamos al campo de la clase de utilidad como se muestra a continuación.
|_+_|La definición anterior indica que el grupo tiene un nivel de paralelismo de 2, de modo que el grupo utilizará 2 núcleos de procesador.
Para acceder al grupo anterior, podemos dar la siguiente declaración.
|_+_|El tipo base para las tareas de ForkJoinPool es 'ForkJoinTask'. Deberíamos extender una de sus subclases, es decir, para tareas nulas, RecursiveAction y para tareas que devuelven un valor, RecursiveTask. Ambas clases extendidas proporcionan un método abstracto compute () en el que definimos la lógica de la tarea.
A continuación se muestra un ejemplo para demostrar ForkJoinPool.
|_+_|Producción
En el programa anterior, encontramos el número de subprocesos activos en el sistema antes y después de llamar al método 'invoke ()'. El método invoke () se utiliza para enviar las tareas al grupo. También encontramos la cantidad de núcleos de procesador disponibles en el sistema.
Preguntas frecuentes
P # 1) ¿Qué es Java Util Concurrent?
Responder: El paquete 'java.util.concurrent' es un conjunto de clases e interfaces proporcionadas por Java para facilitar el desarrollo de aplicaciones concurrentes (multiproceso). Usando este paquete podemos usar directamente la interfaz y las clases, así como las API sin tener que escribir nuestras clases.
P # 2) ¿Cuáles de las siguientes son implementaciones concurrentes presentes en java.util. paquete concurrente?
¿Cuáles son las fases de sdlc?
Responder: En un nivel alto, el paquete java.util.concurrent contiene utilidades como Ejecutores, Sincronizadores, Colas, Tiempos y Colecciones concurrentes.
P # 3) ¿Qué es Future Java?
Responder: Se utiliza un objeto Future (java.util.concurrent.Future) para almacenar el resultado devuelto por un hilo cuando se implementa la interfaz Callable.
P # 4) ¿Qué es seguro para subprocesos en Java?
Responder: Un código o clase seguro para subprocesos en Java es un código o clase que se puede compartir en un entorno de subprocesos múltiples o concurrente sin ningún problema y produce los resultados esperados.
P # 5) ¿Qué es la colección sincronizada en Java?
Responder: Una colección sincronizada es una colección segura para subprocesos. El método colección sincronizada () de la clase java.util.Collections devuelve una colección sincronizada (segura para subprocesos).
Conclusión
Con este tutorial, hemos completado el tema de subprocesos múltiples y concurrencia en Java. Hemos discutido el multihilo en detalle en nuestros tutoriales anteriores. Aquí, discutimos la concurrencia y la implementación relacionada con la concurrencia y el multihilo que son parte del paquete java.util.concurrent.
Discutimos dos métodos de sincronización más, semáforos y ReentrantLock. También discutimos el ForkJoinPool que se usa para ejecutar las tareas dividiéndolas en tareas más simples y, finalmente, uniendo el resultado.
El paquete java.util.concurrent también es compatible con el framework Executor y los ejecutores que nos ayudan a ejecutar hilos. También discutimos la implementación del grupo de subprocesos que consiste en subprocesos reutilizables que se devuelven al grupo cuando finaliza la ejecución.
Hablamos de otra interfaz similar a Runnable que también nos ayuda a devolver un resultado del hilo y el objeto Future utilizado para almacenar el resultado del hilo obtenido.
=> Tenga cuidado con la serie de capacitación simple de Java aquí.
Lectura recomendada
- Thread.Sleep () - Método Thread Sleep () en Java con ejemplos
- Implementación de Java: creación y ejecución de un archivo JAR de Java
- Conceptos básicos de Java: sintaxis de Java, clase de Java y conceptos básicos de Java
- Máquina virtual Java: cómo JVM ayuda a ejecutar aplicaciones Java
- Modificadores de acceso en Java: tutorial con ejemplos
- Java Synchronized: ¿Qué es la sincronización de subprocesos en Java?
- Tutorial de JAVA para principiantes: más de 100 tutoriales prácticos en vídeo de Java
- Clase Java Integer y Java BigInteger con ejemplos