Red de conocimiento informático - Material del sitio web - En el caso de productores y consumidores, ¿cómo lograr la concurrencia de subprocesos y el intercambio de recursos?

En el caso de productores y consumidores, ¿cómo lograr la concurrencia de subprocesos y el intercambio de recursos?

Resolver el problema

En aplicaciones prácticas, a menudo es necesario permitir que varios subprocesos accedan a * * * recursos en un orden determinado, como el clásico problema de productor y consumidor. Este tipo de problema describe una situación en la que se supone que solo se puede almacenar un producto en el almacén, el productor coloca el producto producido en el almacén y el consumidor saca el producto del almacén para su consumo. Si no hay productos en el almacén, el productor puede poner los productos en el almacén; de lo contrario, detener la producción y esperar a que los consumidores recojan los productos en el almacén. Si hay productos en el almacén, los consumidores pueden retirarlos para su consumo; de lo contrario, detienen el consumo y esperan a que los productos se almacenen nuevamente. Obviamente, se trata de un problema de sincronización. Los productores y los consumidores * * * comparten el mismo recurso. Los productores y los consumidores dependen unos de otros y se condicionan mutuamente para avanzar. ¿Pero cómo escribir un programa para resolver este problema?

La forma tradicional de pensar es utilizar la detección de bucles para determinar el orden de avance del hilo comprobando repetidamente si una condición específica es verdadera. Por ejemplo, una vez que se completa la producción, los productores continuarán usando la detección de ciclos para determinar si los productos en el almacén han sido consumidos por los consumidores. Los consumidores usarán la detección de ciclos inmediatamente después del consumo para determinar si los productos se almacenan nuevamente en el almacén. Obviamente, estas operaciones consumen mucha CPU y no vale la pena promocionarlas. Entonces, ¿existe una mejor manera de resolver este tipo de problema?

En primer lugar, cuando un hilo necesita esperar una condición antes de continuar con la ejecución, la palabra clave sincronizada por sí sola no es suficiente. Aunque la palabra clave sincronizada puede evitar actualizaciones simultáneas del mismo recurso compartido y lograr la sincronización, no se puede utilizar para implementar la transferencia de mensajes entre subprocesos. Esta transferencia se denomina comunicación. Al abordar tales problemas, se debe seguir un principio: para los productores, se debe informar a los consumidores que esperen antes de volver a producir, después de que los productores produzcan, se debe notificar a los consumidores inmediatamente para el consumo; Para finalizar, es necesario seguir produciendo nuevos productos para el consumo.

De hecho, Java proporciona tres métodos muy importantes para resolver inteligentemente el problema de comunicación entre subprocesos. Los tres métodos son: esperar(), notificar() y notificar a todos(). Son métodos finales de la clase Objeto, por lo que cada clase los tiene por defecto.

Aunque todas las clases tienen estos tres métodos de forma predeterminada, solo tienen significado práctico cuando se usan juntas en el mismo problema de sincronización dentro del alcance de la palabra clave sincronizada.

El formato de sintaxis de estos métodos declarados en la clase Objeto es el siguiente:

final void wait() lanza InterruptedException

Final void notification()

Notificación final no válida ()

Entre ellos, llamar al método wait() puede hacer que el hilo que llama al método libere el bloqueo * * * del recurso compartido y luego salga del estado de ejecución. y entre en la cola de espera hasta que se despierte nuevamente. Llamar al método notify() puede activar el primer subproceso que espera el mismo recurso en la cola de espera, de modo que el subproceso salga de la cola de espera y entre en un estado ejecutable. Llamar al método notifyAll () puede hacer que todos los subprocesos que esperan el mismo recurso en la cola de espera salgan del estado de espera y entren en el estado ejecutable. En este punto, el hilo con mayor prioridad se ejecutará primero. Obviamente, al utilizar estos métodos, no es necesario reciclar ni detectar el estado de los recursos **, y solo es necesario activar directamente los subprocesos en la cola de espera cuando sea necesario. Esto no sólo ahorra valiosos recursos de la CPU, sino que también mejora la eficiencia del programa.

Debido a que el método wait() se declara para lanzar InterruptedException cuando se declara, debe colocarse en el bloque de código try...catch cuando se llama al método wait(). Además, cuando utilice este método, debe colocarlo en un segmento de código sincronizado; de lo contrario, se producirá la siguiente excepción:

" Java. lang. ilegalmonitorstateexception: el hilo actual no es el propietario"

¿Pueden estos métodos lograr la comunicación entre subprocesos? A continuación se utilizará el modelo de sincronización de subprocesos múltiples para explicar cómo resolver el problema de comunicación entre subprocesos múltiples: el problema del productor y del consumidor.

Pasos específicos

El siguiente programa demuestra el proceso de implementación específico de la comunicación entre múltiples subprocesos. En el programa se utilizan cuatro clases, entre las cuales la clase ShareData se utiliza para definir * * * datos compartidos y métodos de sincronización. El método wait () y el método notify () se llaman en el método de sincronización para realizar la transmisión de mensajes entre subprocesos a través de semáforos.

//Ejemplo 4. 6. 1 Descripción de CommunicationDemo.java: El proceso de transferencia de mensajes entre productores y consumidores.

Categoría de datos compartidos

{

Carácter privado;

valor booleano privado isProduced = false//Semaphore

público sincronizado void putShareChar(char c)//Método sincronizado putShareChar()

{

If (isProduced) //Si el producto aún no se ha consumido, el productor lo hará esperar.

{

Probar

{

wait(); //El productor espera

} catch ( InterruptedException e) {

e . printstacktrace();

}

}

this.c = c

isProduced = true//La marca ha sido producida.

notify(); //Notifica a los consumidores que se ha producido y se puede comer.

}

caracter público sincronizado getShareChar()//Método sincronizado getShareChar()

{

If (!IsProduced) // Si el producto aún no se ha producido, los consumidores están esperando.

{

Probar

{

wait(); //El consumidor espera

} catch ( InterruptedException e) {

e . printstacktrace();

}

}

isProduced = false//La marca se ha consumido.

notify(); //Notifica que se requiere producción.

Volver a this.c

}

}

Hilo de extensión del generador de clases //Hilo del generador

{

Datos privados compartidos;

Productor (datos compartidos)

{

this.s = s

}

Ejecución de vacío público()

{

for(char ch = ' A '; ch & lt= ' Dch++)

{

Prueba

{

thread . >} catch (InterruptedException e) {

e . printstacktrace();

}

s . almacén

System.out.println(ch + "Generado por el productor.

);

}

}

}

Hilo de extensión de usuario de clase//Hilo de usuario

{

Datos privados compartidos;

Consumidor (datos compartidos)

{

this.s = s

}

Ejecución de vacío público()

{

char ch

hacer {

intentar

{

hilo . e . printstacktrace();

}

ch = s . getsharechar(); //Obtener el producto del almacén

println( ch + "Usado por los consumidores.);

} while (ch!= 'D');

}

}

Demostración de comunicación en el aula

{

Public static void main(String[] args)

{

ShareData s = new ShareData() ;

Nuevo consumidor. start();

Nuevo productor.

}

}

Lo anterior. El programa demuestra todo el proceso en el que los productores producen cuatro roles, A, B, C y D, y los consumidores consumen estos cuatro roles. Los resultados del programa se muestran en la Figura 4.6.1:

p>

Figura 4.6. 1 Ejemplo de productor y consumidor

Puedes ver en los resultados de ejecución del programa que, aunque el hilo del consumidor se inicia primero en el método principal, dado que no hay productos en el almacén, el hilo del consumidor llamará el método wait() para ingresar a la cola de espera y esperar hasta que el subproceso productor produzca el producto y lo coloque en el almacén, y luego se active usando el método notify()

Porque está especificado en ambos subprocesos Se requiere un tiempo de sueño específico, por lo que también puede suceder que el productor coloque el producto en el almacén y notifique al hilo del consumidor en la cola de espera. Sin embargo, debido al largo tiempo de sueño, el hilo del consumidor no tiene intención de consumirlo. producto en este momento se produce el siguiente producto, pero los productos en el almacén no se han consumido. Por lo tanto, el subproceso productor ejecuta el método wait() y espera en la cola de espera hasta que el subproceso consumidor consuma los productos en el almacén. se despierta a través del método notify () Esperando el subproceso productor en la cola. Se puede ver que además de mantener la sincronización, los dos subprocesos también deben comunicarse entre sí para avanzar.

En el programa anterior, los productores solo pueden producir un producto a la vez, y los consumidores solo pueden consumir un producto a la vez. En realidad, también hay situaciones en las que los productores pueden producir varios productos a la vez, siempre que la capacidad del almacén sea lo suficientemente grande, siempre se podrán producir. Los consumidores también pueden consumir varios productos a la vez hasta que no queden más productos en el almacén.

Sin embargo, sólo se permite una operación a la vez, independientemente de si el producto se produce o se consume desde el almacén. Obviamente, esto también es un problema de sincronización, pero en este problema, el recurso compartido es un grupo de recursos que puede almacenar múltiples recursos. A continuación se utiliza la estructura de la pila como ejemplo para proporcionar el código del programa para la comunicación de subprocesos para resolver este problema.

//Ejemplo 4. 6. 2 CommunicationDemo2.java

Clase SyncStack // La clase de pila sincronizada puede colocar varios datos a la vez.

{

private int index = 0; //El valor inicial del puntero de la pila es 0

private char[]buffer = new char[5] ;/ /la pila tiene espacio para 5 caracteres.

Sincronización pública void push(char c)//Método de sincronización de pila

{

If (index == buffer.length) //La pila está llena , Por lo que no se puede poner en la pila.

{

Prueba

{

this . wait(); // Espera a que el hilo saque los datos de la pila. .

} catch (InterruptedException e) {

}

}

Buffer [índice] = c // poner pila de datos<; /p>

index++; //Agrega 1 al puntero para reducir el espacio en la pila.

this . notify(); //Notifica a otros subprocesos para sacar datos de la pila.

}

Char pop sincronizado público() //Método de sincronización fuera de la pila

{

If (index == 0) // No hay datos en la pila, por lo que no se puede extraer.

{

Pruebe

{

esto . wait() //Esperando datos de la pila del hilo de la pila

} catch (InterruptedException e) {

}

}

this notify(); //Notificar otras pilas de subprocesos.

Index-; //El puntero se mueve hacia abajo.

Devolver buffer [index]; // Extraer datos de la pila

}

}

Se puede ejecutar la implementación del productor de clases / /Clase de productor

{

Pila de sincronización s; las letras generadas por la clase de productor se guardan en la pila de sincronización.

Generador público (pila sincronizada)

{

this.s = s

}

Vacío público run()

{

char ch

for(int I = 0;i<5;i++)

{

Prueba

{

thread . {

}

ch =(char)(math . random()* 26+' A '); // Genera aleatoriamente 5 caracteres

s . push(ch); //Pon los caracteres en la pila

system . println(" Push "+ch+" en la pila "); p>}

}

}

Clase de consumidor implementa ejecutable//clase de consumidor

{

Sincronización stack s; // Los caracteres obtenidos por la clase de consumidor provienen de la pila de sincronización.

Consumidor público (pila de sincronización)

{

this.s = s

}

Vacío público run()

{

char ch

for(int I = 0;i<5;i++)

{

Pruebe

{

thread . {

}

ch = s . pop();//Leer caracteres de la pila

system out . from Stack "); //Imprimir caracteres en la pila

}

}

}

Comunicación de clase pública Demostración 2

{

Public static void main(String[] args)

{

Pila SyncStack = new SyncStack();

//Los siguientes objetos de clase de usuario y objetos de clase de productor operan el mismo objeto de pila de sincronización.

Subproceso t1 = nuevo subproceso (nuevo productor (pila)); // Creación de instancias de subproceso

Subproceso t2 = nuevo subproceso (nuevo consumidor (pila) // Creación de instancias de subproceso

p>

T2 . start(); //El hilo comienza

t 1. start(); //El hilo comienza

}

}

El búfer de matriz de pila [] se introduce en el programa para simular el grupo de recursos. Tanto la clase productora como la clase consumidora implementan la interfaz Runnable. Luego, cree dos subprocesos que compartan el mismo recurso de pila en el programa principal mediante el método anterior e inicie intencionalmente primero el subproceso consumidor y luego el subproceso productor. Observe atentamente las similitudes y diferencias entre el Ejemplo 4.6.1 y este ejemplo cuando lea el programa para comprender la intención del autor. El resultado del programa se muestra en la Figura 4.6.2:

Figura 4.6.2 ***Problemas de productores y consumidores del grupo de recursos

Debido a que es una estructura de pila, se ajusta al principio de "el último en entrar, el primero en salir". Los lectores interesados ​​también pueden utilizar una estructura de cola que se ajuste al principio de primero en entrar, primero en salir para simular el proceso de comunicación entre subprocesos. Creo que este problema se puede resolver consultando información relevante. El código del programa no se proporciona aquí como una pregunta reflexiva para que los lectores practiquen.

Explicación de un experto

Esta sección presenta tres métodos importantes: esperar(), notificar() y notificarTodos(). Se pueden usar para completar de manera eficiente problemas de comunicación entre subprocesos múltiples, por lo que no es necesario usar la detección de bucle para esperar a que ocurra una determinada condición, porque este método es una gran pérdida de recursos de la CPU y, por supuesto, esta situación no lo es. se espera que ocurra. En el Ejemplo 4.6.1, para una mejor comunicación, se introduce un semáforo utilizado específicamente para transmitir información. Usar un semáforo para decidir si un hilo está esperando es sin duda una operación muy segura y merece ser promovida. Además, se introduce un grupo de recursos como un * * * recurso compartido en el Ejemplo 4.6.2, lo que resuelve cómo implementar la comunicación entre múltiples subprocesos en este caso. Espero que los lectores puedan sacar inferencias de un ejemplo y escribir programas para resolver problemas más complejos.

Opinión de expertos

Lo cierto es que el uso razonable de los métodos wait(), notify() y notifyAll() puede resolver el problema de la comunicación entre subprocesos. Sin embargo, también debe entenderse que estos métodos son los componentes básicos de códigos de bloqueo, colas y concurrencia más complejos. En particular, usar notify() en lugar de notifyAll() es arriesgado. A menos que sepas realmente qué está haciendo cada hilo, es mejor usar notifyAll(). De hecho, se introdujo un nuevo paquete java.util.concurrent en JDK 1.5, que es una caja de herramientas de código abierto ampliamente utilizada con útiles utilidades de concurrencia.

Puede reemplazar completamente los métodos esperar () y notificar () para escribir su propio programador y bloquear. La información relevante se puede encontrar en materiales relacionados y no se repetirá en este libro.

Problemas relacionados

Java proporciona una variedad de flujos, lo que permite a los programadores manipular datos fácilmente. Entre ellos, el flujo de canalización es un flujo especial que se utiliza para transmitir datos directamente entre diferentes subprocesos. Un hilo envía datos al tubo de salida y otro hilo lee datos del tubo de entrada. Mediante el uso de tuberías, se puede lograr la comunicación entre múltiples subprocesos. Entonces, ¿cómo se crean y utilizan canalizaciones?

Java proporciona dos clases especiales diseñadas para manejar canalizaciones, a saber, la clase PipedInputStream y la clase PipedOutputStream.

Entre ellos, PipedInputStream representa el extremo de salida de los datos en la tubería, es decir, el extremo donde el hilo lee los datos de la tubería; PipedOutputStream representa el extremo de entrada de los datos en la tubería, es decir. , el final donde el hilo escribe datos en la tubería. Juntas, estas dos clases crean un objeto de flujo de canalización para la entrada y salida de datos.