Programación Concurrente en Java: Conceptos, Implementación y Mejores Prácticas

Enviado por Programa Chuletas y clasificado en Informática y Telecomunicaciones

Escrito el en español con un tamaño de 145,24 KB

En la programación ordinaria, con procesos secuenciales, hay problemas que no se pueden resolver adecuadamente:

  • Aprovechar la capacidad de todos los procesadores disponibles.
  • Conseguir un reducido tiempo de respuesta.
  • Descomponer en partes y resolverlas independientemente.
  • Permitir la interacción con interfaces gráficas y al mismo tiempo realizar cálculos.

La concurrencia es la manera de hacer que se pueda realizar más de una cosa al mismo tiempo.

Concurrencia

Los usuarios de computadoras dan por sentado que sus sistemas pueden hacer más de una cosa a la vez. Ellos asumen que pueden seguir trabajando en un procesador de texto, mientras que otras aplicaciones descargan archivos, gestionan la cola de impresión, y el audio stream. A menudo se espera incluso que una sola aplicación haga más de una cosa a la vez. Por ejemplo, que el streaming de aplicaciones de audio debe leer simultáneamente el audio digital de la red, descomprimirlo, gestionar la reproducción, y actualizar su pantalla. Incluso el procesador de texto siempre debe estar preparado para responder a eventos de teclado y ratón, no importa lo ocupado que está el reformateo de texto o actualizar la pantalla. El software que puede hacer este tipo de cosas es conocido como software concurrente.

Proceso secuencial: Es la ejecución de un conjunto de instrucciones una tras otra, desde la primera hasta la última, la traza es lineal (Tiene un solo hilo (thread) de ejecución).

Proceso concurrente: colección de procesos secuenciales autónomos que se ejecutan en paralelo (al menos de manera lógica).

Se dice que dos procesos son concurrentes si el inicio de uno es posterior al inicio del otro y al mismo tiempo anterior a su finalización.

Características:

  • Procesos disjuntos: dos procesos se denominan disjuntos si no comparten datos.
  • Procesos independientes: son procesos que no se comunican entre sí en ningún caso.
  • Procesos no cooperantes: dos procesos se denominan no cooperantes si son concurrentes y disjuntos.

Procesos cooperantes: procesos que buscan la solución a un problema en común.

  • Regularmente se comunican y se sincronizan.
  • En definitiva, si comparten información.
  • Procesos competentes: son procesos que quieren conseguir el control de los recursos del sistema.
  • Necesariamente requieren la comunicación y la sincronización entre ellos.
  • De todas maneras, los procesos son esencialmente independientes (no tienen un objetivo común).

Especificación de la concurrencia

  • Mediante herencia: Thread
    • Creando una clase derivada.
  • Mediante interface: Runnable
    • Implementando la interface y utilizando una instancia de Thread anónima o no.

Especificación de la concurrencia: Crear una instancia o implementar la interface NO provoca la ejecución concurrente.

  • Para generar una ejecución concurrente es necesario generar el proceso:
    • Invocando el método start() definido en Thread.
    • Si no, se tiene un objeto normal.

El proceso acaba cuando acaba la ejecución de run.

  • Sigue existiendo el objeto.
    • Se pueden enviar mensajes como con cualquier otro objeto.
    • Ya no es un proceso.

Comportamiento de los procesos:

  • Con Java existen dos tipos de procesos:
    • Procesos de usuario (user thread).
    • Procesos de servicio (daemon thread).
  • Por defecto, un proceso se crea en la categoría de proceso de usuario.
  • Si se quiere un proceso de servicio se debe indicar explícitamente.
  • La máquina virtual acaba si solo existen procesos de servicio.

Creación de un proceso de servicio

  • Antes de la activación:

Thread th = new Thread(...);

th.setDaemon(true);

th.start();

  • Respecto de la iniciación el proceso que genera no espera al generado.
  • Respecto de la finalización, se puede esperar si se indica explícitamente.

Un proceso puede renunciar al procesador

  • Si hay otro proceso ejecutable es posible que pueda ser escogido para pasar a ejecución.

class Proces extends Thread {

...

public void run() {

...

if (aconsejable aturar)

this.yield();

...

}

Prioridades

  • La prioridad es la manera de definir la preferencia en la ejecución de algunos procesos frente a los otros.
  • Teóricamente, un proceso de una determinada prioridad en estado ejecutable no puede ser escogido para pasar a ejecución si existe otro proceso también ejecutable con una prioridad mayor.

En Java las prioridades se definen dentro de un rango:

  • Máxima prioridad (MAX_PRIORITY): 10
  • Mínima prioridad (MIN_PRIORITY): 1
  • Prioridad normal (NORM_PRIORITY): 5
  • Se establecen: public final void setPriority(int nuevaPrioridad)
  • Se consultan: public final int getPriority()

Es perfectamente posible no modificar la prioridad de los threads.

  • Todos los threads son iguales para poder ser escogidos.

Peligros de la concurrencia

La información compartida por los diferentes componentes debe ser coherente.

Atomicidad

  • Atomicidad es la propiedad que asegura que una operación no se puede realizar parcialmente.
    • O se hace completamente o no se hace en absoluto.
  • Si la información que se trata es mayor que un registro de procesador no se puede garantizar que la modificación de los datos sea atómica.
    • Se debe añadir código para asegurar que todo es correcto.

Volatilidad

  • Si un valor es consultado se desplaza hacia la memoria CACHE.
  • Si un valor es consultado por más de un proceso y los procesos se ejecutan en más de un procesador, el valor se replicará en las diferentes memorias CACHE.
    • ¿Qué pasa si uno de los procesos modifica el valor?

Es posible impedir que un valor se almacene en memorias CACHE.

  • Modificador volatile ámbito volatile tipo nombre: public volatile int contador = 0;
  • Pero esto no es útil en todos los casos.
    • Lectura-Actualización-Escritura no es atómico.
    • La modificación dependiente de condiciones.
  • Sería necesario bloquear los datos a manipular.
    • El proceso que quiere acceder, previamente debe adquirir el derecho a hacerlo.
    • Si un proceso tiene derecho a acceder, ningún otro puede hacerlo.

Comunicación entre Procesos

  • Las dadas de los procesos forman dos conjuntos:
    • El conjunto de lectura: los datos consultados.
    • El conjunto de escritura: los datos modificados.

Problema: Regiones Críticas

Sincronización

  • La sincronización consiste en establecer mecanismos que permitan la ejecución coordinada entre procesos.
  • Cuando un proceso llega a un determinado punto de su ejecución debe esperar que otro llegue a otro punto de la suya.
    • Y mientras esto no pase debe esperar.

La sincronización se establece sobre los datos.

  • Por tanto, se realiza sobre los métodos definidos en Object.

Lo que suministra Object:

  • wait(): El thread espera.
  • notify(): Un thread se despierta.
  • notifyAll(): Todos despiertan.
  • ¿Qué pasa con las excepciones? Se deben gestionar.

Para poder acceder a los métodos de sincronización se debe adquirir la propiedad del objeto.

También puede interesar seguir un determinado ritmo.

  • marcado por algún tipo de reloj.

Espera temporizada

  • Las esperas pueden ser:
    • Incondicionales: public void wait()
    • Condicionales:

      public void wait(long millis)

      public void wait(long millis, int nanos)

Excepciones

  • IllegalArgumentException
    • millis
    • nanos [0, 999999]
  • IllegalMonitorStateException
    • El Thread no es el propietario del objeto.
  • InterruptedException
    • Otro Thread ha interrumpido la espera.

¿Qué se debe tener en cuenta?

  • Los productores: producen.
  • Los consumidores: consumen.
  • El lugar donde los productos pasan de productores a consumidores.
    • Que tiene una capacidad de almacenamiento finita.

Declaración de procesos en Java

En Java los procesos se definen como objetos de clases que pueden ser declaradas de dos maneras:

  • Clases derivadas de la clase Thread (definida dentro de java.lang).
  • Clases que implementan la interface Runnable (definida dentro de java.lang).

Clase Thread

  • Implementa la funcionalidad de un proceso.

public class Thread implements Runnable {

...

public void run() {}

...

}

  • Se trata de sobrecargar el método run.
    • Que viene a ser el programa principal del proceso.

La clase Thread suministra una gran cantidad de métodos. De entre todos, hay dos especialmente destacados por su importancia.

  • método run(): este método se debe sobrecargar en la clase derivada, debe contener el código 'principal' de ejecución del thread. Este método no debe ser invocado directamente.
  • método start(): este método debe ser invocado para iniciar la ejecución del thread. Este método invocará a su vez el método run() del thread. A partir del momento en que esto pasa habrá dos 'procesos' ejecutándose en la Máquina Virtual Java.

Ejemplo de una clase derivada:

class FilExecucio extends Thread {

// Definición atributos de la clase

// Definición métodos de la clase

public void run() {

// Código 'principal' de la clase (y por tanto del thread)

}

// más cosas

}

class Principal {

// Declaraciones y métodos de la clase

public static void main() {

FilExecucio fil = new FilExecucio();

fil.start(); // a partir de ahora hay dos threads

// más cosas

}

}

La ejecución de threads está ligada a la excepción InterruptedException.

try {

// instrucciones sensibles

}

catch(InterruptedException exc) {

// instrucciones de gestión de la excepción

}

Interface Runnable

  • Especificación de un método.

public interface Runnable {

public void run();

}

  • Por tanto, se trata de implementar este método.
    • Que viene a ser el programa principal del proceso.

La interface Runnable también contiene una gran cantidad de métodos útiles. De entre todos, hay que destacar el método abstracto run() clase que se defina deberá implementar este método para poder funcionar correctamente.

Ejemplo de una clase derivada:

class SubPrograma implements Runnable {

// Definición atributos de la clase

// Definición métodos de la clase

public void run() {

// Código 'principal' de la clase (y por tanto del thread)

// Es una sobrecarga del método abstracto run() de Runnable

}

// más cosas

}

class Principal {

// Declaraciones y métodos de la clase

// dentro de algún método de la clase:

public static void main() {

Runnable fil = new SubPrograma();

Thread thr = new Thread(fil);

thr.start(); // a partir de ahora hay dos threads

// más cosas

}

}

Sincronización entre procesos

  • Exclusión Mutua: establecer zonas donde en cada instante de tiempo solo puede acceder un thread.
  • Se debe indicar un objeto sobre el que se desea este comportamiento.
    • Y asociar un bloque de código: synchronized (Object) { … }
  • Se puede definir un método syncronized.
    • El objeto de sincronización será la instancia propietaria del método.
    • El código de exclusión mutua es el código del método.
  • Un método puede servir para enviar o recibir información de un proceso.
    • Es decir, para que los procesos puedan comunicarse.

La sincronización se establece a nivel de objeto

  • ¡Los métodos y atributos de clase no se ven afectados!
  • Pero se pueden establecer sincronizaciones a nivel de clase.
  • Los bloques de exclusión mutua no tienen por qué formar parte de los Procesos.
    • Son entidades pasivas (estructuras de datos) modificadas por las entidades activas (procesos).

Exclusión Mutua

En Java existe una forma sencilla de implementar la exclusión mutua sobre métodos o bloques de código. El modificador de método sinchronized garantiza que el acceso a un método o bloque será exclusivo.

public synchronized void prueba () {

// instrucciones sensibles

}

Object lock = new Object();

synchronized (lock) {

// instrucciones sensibles

}

Espera no activa

Los mecanismos de Java para forzar una espera son una espera incondicional con sleep() (método estático de la clase Thread) y wait()/notify()/notifyAll(), disponibles a través de java.lang.Object.

Imagen

En la programación concurrente, hay dos unidades básicas de ejecución: los procesos y los hilos. En el lenguaje de programación Java, la programación concurrente se refiere principalmente a los hilos. Sin embargo, los procesos también son importantes.

Un sistema informático normalmente tiene muchos procesos activos e hilos. Esto es cierto incluso en sistemas que solo tienen un único núcleo de ejecución, por lo que solo tienen un hilo ejecutando realmente en un momento dado. El tiempo de procesamiento para un solo núcleo se comparte entre procesos y subprocesos a través de una característica del sistema operativo llamado segmentación de tiempo.

Se está volviendo cada vez más común para los sistemas informáticos tener varios procesadores o procesadores con múltiples núcleos de ejecución. Esto mejora enormemente la capacidad de un sistema para la ejecución concurrente de procesos y subprocesos - pero la concurrencia es posible, incluso en sistemas simples, sin varios procesadores o núcleos de ejecución.

Procesos

Un proceso tiene un entorno de ejecución autónomo. Un proceso en general, tiene un juego completo y privado de recursos básicos de tiempo de ejecución; en particular, cada proceso tiene su propio espacio de memoria.

Los procesos se ven a menudo como sinónimo de programas o aplicaciones. Sin embargo, lo que el usuario ve como una sola aplicación puede ser en realidad un conjunto de procesos que cooperan. Para facilitar la comunicación entre procesos, la mayoría de los sistemas operativos son compatibles con Inter Process Communication recursos (IPC), tales como tuberías y tomas de corriente. IPC se utiliza no solo para la comunicación entre procesos en el mismo sistema, sino para diferentes sistemas.

La mayoría de las implementaciones de la máquina virtual de Java se ejecutan como un solo proceso. Una aplicación Java puede crear procesos adicionales utilizando un objeto ProcessBuilder.

Hilos

Los hilos a veces se llaman procesos ligeros. Ambos procesos y subprocesos proporcionan un entorno de ejecución, pero la creación de un nuevo hilo requiere menos recursos que la creación de un nuevo proceso.

Los hilos existen dentro de un proceso - Cada proceso tiene al menos uno. Los hilos comparten los recursos del proceso, incluyendo la memoria y los archivos abiertos. Esto provoca una eficiente, pero potencialmente problemática, comunicación.

La ejecución multiproceso es una característica esencial de la plataforma Java. Cada aplicación tiene al menos un hilo - o varios, si se cuentan las discusiones del "sistema" que hacen cosas como la gestión de la memoria y el manejo de señales. Pero desde el punto de vista del programador de la aplicación, se inicia con un solo hilo, llamado el hilo principal. Esta discusión tiene la capacidad de crear subprocesos adicionales, como demostraremos en la siguiente sección.

Cada hilo se asocia con una instancia de la clase Thread. Hay dos estrategias básicas para el uso objetos Thread para crear una aplicación concurrente.

  • Para controlar directamente la creación y gestión de hilos, simplemente instancia Thread cada vez que la aplicación necesite iniciar una tarea asincrónica.
  • Para la gestión de hilo abstracto del resto de su aplicacion, pasar las tareas de la aplicación a un executor.

Una aplicación que crea una instancia de Thread debe proporcionar el código que se ejecutará en ese hilo. Hay dos maneras de hacer esto:

  • Proporcionar un objeto Runnable. La interfaz Runnable define un método único, run, destinado a contener el código que se ejecuta en el hilo. El objeto Runnable se pasa al constructor, como en el HelloRunnable ejemplo:
public class HelloRunnable implements Runnable {

    public void run () {
        System.out.println ("Hola de un hilo!");
    }

    public static void main (String args []) {
        (New Thread (nuevo HelloRunnable ())) start ().;
    }

}
  • Subclase Thread. La propia clase Thread implementa Runnable, aunque su método run no hace nada. Una aplicación puede crear una subclase de Thread, proporcionando su propia implementación de run, como en el HelloThread ejemplo:
public class HelloThread extends Thread {

    public void run () {
        System.out.println ("Hola de un hilo!");
    }

    public static void main (String args []) {
        (Nuevo HelloThread ()) start ().;
    }

}

¿Cuál de estas expresiones se debe utilizar? El primer idioma, que emplea un Runnable objeto, es más general, ya que el Runnable objeto puede crear una subclase de una clase distinta de Thread. El segundo idioma es más fácil de usar en aplicaciones simples, pero está limitada por el hecho de que la clase de tarea debe ser un descendiente de Thread. Esta lección se centra en la primera aproximación, que separa el Runnable tarea del Thread objeto que ejecuta la tarea. No solo es este enfoque más flexible, pero es aplicable a las API de gestión de hilo de alto nivel cubiertos después. La clase Thread define una serie de métodos útiles para la gestión de hilo. Estos incluyen estáticas métodos, que proporcionan información sobre, o afectan el estado del thread, invocando el método.Los otros métodos se invocan desde otros hilos que intervienen en la gestión del proceso y Thread objeto.

Pausa de Ejecución con el sueño

Thread.sleep hace que el hilo actual de suspender la ejecución por un período determinado. Este es un medio eficaz de hacer que el tiempo de procesador disponible para los demás hilos de una aplicación o de otras aplicaciones que podrían estar ejecutándose en un sistema informático. El sleep método también se puede utilizar para la estimulación, como se muestra en el ejemplo que sigue, y en espera de otro hilo con funciones que se entiende que tienen requisitos de tiempo, como con el SimpleThreads ejemplo, en una sección posterior.

Dos versiones sobrecargadas de sleep están disponibles: uno que especifica el tiempo de sueño a la milésima de segundo y uno que especifica el tiempo de sueño al nanosegundo. Sin embargo, estos tiempos de sueño no se garantiza que sea precisa, ya que están limitados por los recursos proporcionados por el sistema operativo subyacente. Además, el período de sueño puede ser denunciado por las interrupciones, como veremos en una sección posterior. En cualquier caso, no se puede suponer que la invocación de sleep suspenderá el hilo precisamente por el período de tiempo especificado.

public class SleepMessages {
    public static void main(String args[])
        throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };

        for (int i = 0;
             i 
Observe que principal declara que lanza InterruptedException. Se trata de una excepción que el sueño lanza cuando otro hilo interrumpe el hilo actual, mientras que el sueño está activo. Desde esta aplicación no ha definido otro hilo para causar la interrupción, no se molestó en ponerse InterruptedException.

Una interrupción es una indicación de un hilo que debería dejar de hacer lo que está haciendo y hacer otra cosa. Es responsabilidad del programador decidir exactamente cómo un hilo responde a una interrupción, pero es muy común que el hilo termine. Un hilo envía una interrupción mediante la invocación de interrupción en el Thread objeto para el hilo que se interrumpió. Para que el mecanismo de interrupción funcione correctamente, el hilo interrumpido debe ser compatible con su propia interrupción.

¿Cómo soporta un hilo de su propia interrupción? Esto depende de lo que está haciendo actualmente. Si el hilo está frecuentemente invocando métodos que arrojan InterruptedException, simplemente regresa de la run método después de que se capture esta excepción. Por ejemplo, supongamos que el bucle central mensaje en el SleepMessages ejemplo estaban en el run de método de un hilo Runnable objeto. Entonces podría ser modificado de la siguiente manera para apoyar las interrupciones:

for (int i = 0; i 

Many methods that throw InterruptedException, such as sleep, are designed to cancel their current operation and return immediately when an interrupt is received.

What if a thread goes a long time without invoking a method that throws InterruptedException? Then it must periodically invoke Thread.interrupted, which returns true if an interrupt has been received. For example:

for (int i = 0; i 

In this simple example, the code simply tests for the interrupt and exits the thread if one has been received. In more complex applications, it might make more sense to throw an InterruptedException:

if (Thread.interrupted()) {
    throw new InterruptedException();
}

This allows interrupt handling code to be centralized in a catch clause.

El Estado de la bandera de interrupción

El mecanismo de interrupción se implementa utilizando un indicador interno conocido como el estado de interrupción. Invocación Thread.interrupt establece este indicador. Cuando un hilo cheques por una interrupción invocando el método estático Thread.interrupted, estado de alarma se borra. El no estático isInterrupted método, que es utilizado por un hilo para consultar el estado de alarma del otro, no cambia el indicador de estado de interrupción.

Por convención, cualquier método que sale lanzando un InterruptedException despeja estado de alarma cuando lo hace. Sin embargo, siempre es posible que el estado de interrupción de inmediato se vuelve a activar, por otra invocación hilo de interrupción.

The join method allows one thread to wait for the completion of another. If t is a Thread object whose thread is currently executing,

t.join();

causes the current thread to pause execution until t's thread terminates. Overloads of join allow the programmer to specify a waiting period. However, as with sleep, join is dependent on the OS for timing, so you should not assume that join will wait exactly as long as you specify.

Like sleep, join responds to an interrupt by exiting with an InterruptedException.

Hilos comunican principalmente mediante el intercambio de acceso a los campos y los objetos de referencia se refieren a los campos. Esta forma de comunicación es muy eficiente, pero hace dos tipos de errores posibles: interferencia hilo(La interferencia ocurre cuando dos operaciones, que se ejecuta en diferentes hilos, pero actuando en los mismos datos, intercalar. Esto significa que las dos operaciones consisten en múltiples pasos, y las secuencias de pasos se superponen.) y errores de coherencia de memoria(Errores de coherencia de memoria ocurrir cuando diferentes hilos tienen vistas inconsistentes de cuáles deben ser los mismos datos. Las causas de errores de coherencia de memoria son complejos y más allá del alcance de este tutorial. Afortunadamente, el programador no necesita un conocimiento detallado de estas causas. Todo lo que se necesita es una estrategia para evitarlos.). La herramienta necesaria para evitar estos errores es la sincronización.

Sin embargo, la sincronización puede introducir la discordia hilo, que se produce cuando dos o más subprocesos intentan acceder al mismo recurso al mismo tiempo y hacer que el tiempo de ejecución de Java para ejecutar uno o más hilos más lentamente, o incluso suspender su ejecución. El hambre y livelock son formas de contención hilo . Vea la sección Liveness para más información.

Ya hemos visto dos acciones que crean relaciones que sucede-antes.

  • Cuando una sentencia invoca hilo.start, cada declaración que tiene un pasa-antes de relación con esa declaración también tiene un pase, antes de la relación con cada sentencia ejecutada por el nuevo hilo. Los efectos del código que condujo a la creación del nuevo hilo son visibles para el nuevo hilo.
  • Cuando un subproceso termina y causa una Thread.join en otro hilo devolver y todas las declaraciones realizadas por el subproceso finalizado tienen un pase, antes de la relación con todas las declaraciones después de la exitosa combinación. Los efectos del código en el hilo ahora son visibles para el hilo que realiza la unión.

En programación, un atómica acción es uno que efectivamente sucede todos a la vez. Una acción atómica no puede detenerse a la mitad: tampoco pasa por completo, o no ocurrir en absoluto. No hay efectos secundarios de una acción atómica son visibles hasta que la acción se ha completado.

Ya hemos visto que una expresión de la subasta, como c ++ , no describe una acción atómica. Incluso expresiones muy simples pueden definir acciones complejas que se pueden descomponer en otras acciones. Sin embargo, hay acciones que puede especificar que son atómica:

Lee y escribe son atómicos para las variables de referencia y para las variables más primitivas (todos los modelos excepto el largo y doble ).

Lee y escribe son atómicas para todas las variables declaradas volátil ( incluyendo largas y dobles de variables).

Acciones atómicas no pueden ser intercalados, por lo que se pueden utilizar sin temor de interferencia de rosca. Sin embargo, esto no elimina toda la necesidad de sincronizar acciones atómicas, porque los errores de consistencia de memoria siguen siendo posibles. Usando volátiles variables de reduce el riesgo de errores de coherencia de la memoria, porque cualquier escritura a una volátil establece una variable de sucede antes relación con la posterior lee de la misma variable. Esto significa que los cambios en una volátil variables son siempre visibles a otros hilos. Lo que es más, también significa que cuando un hilo lee una volátil variable, se ve no sólo el último cambio en la volatilidad , sino también los efectos secundarios de un código que conducía el cambio.


Uso sencillo acceso a variables atómica es más eficiente que el acceso a estas variables a través de código sincronizado, pero requiere más cuidado por el programador para evitar errores de coherencia de memoria. Si el esfuerzo adicional es que vale la pena depende del tamaño y la complejidad de la aplicación.

luego hacer estos métodos sincronizados tiene dos efectos:

En primer lugar, no es posible que dos invocaciones de métodos sincronizados en el mismo objeto para intercalar. Cuando un hilo está ejecutando un método sincronizado para un objeto, todos los otros hilos que invocar métodos sincronizados para el mismo bloque de objeto (suspender la ejecución) hasta el primer hilo se realiza con el objeto.

En segundo lugar, cuando un método sincronizado sale, se establece automáticamente una relación que sucede antes con cualquier invocación subsiguiente de un método sincronizado para el mismo objeto. Esto garantiza que los cambios en el estado del objeto son visibles para todos los temas.

Tenga en cuenta que los constructores no pueden sincronizarse - utilizando el sincronizado de palabras clave con un constructor es un error de sintaxis. Sincronización de los constructores no tiene sentido, ya que sólo el hilo que crea un objeto debe tener acceso a ella mientras se está construyendo.

Métodos sincronizados permiten una simple estrategia para prevenir la interferencia de rosca y errores de coherencia de memoria: si un objeto es visible a más de un hilo, todo lee o escribe a las variables de ese objeto se realiza a través de sincronizados métodos. (Una excepción importante: finales campos, que no pueden ser modificados después de construir el objeto, se pueden leer de forma segura a través de métodos no sincronizados, una vez que se construye el objeto) Esta estrategia es efectiva, pero pueden presentar problemas con la vida de la conexión , como veremos ver más adelante en esta lección.

La capacidad de una aplicación concurrente para ejecutar en tiempo y forma se conoce como su vivacidad .


Punto muerto

Deadlock describe una situación en la que dos o más hilos se bloquean para siempre, esperando a que sí. He aquí un ejemplo.

Alphonse y Gaston son amigos y grandes creyentes en cortesía. Una regla estricta de la cortesía es que cuando te inclinas a un amigo, usted debe permanecer inclinó hasta que su amigo tiene la oportunidad de volver la proa. Por desgracia, esta regla no tiene en cuenta la posibilidad de que dos amigos podrían saludarán entre sí al mismo tiempo. Esta aplicación ejemplo, estancamiento , los modelos de esta posibilidad:

public class Deadlock {
    clase estática amigo {
        String nombre final privado;
        amigo pública (String nombre) {
            this.name = nombre;
        }
        public String getName () {
            volver this.name;
        }
        sincronizada public void arco (amigo enramada) {
            System.out.format ("% s:% s"
                + "Se ha plegado a mí!% N", 
                this.name, bower.getName ());
            bower.bowBack (this);
        }
        public void sincronizada bowBack (amigo enramada) {
            System.out.format ("% s:% s"
                + "Se ha plegado de nuevo a mí!% N",
                this.name, bower.getName ());
        }
    }

    main (String [] args) {static void públicos
        amigo definitiva alfonso =
            nuevo amigo ("Alfonso");
        amigo definitiva gaston =
            nuevo amigo ("Gaston");
        nuevo hilo (nuevo Ejecutable () {
            public void run () {alphonse.bow (gaston); }
        .}) Start ();
        nuevo hilo (nuevo Ejecutable () {
            public void run () {gaston.bow (alfonso); }
        .}) Start ();
    }
}

Cuando Deadlock corre, es muy probable que ambos hilos bloquearán cuando intentan invocar bowBack . Ni bloque nunca acabar, ya que cada hilo está esperando que el otro para salir del arco .


El hambre y livelock son tanto un problema menos común que el punto muerto, pero siguen siendo problemas que puede enfrentarse, cada diseñador de software concurrente.

Inanición

El hambre se describe una situación en la que un hilo no es capaz de tener acceso regular a los recursos compartidos y es incapaz de progresar. Esto ocurre cuando los recursos compartidos se hacen disponibles durante largos períodos por hilos "codiciosos". Por ejemplo, supongamos que un objeto proporciona un método sincronizado que a menudo toma mucho tiempo para volver. Si un hilo invoca este método con frecuencia, otros hilos que también necesitan el acceso sincronizado frecuente al mismo objeto a menudo serán bloqueados.

Livelock

Un hilo a menudo actúa en respuesta a la acción de otro hilo. Si la acción del otro hilo es también una respuesta a la acción de otro tema, entonces livelock puede resultar. Al igual que en punto muerto, hilos livelocked son incapaces de seguir avanzando. Sin embargo, los hilos no están bloqueados - son simplemente demasiado ocupados respondiendo a la otra para reanudar el trabajo. Esto es comparable a dos personas que intentan pasar uno al otro en un pasillo: Alphonse mueve hacia la izquierda para dejar pasar Gaston, mientras que Gastón se mueve hacia la derecha para dejar Alphonse pasar. Al ver que todavía están bloqueando entre sí, Alphone mueve hacia la derecha, mientras que Gastón se mueve hacia la izquierda. Todavía están bloqueando entre sí, así que ...

Entradas relacionadas: