Explicación detallada de la programación concurrente de Java

En el artículo anterior, la combinación de conceptos relacionados con subprocesos múltiples (comprensión personal) habló principalmente sobre algunos conceptos de concurrencia de subprocesos múltiples a nivel macro. Este artículo se centra en Java y habla sobre programación concurrente.

sychronizedpalabras clave

La JVM en realidad solo proporciona un tipo de bloqueo, sychronizedla palabra clave , que podemos ver en Threadla definición de las clases de Java State.

Hay un total de estados de subprocesos en Java NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING. TERMINATEDSolo BLOCKEDcorresponde al caso en que el hilo ingresa sychronizeral bloque y no logra adquirir el bloqueo. Para el bloqueo basado en AQS, si el subproceso está bloqueado, el estado es WAITINGporque AQS en realidad llama LockSupportal método de la clase para lograr el bloqueo del subproceso, y el WAITINGestado correspondiente de estos métodos java.lang.Thread.Statese escribe en los comentarios.

También vale la pena señalar que aunque los subprocesos de Java tienen una correspondencia uno a uno con el sistema operativo real (en HotSpot), de hecho, si la llamada del sistema realiza operaciones de E/S y bloquea el subproceso, el estado actual del subproceso sigue siendo el estado, RUNNABLEporque el estado del hilo del sistema operativo y el estado del hilo de Java no se corresponden exactamente entre sí, ni están completamente unificados.

encabezado de objeto

Como se mencionó anteriormente, sychronizedJVM implementa el bloqueo provisto, lo que significa que debe haber algunas variables en JVM para almacenar el estado del bloqueo. De hecho, este es el caso En Java, hay una parte del espacio dentro de cada objeto para almacenar información de encabezado de objeto .

El diseño de memoria de un objeto JVM se divide en tres áreas: encabezado de objeto , datos de instancia y relleno de alineación . Los datos de instancia almacenan la información real del objeto.
Para obtener detalles sobre el diseño de la memoria de los objetos JVM, consulte este artículo: Principios de la estructura de objetos Java e implementación de bloqueo y explicación detallada de MarkWord .

El encabezado del objeto contiene mucha información, entre la cual Mark Word se usa para almacenar hashCodey bloquear información del objeto.

En Mark Word, si el objeto tiene un candado, monitorse almacenará un puntero a él, y este monitorobjeto es equivalente al candado del objeto. Los comentarios del código fuente de Java se denominan generalmente bloqueos de monitor .

Sabemos que cada objeto en Java tiene un monitormonitor que le corresponde. Al javapdescompilar un sychronizedcódigo fuente que contiene bloques, podemos encontrar monitorentery monitorexit.

Cerraduras pesadas y livianas

Siempre escucho que sychronizedes un candado de peso pesado, entonces, ¿qué son exactamente los candados de peso pesado y los candados de peso ligero?

Un bloqueo pesado significa que los subprocesos que no pueden competir por el bloqueo entrarán en un estado bloqueado y luego deberán esperar para reactivarse después de ejecutarse nuevamente.

En términos simples, un bloqueo ligero significa que los subprocesos que compiten por el bloqueo seguirán adquiriendo el bloqueo a través de giros CAS, es decir, no perderán la oportunidad de ejecutar el subproceso. Obviamente, girar todo el tiempo desperdiciará el rendimiento de la CPU en vano, por lo que sychronizedse actualizará a un bloqueo de peso pesado después de una cierta cantidad de giros, para evitar el bloqueo de subprocesos y despertar hasta cierto punto y afectar el rendimiento (porque estas operaciones no pueden completarse en modo usuario, todos implican Cambiar de modo usuario a modo kernel).

Adicional a esto existe otro concepto de biased locks , el escenario donde nace es porque encontramos que un mismo hilo compite por los locks en muchos casos, por lo que se diseñó el concepto de biased locks, es decir, un hilo que ha adquirido el candado Si el candado se adquiere de nuevo, no es necesario pasar por el proceso de giro de candado ligero para adquirir el candado, pero puede adquirirse directamente, mejorando así el rendimiento.

Un objeto está en un estado sin bloqueo al principio , y luego pasará por las etapas de bloqueos sesgados, bloqueos livianos y bloqueos pesados. El sychronizedbloqueo agregado es un proceso de este tipo, durante el cual el bloqueo solo puede actualizarse y no puede ser degradado.

Los detalles específicos de los bloqueos de Java sychronizedpueden consultar este artículo: "Bloqueos" de Java que deben decirse
.

mecanismo de espera/notificación

Con sychronizedel bloqueo, podemos garantizar la consistencia de los datos en subprocesos múltiples, y luego a través del método de espera/notificaciónObject proporcionado por la clase , podemos realizar la comunicación entre subprocesos múltiples.

Por qué esperar/notificar debe estar bloqueado

Vale la pena señalar que esperar/notificar debe usarse con un bloqueo, debido a los dos puntos siguientes:

  1. Sin bloqueo, no hay garantía de que ocurra antes de la regla . Entonces, las modificaciones que realice en la variable de condición pueden no ser visibles para otros subprocesos temporalmente. Pero esto volatilepuede lograr el mismo efecto siempre que agregue la modificación de palabras clave a la variable de condición.
  2. Causará un problema de despertar perdido . Por ejemplo, si no hay bloqueo, un subproceso solo espera y otro subproceso notifica a otros subprocesos en este momento, debido a que el primer subproceso no se ha agregado a la secuencia de espera, la notificación no se recibirá esta vez.

Con base en los dos puntos anteriores, los diseñadores de Java exigen que se agreguen bloqueos cuando se usa esperar/notificar, y el objeto que llama a esperar/notificar debe ser el objeto correspondiente al bloqueo mantenido por el subproceso actual. (La cerradura debe ser la misma, debe ser solo un diseño conveniente, después de todo, el punto es que debe haber una cerradura)

Para una discusión detallada sobre este punto, consulte: Por qué las llamadas en Java notify()requieren bloqueos .

notify()¿Realmente despertará el hilo?

Dijimos anteriormente que notify()necesitamos usarlo con un bloqueo, por lo que esto requiere que primero notifiquemos y luego desbloqueemos. Pero de acuerdo con los comentarios, sabemos que notify()el subproceso que espera el bloqueo se despertará nuevamente, por lo que ¿no hará que el subproceso notificado simplemente se despierte y se bloquee nuevamente porque no puede adquirir el bloqueo (suponiendo que no hay desbloqueo en este momento)?

Obviamente, notify()el hilo no se puede despertar, de lo contrario ocurrirá tal accidente. java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObjectPodemos confirmar esto desde AQS . AQS mantiene dos tipos de colas, la cola de espera que espera para adquirir el bloqueo y la await()cola condicional del subproceso bloqueado.Cuandosignal() la cola condicional se quita de la cola, el elemento principal de la cola condicional se agrega a la cola de espera para prepararse para la competencia de bloqueo . . (Sobre el código fuente de AQS se analizará en detalle más adelante)

Por lo tanto, podemos especular razonablemente que notify()el subproceso no se puede reactivar, lo que también se puede confirmar escribiendo una pequeña demostración.

Sabemos que notify()es un método local y que la capa inferior está implementada en lenguaje C, por lo que no es fácil ver directamente el código fuente. wait()En comparación con el estándar en c en Java pthread_cond_wait(), este método tiene requisitos muy flexibles y ni siquiera requiere que se llame a un bloqueo, por lo que encontré una pregunta: si el desbloqueo debe realizarse antes o después de notificar en c , puede ver Gao Zan's respuesta La explicación es que, en términos generales, el diseñador considerará que si la notificación se desbloquea nuevamente, el subproceso activado se bloqueará nuevamente, por lo que el diseñador no diseñará la notificación para activar directamente el subproceso.

por qué wait()en el cuerpo del bucle

Tenga en cuenta que java.lang.Object#wait()hay este comentario:

Un subproceso también puede activarse sin ser notificado, interrumpido o agotado, lo que se conoce como activación espuria. Si bien esto rara vez ocurrirá en la práctica, las aplicaciones deben protegerse comprobando la condición que debería haber provocado que se despertara el subproceso y continuar esperando si la condición no se cumple. En otras palabras, las esperas siempre deben ocurrir en bucles, como este:
sincronizado (obj) { while (la condición no se cumple) obj.wait (tiempo de espera); … // Realiza la acción adecuada a la condición }



Se dice que wait()puede haber despertares falsos , lo que requiere que verifiquemos si la variable de condición cumple la condición en un bucle y llamemos wait().

Sabemos que wait()estas funciones requieren llamadas al sistema al final, porque están involucradas operaciones como el bloqueo de subprocesos, que no se pueden realizar en modo usuario. Y estas llamadas al sistema de bloqueo volverán cuando se interrumpan EINTR. Si espera en este momento, pueden ocurrir problemas, porque no puede estar seguro de si otros subprocesos han llamado para notify()despertarlo durante este proceso. Esperar, puede perder la activación, lo que resulta en una situación de estancamiento permanente. Por tanto, a nivel de sistema operativo, mientras llames wait(), si te interrumpen, no optarás por seguir esperando, aunque se puedan producir falsas reactivaciones.

Para una discusión sobre este punto, consulte esta respuesta: ¿ Por qué los bloqueos condicionales generan activaciones falsas? .

También vale la pena señalar que java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#await()no habrá un despertar falso, porque su implementación elimina por completo la ocurrencia de esta situación (también es una condición de juicio de bucle inalámbrico).

notify()¿Los despertares son aleatorios?

Según java.lang.Object#notifylos comentarios, sabemos que este método activa aleatoriamente un hilo en espera, pero de hecho, también se basa en la implementación:

La elección es arbitraria y ocurre a discreción de la implementación.

En la máquina virtual HotSpot notify()se sigue el orden de FIFO, mientras que notifyAll()se sigue el orden de LIFO.

Para notify()secuencia:

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Main main = new Main();
        for (int i = 0; i < 5; i++) {
    
    
            String name = "线程-" + i;
            Thread.sleep(1000);
            new Thread(()->{
    
    
                main.await(name);
            }).start();
        }
        for (int i = 0; i < 5; i++) {
    
    
            Thread.sleep(1000); // 由于synchronized不是公平锁,这里得每隔一段时间notify一次
            main.signal();
        }
    }

    private synchronized void await(String name){
    
    
        try {
    
    
            System.out.println(name + "被阻塞");
            wait();
            System.out.println(name + "继续执行");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    private synchronized void signal(){
    
    
        notify();
    }

    private synchronized void signalAll(){
    
    
        notifyAll();
    }
}

El programa imprime como:

El subproceso 0 está bloqueado
El subproceso 1 está bloqueado El subproceso
2 está bloqueado
El subproceso 3 está bloqueado El subproceso
4 está bloqueado El
subproceso 0 continúa la ejecución El
subproceso 1 continúa la ejecución
El subproceso 2 continúa la ejecución
El subproceso 3 continúa la ejecución
El subproceso 4 continúa la ejecución

Para notifyAll()secuencia:

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Main main = new Main();
        for (int i = 0; i < 5; i++) {
    
    
            String name = "线程-" + i;
            Thread.sleep(1000);
            new Thread(()->{
    
    
                main.await(name);
            }).start();
        }
        Thread.sleep(1000); // 这里得等所有线程被wait方法阻塞后再notifyAll
        main.signalAll();
    }

    private synchronized void await(String name){
    
    
        try {
    
    
            System.out.println(name + "被阻塞");
            wait();
            System.out.println(name + "继续执行");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    private synchronized void signal(){
    
    
        notify();
    }

    private synchronized void signalAll(){
    
    
        notifyAll();
    }
}

El programa imprime como:

El subproceso 0 está bloqueado
El subproceso 1 está bloqueado El subproceso
2 está bloqueado
El subproceso 3 está bloqueado El
subproceso 4 está bloqueado El subproceso 4
continúa la ejecución El subproceso
3 continúa la ejecución
El subproceso 2 continúa la ejecución
El subproceso 1 continúa la ejecución
El subproceso 0 continúa la ejecución

AQS

Además de los bloqueos proporcionados por JVM sychronized, también podemos confiar AbstractQueuedSynchronizeren AQS para implementar bloqueos. Los bloqueos típicos, como ReentrantLock, ReentrantReadWriteLocketc., se basan en AQS. Se puede decir que el núcleo de JUC es AQS.

Los bloqueos basados ​​en AQS proporcionan un bloqueo más detallado, múltiples colas condicionales ( Condition), etc., que son superiores a sychronizedlos bloqueos y se puede decir que reemplazan por completo sychronizeda los bloqueos. Quizás sychronizedel único beneficio del bloqueo es que es más fácil de usar y de leer.

Si comprende el código fuente de AQS, tendrá una comprensión más profunda de los bloqueos. Como dije en mi artículo anterior, los bloqueos son una abstracción de alto nivel que nos facilita la resolución de problemas de concurrencia.La capa inferior aún se basa en las instrucciones de la CPU. AQS usa muchas operaciones y LockSupportclases de CAS para construir, aquí hay un resumen macro:

Análisis de código fuente

AQS es una cola síncrona. Usando esta cola, podemos realizar el concepto de bloqueos. lock()Corresponde a llamar al que está en AQS acquire(), unlock()y corresponde a llamar al que está en AQS release().

acquire()Es un método de plantilla, que llama tryAcquire()y por defecto acquireQueued(). El primero es para adquirir el bloqueo, y no hay implementación dada en AQS. El último es equivalente a agregar el tryAcquire()nodo fallido, el nodo que no pudo adquirir el bloqueo, a la cola, y adquisición cíclica.En términos generales, si la adquisición de bloqueo falla en el ciclo, se bloqueará, esperando que el nodo predecesor, el nodo que sostiene el bloqueo, lo despierte.

release()También es un método de plantilla, que se llamará tryRelease(). Este también es un método para liberar el bloqueo que implementará la subclase. Después de que la liberación sea exitosa, si el nodo actual es el nodo principal (porque la cola está vacía, en esta vez, el primer subproceso A que haya adquirido el bloqueo no se pondrá en cola, enqueue solo se enq()implementa en ), se ejecutará nuevamente unparkSuccessor()y el siguiente subproceso se despertará.

Otros, como acquireShared()etc., también son métodos de plantilla.

Acerca de ConditionObject:

AQS también implementa la función Condition, que mantiene una cola de condición, que es diferente de la cola de espera que mantiene AQS, y las dos colas son diferentes. Los métodos en ConditionObjectél reciben una implementación completa. El nodo en la cola condicional waitStatuses CONDITION.

En await(), se llamará al método en AQS fullyRelease()para darse cuenta de que el subproceso libera el bloqueo, sale de la cola de espera y luego se une a la cola condicional. Luego, en signal()el método, llame capa por capa y finalmente llame transferForSignal()para realizar la operación de salir de la cola condicional y unirse a la cola de espera.

Vale la pena señalar que ya sea una cola condicional o una cola de espera, siempre se sigue el orden de FIFO, no el orden aleatorio. Además, los objetos en Object notify()también siguen FIFO, pero notifyAll()siguen LIFO. Pero estos son métodos nativos, principalmente depende de la implementación de la JVM, al menos en HotSpot.

Acerca de los bloqueos exclusivos y los bloqueos compartidos:

Como se mencionó anteriormente, AQS mantiene dos tipos de colas, colas de espera y colas condicionales . En la clase Node, hay una variable miembro nextWaiterpara registrar el siguiente nodo en la cola condicional.

En la cola de espera , esta variable se usa para marcar si el modo del nodo es un modo compartido o un modo exclusivo, y una variable miembro se usa en la cola de espera nextpara indicar el siguiente nodo. Puede tener curiosidad, qué variable se usa en la cola condicional para identificar el modo del nodo de cola, de hecho, no hay necesidad de identificar el modo para el nodo en la cola condicional, porque solo se puede acceder a la cola condicional en modo exclusivo.

Desde este aspecto, la legibilidad del código fuente de AQS en realidad no es muy buena. Después de todo, una variable se usa para múltiples propósitos, y el código fuente de AQS tiene una gran cantidad de códigos de una línea para implementar múltiples funciones, por lo que la legibilidad es realmente pobre. Pero es innegable que el rendimiento es alto, al menos bajo la premisa de sacrificar la legibilidad.

En la cola de espera, al acquireShared()adquirir el bloqueo, cuando se obtenga el bloqueo compartido, setHeadAndPropagate()se llamará cíclicamente al método para determinar si el nodo posterior que ha obtenido el bloqueo compartido waitStautses menor que 0 (porque PROPAGATEpuede o se vuelve SIGNAL), y de ser así , luego juzgue el nodo subsiguiente Si el estado es un bloqueo compartido, o si lo es null, despierte los nodos subsiguientes.

También es necesario juzgar si nulles porque nullno significa que el nodo está al final de la cola. Esto se debe a que enq()la operación de unirse a la cola en el medio es apuntar primero node.prev = t;el nodo predecesor del nodo de unión al final de la cola, luego compareAndSetTail(t, node)reemplazar el final de la cola con CAS y finalmentet.next = node;

Sabemos que si varios subprocesos han estado adquiriendo bloqueos compartidos, solo necesitamos aumentar continuamente el estado, pero puede haber un bloqueo exclusivo agregado en un momento determinado. Durante el período, se han acumulado muchos nodos con bloqueos compartidos. Si el bloqueo se libera, luego de que el primer nodo de bloqueo compartido obtenga el bloqueo, debe notificar a todos los nodos de bloqueo compartido posteriores.

Ampliar aquí. Un nodo que adquiere un bloqueo no se unirá a la cola. Solo se unirá a la cola (aprobado) si la adquisición falla enq(). Después de una adquisición exitosa, se eliminará de la cola (por ejemplo acquireQueued()). Y el algoritmo que queremos implementar solo implica cómo adquirir bloqueos y cómo liberar bloqueos En cuanto a la gestión de colas, toda la lógica se ha implementado en AQS. Por ejemplo, la lógica de bloqueos justos y bloqueos injustos debe implementarse en subclases, porque esto pertenece a cómo adquirir y liberar bloqueos.

Acerca de la cancelación de nodos:

En la clase Nodo de AQS, waitStatushay otro valor CANCELLEDque indica que el nodo ha expirado o ha sido interrumpido.

Pasa cancelAcquire()ajustes. Mientras que los métodos se llaman en bloques en cancelAcquire()todos los métodos que adquieren bloqueos (por ejemplo , .acquireQueued()finally

cancelAcquire()Este método solo señalará el siguiente nodo que se cancelará a sí mismo, y no lo eliminará de la cola. La operación de eliminación de la cola se realizará en otros métodos, como shouldParkAfterFailedAcquire()o nuevamente .cancelAcquire()

En cuanto a si hay interrupción en el método:

Los métodos con esta palabra clave generarán una excepción de interrupción. Si no, llama al subproceso actual interrupt(). En cuanto a lo que sucederá al llamar a este método, puede consultar java.lang.Thread#interruptlos comentarios. Diferentes situaciones tendrán diferentes reacciones:

Los números de serie corresponden a las situaciones en las que se interrumpen los cuatro subprocesos.

Supongo que te gusta

Origin blog.csdn.net/weixin_55658418/article/details/130795566
Recomendado
Clasificación