[JavaEE] Cola de bloqueo de casos de subprocesos múltiples

Insertar descripción de la imagen aquí

1. Introducción

BlockingQueue es una cola que admite dos operaciones adicionales. Las dos operaciones adicionales son:

  • Cuando la cola está vacía, el hilo que obtiene el elemento esperará a que la cola deje de estar vacía.
  • Cuando la cola está llena, el hilo que almacena el elemento espera a que la cola esté disponible.

Las colas de bloqueo se utilizan a menudo en escenarios de productores y consumidores: el productor es el hilo que agrega elementos a la cola y el consumidor es el hilo que toma elementos de la cola. La cola de bloqueo es un contenedor donde el productor almacena elementos y el consumidor solo toma elementos del contenedor.

2. ¿Qué es el modelo productor-consumidor?

El modelo productor-consumidor es un modelo de colaboración concurrente de subprocesos múltiples, que consta de dos tipos de subprocesos y un búfer: el subproceso productor produce datos y los coloca en el búfer, y el subproceso consumidor toma datos del búfer y los consume. Los productores y consumidores comparten el mismo espacio de almacenamiento dentro del mismo período de tiempo: los productores agregan productos al espacio de almacenamiento y los consumidores toman productos del espacio de almacenamiento. Cuando el espacio de almacenamiento está vacío, el consumidor bloquea; cuando el espacio de almacenamiento está lleno, el productor bloquea.
Insertar descripción de la imagen aquí

3. La importancia del modelo productor-consumidor

  1. desacoplamiento
  2. Afeitado de picos y relleno de valles

3.1 Desacoplamiento

Cuanto más cercana sea la conexión entre los dos módulos, mayor será el grado de acoplamiento. Cuanto mayor sea el grado de acoplamiento, mayor será la influencia de los dos módulos. Cuando ocurre un problema en un módulo, el otro módulo también se verá afectado y causará problemas Especialmente para sistemas distribuidos: el desacoplamiento es muy importante.

Insertar descripción de la imagen aquí
Supongamos que lo anterior es un sistema distribuido simple, y el servidor A y el servidor B interactúan directamente (el servidor A envía una solicitud al servidor B y recibe la información devuelta por el servidor B, el servidor B envía una solicitud al servidor A y recibe la información devuelta por el servidor A. Información), el acoplamiento entre el servidor A y el servidor B es relativamente alto. Cuando uno de los dos servidores falla, ambos servidores no estarán disponibles.

No solo eso, cuando queremos agregar otro servidor C para interactuar con el servidor A, no solo es necesario modificar el servidor C, sino que también es necesario modificar el servidor A.

En comparación con la situación anterior, si utilizamos el modelo productor-consumidor, podemos resolver el problema anterior de acoplamiento excesivo.

Insertar descripción de la imagen aquí
Cuando el servidor A recibe la solicitud del cliente, no la envía directamente al servidor B. En cambio, agrega la solicitud recibida a la cola de bloqueo y luego el servidor B recibe la solicitud de la cola de bloqueo, evitando así la necesidad de comunicación. entre los dos servidores La interacción directa reduce el acoplamiento; no solo eso, cuando necesitamos agregar un servidor C adicional, no necesitamos modificar el servidor A, sino que obtenemos directamente la información de la solicitud de la cola de bloqueo.

Insertar descripción de la imagen aquí

3.2 Afeitado de picos y relleno de valles

Cuando el cliente envía una gran cantidad de información de solicitud al servidor A en un corto período de tiempo, cuando el servidor A recibe la solicitud del cliente, enviará inmediatamente toda la información recibida al servidor B. Sin embargo, aunque el servidor A puede El número El número de solicitudes recibidas puede ser grande, pero el servidor B no puede recibir tantas solicitudes a la vez, lo que provocará que el servidor B cuelgue.

Si se utiliza el modelo productor-consumidor, cuando el cliente envía una gran cantidad de solicitudes al servidor A en un corto período de tiempo, el servidor A no enviará la solicitud a B, sino a la cola de bloqueo. Cuando la cola de bloqueo está llena , El servidor A dejará de enviar solicitudes a la cola de bloqueo y caerá en un estado de bloqueo. Cuando el servidor B recibe solicitudes de la cola de bloqueo y la capacidad de la cola de bloqueo se reduce, el servidor A continuará enviando las solicitudes recibidas a la cola de bloqueo. de modo que Esto evita la situación en la que el servidor B recibe una gran cantidad de solicitudes en un corto período de tiempo y cuelga; si se lee toda la información de la solicitud recibida en la cola de bloqueo, el servidor B dejará de leer las solicitudes de la cola de bloqueo e ingresará el estado de bloqueo Hasta que el servidor A envíe una solicitud a la cola de bloqueo.
Insertar descripción de la imagen aquí

4. Cómo utilizar la cola de bloqueo proporcionada por la biblioteca estándar de Java

Después de conocer el significado del modelo productor-consumidor, echemos un vistazo a cómo utilizar las colas de bloqueo. La cola de bloqueo BlockingQueue se proporciona en la biblioteca estándar de Java y se puede utilizar directamente.

Insertar descripción de la imagen aquí
Debido a que BlockingQueu es una interfaz y no se puede crear una instancia de ella, debe crear una clase que implemente la interfaz BlockingQueue , y ArrayBlockingQueue y LinkedBlockingQueue implementan esta interfaz.

También podemos observar que BlockingQueue también hereda Queue, lo que significa que también podemos usar métodos en Queue, como oferta y encuesta, pero estos dos métodos no se usan en colas de bloqueo porque estos dos métodos no tienen propiedades de bloqueo. Utilice los métodos de venta y venta .

public class Demo1 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        queue.put("123");
        queue.put("234");
        queue.put("345");
        queue.put("456");
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
    }
}

Insertar descripción de la imagen aquí
Aquí, se agregan cuatro datos a la cola de bloqueo, pero se leen cinco veces durante la lectura, por lo que vemos que el hilo entra en el estado de bloqueo.

5. Implemente usted mismo una cola de bloqueo

La cola de bloqueo se basa en la cola, por lo que si desea implementar una cola de bloqueo, primero debe implementar una cola, así que primero echemos un vistazo a cómo implementar una cola circular.

5.1 Implementar cola circular

Las colas son relativamente fáciles de implementar, pero ¿cómo implementar colas circulares? Cuando los datos llegan al final de la matriz, el subíndice de la matriz se modifica a 0, para que se pueda lograr el propósito del bucle.

Insertar descripción de la imagen aquí
Hay dos situaciones en las que cola == cabeza:

  1. Cuando no hay datos en la cola
  2. Cuando la cola está llena

Para diferenciar estos dos casos podemos utilizar dos métodos:

  1. Una pérdida de espacio. Después de tail++, si tail + 1 == head, significa que la cola está llena, cambie tail a 0
  2. Defina una variable de tamaño para representar la cantidad de datos válidos en la cola. Cuando tamaño == cola.longitud, significa que la cola está llena.
class MyQueue {
    
    
    private final String[] data = new String[1000];
    private int size;
    private int head = 0;
    private int tail = 0;
    public void put(String str) {
    
    
        //当添加数据的时候需要判断队列的容量是否已经满了
        if(size == data.length) return;
        data[tail++] = str;
        size++;
        if(tail == data.length) tail = 0;
    }

    public String take() {
    
    
    	//读取数据的时候需要判断队列是否为空
        if(size == 0) return null;
        String ret = data[head++];
        size--;
        if(head == data.length) head = 0;
        return ret;
    }
}

5.2 Implementar cola de bloqueo

La cola de bloqueo significa que cuando la cola está vacía, el hilo que obtiene el elemento esperará a que la cola deje de estar vacía; cuando la cola está llena, el hilo que almacena el elemento esperará a que la cola esté disponible. Y debido a que la cola de bloqueo se utiliza en un entorno de subprocesos múltiples, se deben tener en cuenta los problemas de seguridad de los subprocesos.

5.2.1 Bloqueo

Cuando se requieren operaciones de consulta y modificación, la operación debe bloquearse. Debido a que nuestra opción de venta y venta es básicamente consultar y modificar datos, estas dos operaciones se pueden bloquear directamente.

class MyBlockingQueue {
    
    
    private final String[] data = new String[1000];
    private int size;
    private int head = 0;
    private int tail = 0;
    public void put(String str) {
    
    
        synchronized (this) {
    
    
            if(size == data.length) return;
            data[tail++] = str;
            size++;
            if(tail == data.length) tail = 0;
        }
    }

    public String take() {
    
    
        synchronized (this) {
    
    
            if(size == 0) return null;
            String ret = data[head++];
            size--;
            if(head == data.length) head = 0;
            return ret;
        }
    }
}

5.2.2 Realizar operaciones de bloqueo

Una vez completada la operación de bloqueo, también debemos implementar el bloqueo. Al agregar datos, si la capacidad en la cola está llena, entrará en el estado de espera de bloqueo hasta que se realice la operación de lectura de datos para eliminar los datos. Dejar de esperar; al leer datos, si la cola está vacía, el hilo ingresa al estado de espera de bloqueo hasta que se realiza una operación de colocación.

class MyBlockingQueue {
    
    
    private final String[] data = new String[1000];
    private int size;
    private int head = 0;
    private int tail = 0;
    public void put(String str) throws InterruptedException {
    
    
        synchronized (this) {
    
    
            if(size == data.length) {
    
    
                this.wait();
            }
            data[tail++] = str;
            size++;
            if(tail == data.length) tail = 0;
            //这个 notify 用来唤醒 take 操作的等待
            this.notify();
        }
    }

    public String take() throws InterruptedException {
    
    
        synchronized (this) {
    
    
            if(size == 0) {
    
    
                this.wait();
            }
            String ret = data[head++];
            size--;
            if(head == data.length) head = 0;
            //这个 notify 用来唤醒 put 操作的等待
            this.notify();
            return ret;
        }
    }
}

5.2.3 Resolver el problema del estado de espera debido a que se despierta por una interrupción

Después de usar esperar y notificar para bloquear, esperar y activar las operaciones de venta y venta, aún debemos prestar atención: ¿es cierto que solo notificar activará el estado de ESPERA? Anteriormente aprendimos a usar la interrupción para terminar subprocesos, pero la interrupción también activará los subprocesos en el estado de ESPERA, es decir, los subprocesos en el estado de ESPERA aquí no solo pueden despertarse mediante notificación, sino también mediante interrupción.

  1. Cuando el hilo entra en el estado de espera de bloqueo porque la cola de operaciones de colocación está llena, si se despierta mediante una interrupción en lugar de una notificación de la operación de toma, significa que la cola todavía está llena en este momento. Cuando se realiza la operación de agregar, Los datos válidos se sobrescribirán;
  2. Cuando el hilo entra en estado de espera de bloqueo porque la cola de operaciones de toma está vacía, si se despierta mediante una interrupción en lugar de notificar mediante una operación de colocación, significa que la cola todavía está vacía en este momento. Si se realiza una operación de eliminación, habrá no tendrá importancia.

Para resolver el problema causado por el despertar del estado de ESPERA por interrupción, cuando se despierta el hilo, es necesario determinar si el tamaño aún es igual a 0 o cola.longitud, si aún es igual, continúe ingresando al estado de ESPERA. estado, pero un juicio no es suficiente, debido a que puede ser despertado por una interrupción, es necesario realizar múltiples juicios, que se pueden resolver mediante el uso de un bucle while.

class MyBlockingQueue {
    
    
    private final String[] data = new String[1000];
    private int size;
    private int head = 0;
    private int tail = 0;
    public void put(String str) throws InterruptedException {
    
    
        synchronized (this) {
    
    
            while(size == data.length) {
    
    
                this.wait();
            }
            data[tail++] = str;
            size++;
            if(tail == data.length) tail = 0;
            //这个 notify 用来唤醒 take 操作的等待
            this.notify();
        }
    }

    public String take() throws InterruptedException {
    
    
        synchronized (this) {
    
    
            while(size == 0) {
    
    
                this.wait();
            }
            String ret = data[head++];
            size--;
            if(head == data.length) head = 0;
            //这个 notify 用来唤醒 put 操作的等待
            this.notify();
            return ret;
        }
    }
}

5.2.4 Resolución de problemas causados ​​por el reordenamiento de instrucciones

Debido a que las operaciones de colocación y toma requieren lectura y escritura, el reordenamiento de instrucciones puede causar otros problemas, aquí es necesario utilizar volátiles para resolver el problema de reordenamiento de instrucciones.

class MyBlockingQueue {
    
    
    private final String[] data = new String[1000];
    private volatile int size;
    private volatile int head = 0;
    private volatile int tail = 0;
    public void put(String str) throws InterruptedException {
    
    
        synchronized (this) {
    
    
            while(size == data.length) {
    
    
                this.wait();
            }
            data[tail++] = str;
            size++;
            if(tail == data.length) tail = 0;
            //这个 notify 用来唤醒 take 操作的等待
            this.notify();
        }
    }

    public String take() throws InterruptedException {
    
    
        synchronized (this) {
    
    
            while(size == 0) {
    
    
                this.wait();
            }
            String ret = data[head++];
            size--;
            if(head == data.length) head = 0;
            //这个 notify 用来唤醒 put 操作的等待
            this.notify();
            return ret;
        }
    }
}

Implementación de prueba de cola de bloqueo

public class Demo4 {
    
    
    public static void main(String[] args) {
    
    
        MyBlockingQueue queue = new MyBlockingQueue();
        Thread t1 = new Thread(() -> {
    
    
            while(true) {
    
    
                try {
    
    
                    System.out.println("消费元素" + queue.take());
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2 = new Thread(() -> {
    
    
            int count = 1;
            while(true) {
    
    
                try {
    
    
                    queue.put(count + "");
                    System.out.println("生产元素" + count);
                    count++;
                    Thread.sleep(100);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

Ralentiza la producción, lo que hace que las operaciones de lectura se bloqueen mientras esperan que se inserten datos antes de ejecutarse.

Insertar descripción de la imagen aquí

public class Demo4 {
    
    
    public static void main(String[] args) {
    
    
        MyBlockingQueue queue = new MyBlockingQueue();
        Thread t1 = new Thread(() -> {
    
    
            while(true) {
    
    
                try {
    
    
                    System.out.println("消费元素" + queue.take());
                    Thread.sleep(100);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2 = new Thread(() -> {
    
    
            int count = 1;
            while(true) {
    
    
                try {
    
    
                    queue.put(count + "");
                    System.out.println("生产元素" + count);
                    count++;
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

Insertar descripción de la imagen aquí

Deje que la operación de producción entre en el estado de espera de bloqueo.

Supongo que te gusta

Origin blog.csdn.net/m0_73888323/article/details/132862839
Recomendado
Clasificación