¿Cómo implementar la concurrencia de subprocesos y disfrutar de recursos en instancias de productor y consumidor?
En aplicaciones del mundo real, hay muchas ocasiones en las que se necesitan varios subprocesos para acceder a los recursos en un orden determinado, como el clásico problema de productor y consumidor. La situación descrita por este tipo de problema es: suponiendo 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 y lo consume. Si no hay producto en el almacén, el productor puede colocar el producto en el almacén; de lo contrario, detendrá la producción y esperará a que los consumidores recojan el producto en el almacén. Si hay productos en el almacén, los consumidores pueden tomarlos y consumirlos; de lo contrario, dejan de consumir y esperan a que los productos se vuelvan a colocar en el almacén. Obviamente, se trata de un problema de sincronización. Los productores y los consumidores disfrutan de los mismos recursos. Además, los productores y los consumidores son interdependientes, se condicionan mutuamente y avanzan juntos. ¿Pero cómo escribir un programa para resolver este problema?
La idea tradicional es utilizar la verificación de bucles para determinar el orden en que avanzan los subprocesos comprobando repetidamente si condiciones específicas son verdaderas. Por ejemplo, una vez que el productor completa la producción, continuará usando la detección de bucle para determinar si los productos en el almacén han sido consumidos por el consumidor, y el consumidor también usará la detección de bucle inmediatamente después de completar el consumo para determinar si otros productos han sido consumidos. sido puesto en. almacén. Obviamente, estas operaciones consumen muchos recursos de 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, simplemente usar la palabra clave sincronizada no es suficiente. Porque aunque la palabra clave sincronizada evita actualizaciones simultáneas del mismo recurso disfrutado y logra la sincronización, no se puede utilizar para implementar el paso de mensajes entre subprocesos, también conocido como comunicación. Al abordar tales problemas, se debe seguir un principio: para los productores, se debe notificar a los consumidores que esperen antes de que los productores produzcan, para los consumidores se debe notificar a los consumidores inmediatamente después de que los consumidores produzcan; consume, se debe informar al productor que el consumo ha terminado y que la producción debe continuar, y que es necesario continuar con nuevos productos para el consumo.
De hecho, Java proporciona tres métodos muy importantes para resolver inteligentemente el problema de la comunicación entre subprocesos. Los tres métodos son: esperar(), notificar() y notificar a todos(). Ambos son métodos finales de clases de objetos, por lo que cada clase los tiene de forma predeterminada.
Aunque todas las clases tienen estos tres métodos de forma predeterminada, solo tiene sentido usar estos tres métodos en combinación en el contexto del uso de la palabra clave sincronizada y dentro del mismo problema de sincronización.
La sintaxis de estos métodos declarados en la clase de objeto es la siguiente:
final void wait() lanza InterruptedException
final void notify()
final void notifyAll( )
Entre ellos, llamar al método wait() hará que el hilo que llama al método libere el bloqueo en el recurso disfrutado y luego salga del estado de ejecución e ingrese a la cola de espera hasta que se despierte nuevamente. Llamar al método notify() activará el primer subproceso en la cola de espera del mismo recurso disfrutado y hará 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 en la cola de espera que esperan el mismo recurso salgan del estado ejecutable del estado de espera. En este momento, el subproceso con la mayor prioridad se ejecuta primero. Obviamente, el uso de estos métodos no requiere la detección de bucle del estado de los recursos compartidos, pero puede activar directamente el hilo en la cola de espera cuando sea necesario. Esto no sólo ahorra valiosos recursos de la CPU sino que también aumenta la eficiencia del programa.
Dado que se lanza InterruptedException al declarar el método wait(), debes colocarlo en un bloque try...catch al llamar al método wait().
Además, cuando utilice este método, también debe colocarlo en un bloque de código sincronizado; de lo contrario, se producirá la siguiente excepción:
"java.lang.IllegalMonitorStateException: el hilo actual no es propietario"
¿No son estos métodos suficientes para implementar la comunicación entre subprocesos?
this.c = c;
isProduced = true // Márcalo como producido
notify(); // Notifica a los consumidores que se ha producido y se puede consumir
}
public sincronizado char getShareChar() // Método sincronizado getShareChar() p>
{
if (!isProduced) // Si el producto aún no ha sido producido, el consumidor espera
{
probar p>
{
wait(); // El consumidor espera
} catch (tag notify(); // Notifica al consumidor que el producto ha sido producido y está listo para el consumo
}
} catch (flag notify(); // Notifica a los consumidores que el producto ha sido producido.
p>
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
isProduced = false; // Marcar como consumido
notify(); // Notificar que se debe producir
return this.c ;
}
}
clase Productor extiende Hilo // Hilo de Producto
{
private ShareData s ;
Productor( ShareData s)
{
this.s = s;
}
público void run()
{
for (char ch = 'A'; ch <= 'D'; ch++)
{
intentar<
{
Thread.sleep((int) (Math.random() * 3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
s.putShareChar(ch); // Poner el producto en el almacén
> System.out.println(ch + " es producido por el Productor.");
}
}
}
}
clase Consumidor extiende Hilo // Hilo de consumidor
{
private ShareData s;
Consumidor(ShareData s)
{
this.s = s;
}
public void run()
{
char ch;
hacer {
probar
{
Thread.sleep((int) (Math.random() * 3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
ch = s .getShareChar(); // Elimina el producto del almacén
System.out.println(ch + " es consumido por el Consumidor. ");
} while (ch ! = 'D');
}
}
clase CommunicationDemo
{
public static void main (String[] args)
{
ShareData s = new ShareData();
new Consumer(s).start();
nuevo producir
r(s).start();
}
}
El programa anterior demuestra que el productor produce los caracteres A, B, C y D, y el consumidor Después de consumir estos cuatro caracteres, los resultados del programa se muestran en la Figura 4. 6.1:
Figura 4.6.1 Ejemplo de un productor y un consumidor
Los resultados del programa muestran que aunque el hilo del consumidor se inicia primero en el método principal, porque no hay Para los productos, el hilo del consumidor llama al método wait() para ingresar a la cola de espera. Espera hasta que el hilo del productor produzca el producto y lo coloque en el almacén, y luego usa el método notify() para activarlo.
Dado que se especifica un cierto tiempo de inactividad en ambos subprocesos, también puede ocurrir la siguiente situación: el subproceso productor produce el producto y lo coloca en el almacén, y luego notifica al subproceso consumidor en la cola de espera, pero Debido a un tiempo de sueño demasiado prolongado, el hilo del consumidor no tiene intención de consumir el producto, y luego el hilo del productor quiere producir el siguiente producto, pero el producto no se consume en el almacén y luego usa el método notify () para despertarse. arriba. En este momento, el hilo productor quiere producir el siguiente producto. Por lo tanto, los productos en el almacén no se han consumido, por lo que el subproceso productor ejecuta el método wait () para ingresar a la cola de espera, espera a que el subproceso consumidor consuma los productos en el almacén y despierta el subproceso productor en la espera. cola a través del método notify(). Como puede ver, además de estar sincronizados entre sí, los dos hilos también deben comunicarse entre sí para poder 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, los productores pueden producir más de un producto al mismo tiempo y, siempre que el almacén sea lo suficientemente grande, pueden seguir produciéndolo. Los consumidores también pueden consumir varios productos a la vez hasta que no queden más productos en el almacén.
Sin embargo, solo se puede realizar una operación a la vez, ya sea producir productos o consumir productos del almacén. Obviamente, esto también es un problema de sincronización, pero en este problema, el recurso que disfruta *** es un grupo de recursos que puede acomodar 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 sobre cómo resolver este problema.
//Ejemplo 4.6.2 CommunicationDemo2.java
clase SyncStack // Clase de pila de sincronización, que puede acomodar múltiples datos al mismo tiempo
{ p >
private int index = 0; // El valor inicial del puntero de la pila es 0
private char[] buffer = new char[5] // La pila puede contener 5 caracteres
public sincronizado void push(char c) // Método sincronizado para ingresar a la pila
{
if (index == buffer.notify(); // Notificar a otros subprocesos que los datos se sacan de la pila
}
public sincronizado char pop() // Método sincronizado para sacar datos de la pila
{
if (index == 0) // No hay datos en la pila y no se pueden sacar
{
intentar
{
this. wait(); // Espera a que el hilo entrante coloque datos en la pila
} catch (InterruptedException e) {
}
}
this.notify(); // Notificar a otros subprocesos de los datos en la pila
index-- // Mover el puntero down
return buffer[index]; // Salida de datos
}
}
clase Productor implementa Runnable // Clase de productor
{
SyncStack s ; // Las letras generadas por la clase Productor se guardan en la pila de sincronización
public Producer( SyncStack s)
{
this.s = s;
}
public void run()
{
char ch;
for (int i = 0 ; i < 5; i++)
{
> probar
{ p>
Thread.sleep((int) (Math.random () * 1000));
} catch (InterruptedException e) {
}
ch = (char) (Math.random() * 26 + 'A').random() * 26 + 'A'); // Genera aleatoriamente 5 caracteres
s.push(ch) ); // Poner caracteres en la pila
System.out.println("Push " + ch + " in Stack" // Imprimir caracteres en la pila
}
}
}
clase Consumidor implementa Runnable // Clase de consumidor
{
SyncStack s; Los caracteres obtenidos por la clase Consumer provienen de SyncStack
public Consumer(SyncStack s)
{
this.s = s;
}
ejecución pública vacía()
{
char ch;
for (int i = 0; i < 5; i++)
{
probar
{
Thread.sleep((int) (Math.random() * 3000));
} catch (InterruptedException e) {
}
ch = s.pop(); // Leer caracteres de la pila
System.out .println("Pop " + ch + " de la pila " );//Imprimir caracteres en la pila
}
}
public static void main(String[] args)
{
SyncStack stack = new SyncStack();
// El siguiente objeto de clase Consumidor y el objeto de clase Productor operan en el mismo objeto SyncStack
Hilo t1 = nuevo Thread( new Producer(stack)); // Creación de instancias de thread
Thread t2 = new Thread(new Consumer(stack)); // Creación de instancias de thread
t2.start (); // Inicio del hilo
t1.start(); // Inicio del hilo
}
}
Este programa presenta la matriz de pila buffer [] se usa para simular el grupo de recursos, y tanto las clases de productor como de consumidor implementan la interfaz Runnable, y luego se crean dos subprocesos en el programa principal a través del método introducido anteriormente *** y comparten los mismos recursos de pila E intencionalmente. Inicie el hilo del consumidor primero y luego el hilo del productor. Al leer el Ejemplo 4.6.1, observe atentamente las similitudes y diferencias con este ejemplo para comprender la intención del autor. El resultado del programa se muestra en la Figura 4.6.2:
Figura 4.6.2 *** El problema de que los productores y consumidores disfruten del conjunto de recursos
Porque es una pila estructura, se ajusta al principio de último en entrar, 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 puede resolver este problema consultando información relevante. No proporcionaremos el código del programa aquí. como pensamiento para la práctica de los lectores.
Notas del 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 múltiples subprocesos, de modo que no sea necesario usar la detección de bucles en problemas de comunicación para esperar a que ocurra una condición, porque la detección de bucles es una gran pérdida de recursos de CPU y ciertamente no es ideal. situación. En el Ejemplo 4.6.1, para una mejor comunicación, se introduce especialmente un transmisor de señales para transmitir información. Usar señales para determinar si un hilo está esperando es sin duda una operación muy segura y vale la pena promoverla. Además, en el Ejemplo 4.6.2, el grupo de recursos se presenta como un recurso que disfruta ****. En este caso, el problema de cómo implementar la comunicación entre múltiples subprocesos se resuelve fácilmente. Con suerte, los lectores podrán aprender de este ejemplo y escribir programas que resuelvan problemas más complejos.
Consejos de expertos
Lo cierto es que el uso razonable de los métodos wait(), notify() y notifyAll() puede resolver bien el problema de la comunicación entre subprocesos. Sin embargo, también debemos comprender 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 sepa exactamente qué está haciendo cada hilo, es mejor usar notifyAll().
De hecho, JDK 1.5 introdujo un nuevo paquete: el paquete java.util.concurrent, un conjunto de herramientas de código abierto ampliamente utilizado que contiene varias herramientas de concurrencia útiles. Puede usarlo para reemplazar los métodos wait() y notify() y escribir su propio programador y bloqueos. Para obtener más información sobre esto, consulte la información relevante, que no se analizará en este libro.
Problemas relacionados
Java proporciona una variedad de flujos de entrada y salida (streams) para que los programadores puedan manipular datos fácilmente. Entre ellos, el flujo de tubería 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. Las tuberías se pueden utilizar para lograr la comunicación entre múltiples subprocesos. Entonces, ¿cómo se crean y utilizan canalizaciones?
Java proporciona dos clases especiales específicamente para pipes, son 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 final del hilo lee datos de la tubería; PipedOutputStream representa el extremo de entrada de los datos en la tubería, es decir. , el final del hilo escribe datos en la tubería. Ambas clases se pueden usar juntas para crear objetos de flujo de tubería para entrada y salida de datos.