12 imágenes para comprender el contenedor concurrente de alto rendimiento ConcurrentLinkedQueue a la vez

12 imágenes para comprender el contenedor concurrente de alto rendimiento ConcurrentLinkedQueue a la vez

 

Prefacio

El artículo anterior habló sobre la implementación y características de las colecciones concurrentes, su desventaja es que no es adecuado para escenarios con mucha escritura y no es adecuado para escenarios con gran concurrencia.CopyOnWeiteArrayList

Este artículo hablará sobre el alto rendimiento en escenarios concurrentes.ConcurrentLinkedQueue

Se necesitan unos 10 minutos para leer este artículo.

Antes de leer este artículo, debe comprender CAS, volátiles y otros conocimientos.

Si no comprende CAS, puede consultar este artículo con 15 000 palabras, 6 casos de código y 5 esquemas para ayudarlo a comprender a fondo la segunda sección de Sincronizado.

Si no comprende volátil, puede consultar este artículo con 5 casos y diagramas de flujo para ayudarlo a comprender la palabra clave volátil del 0 al 1.

estructura de datos

ConcurrentLinkedQueueComo puede ver por el nombre, admite concurrencia y colas implementadas mediante listas vinculadas.

imagen.png

A través del código fuente, podemos ver que los campos se utilizan para registrar el primer y último nodo, y la implementación del nodo es una lista vinculada unidireccional.ConcurrentLinkedQueue

Y todos estos campos clave se modifican con volátil. Utilice volátil para garantizar la visibilidad en escenarios de lectura sin "bloquear".

También hay otros campos, como Inseguro usando CAS y cierta información de compensación, etc., que no se enumerarán aquí.

    clase  pública ConcurrentLinkedQueue <E> extiende AbstractQueue <E>   
 implementa Queue <E> , java.io.Serializable {     clase estática privada Nodo <E> { // Registra datos del elemento E volátil ; // Nodo sucesor Nodo volátil <E> siguiente ; _ _ _ _ _ _ _ _ _       } // El primer nodo privado transitorio volátil Nodo < E > head ; // El nodo de cola privado transitorio volátil Nodo < E > tail ;   }              
   
          
           
             
           
             
 
       
           
       
           
 

Durante la inicialización, el primer y último nodo apuntarán a un nodo con datos de almacenamiento vacíos al mismo tiempo.

       public  ConcurrentLinkedQueue () 
 { cabeza = cola = nuevo Nodo <E> ( nulo ) ;       }               
 

 

 

El pensamiento de diseño

Actualización retrasada del primer y último nodo

Antes de ver el principio de implementación, hablemos primero de la idea de diseño; de lo contrario, es posible que no se comprenda el principio de implementación.ConcurrentLinkedQueue

ConcurrentLinkedQueueLa idea de bloqueo optimista se adopta al escribir escenarios y se utiliza CAS + reintento fallido para garantizar la atomicidad de las operaciones.

Para evitar una sobrecarga excesiva de CAS, se adopta la idea de retrasar la actualización del primer y último nodo para reducir la cantidad de CAS.ConcurrentLinkedQueue

En otras palabras, el primer y el último nodo no son necesariamente los últimos nodos primero y último.ConcurrentLinkedQueue

 

 

ganglio centinela

ConcurrentLinkedQueueUtilice ganglios centinela en su diseño

¿Qué es un ganglio centinela?

Los nodos centinela también se denominan nodos virtuales y se utilizan a menudo en estructuras de datos como listas vinculadas.

Si desea agregar o eliminar un nodo en una lista enlazada unidireccional, debe obtener el nodo predecesor de este nodo antes de realizar la operación.

Al operar el primer nodo, si agrega un nodo virtual (nodo centinela) delante del primer nodo, no hay necesidad de un procesamiento especial.

En otras palabras, el uso de nodos centinela puede reducir la complejidad del código . Creo que los estudiantes que hayan estudiado algoritmos relacionados con listas vinculadas tendrán una comprensión profunda de ellos.

Los nodos centinela también pueden reducir los conflictos de concurrencia cuando solo hay un nodo

Es posible que esta característica solo se comprenda después de leer la implementación y el diagrama de flujo posteriores.

 

 

Implementación del código fuente

ConcurrentLinkedQueueLas principales operaciones son la entrada y salida del equipo, las utilizamos y para analizarlas.offerpoll

oferta

Antes de analizar el código fuente, primero expliquemos el papel de algunas variables complejas.

t registrar la cola del nodo de cola

p se utiliza para atravesar bucles de nodos. Solo se permite agregar nuevos nodos cuando el nodo p es el nodo de cola real.

q se utiliza para registrar el nodo sucesor de p

Hay tres situaciones a la hora de incorporarse al equipo:

  1. Cuando el nodo sucesor de p esté vacío (p es el nodo de cola real), intente agregar un nuevo nodo con CAS y, después de tener éxito, intente actualizar el nodo de cola.

  2. Cuando p es igual al nodo sucesor de p (el siguiente de p apunta a sí mismo, lo que indica que está construido como un nodo centinela, y se puede construir un nodo centinela cuando se retira la cola para la encuesta); en este momento, se juzga si la cola El nodo de cola ha sido modificado, y si el nodo de cola ha sido modificado, ubíquelo. El nodo de cola, si no ha sido modificado (no puede continuar atravesando usando next), solo puede ubicar el nodo principal.

  3. En otros casos, significa que p en este momento no es el nodo de cola real y debe ubicarse en el nodo de cola real; en este momento, si p no es el nodo de cola original y el nodo de cola ha sido modificado, entonces ubique de lo contrario, localice el sucesor. Los nodos continúan atravesándose

El código en el segundo y tercer caso es muy agradable a la vista pero no muy legible. Puedes ver el resumen junto con el análisis del código fuente. Si aún no lo entiendes, hay un diagrama de flujo para facilitar su comprensión.

        oferta booleana  pública ( E e ) {  
 // Verifique el puntero nulo checkNotNull ( e ); // Construya un nuevo nodo final Nodo < E > newNode = nuevo Nodo < E > ( e ); // Bucle de reintento fallido //t: El nodo de cola del registro actual //p: el nodo de cola real //q: el nodo sucesor de p for ( Node < E > t = tail , p = t ;;) { Node < E > q = p . next ; // Caso 1: El nodo sucesor de p está vacío, lo que indica que el p actual es el nodo de cola real if ( q == null ) { // Pruebe CAS para modificar el nodo sucesor de p a un nuevo nodo // If el siguiente de p es nulo, reemplácelo con un nuevo nodo newNode // Si falla, significa que otros subprocesos pueden agregar nodos con éxito y continuar con el bucle; si tiene éxito, determina si se actualiza el nodo de cola tail if ( p . casNext ( null , newNode )) { // Si p no es igual a t, significa que el nodo de cola en este momento no es el nodo de cola real // Pruebe CAS: si el nodo de cola actual es t, establezca el nuevo nodo al nodo de cola if ( p != t ) casTail ( t , newNode );   return true ;                   }               } //Caso 2: p Igual al nodo sucesor de p (p apunta a sí mismo) else if ( p == q ) //t: antiguo nodo de cola // (t = tail): nuevo nodo de cola //t != (t = tail): Descripción El nodo de cola ha sido modificado, p es igual al nuevo nodo de cola; no modificado, p es igual al nodo principal p = ( t != ( t = tail )) ? t : head ; // Caso 3: p no es el nodo de cola real en este momento, es necesario ubicar la cola real node else //p!=t:p ya no es el nodo de cola original //t != (t = tail): el nodo de cola ha sido modificado //p ya no es el nodo de cola original Y si el nodo de cola es modificado, sea p igual al nodo de cola modificado; de lo contrario, sea p igual a su nodo sucesor q p = ( p ! = t && t != ( t = cola )) ? t : q ;           }       }          
           
           
                
           
           
           
           
                
                  
               
                 
                   
                   
                   
                   
                       
                       
                         
                           
                        
 
                   
 
               
                  
                   
                   
                   
                        
               
               
               
                   
                   
                   
                            
 
 

 

encuesta

Si comprende las variables en la oferta de cola, entonces la encuesta de cola también es fácil de entender, donde p y q son similares.

h cabeza de nodo de cabeza de registro

p se utiliza para el recorrido en bucle de nodos y solo se permite sacarlo de la cola cuando el nodo p es el nodo principal real.

q se utiliza para registrar el nodo sucesor de p

Hay cuatro situaciones de salida del equipo:

  1. Cuando p es el nodo principal real, CAS establece los datos en vacío y luego determina si head es el nodo principal real. De lo contrario, actualiza el nodo principal y luego apunta el nodo principal original junto a él para construir un nodo centinela. .

  2. Cuando el nodo sucesor de p está vacío, significa que la cola está vacía. Intente CAS para cambiar el nodo principal a p.

  3. Si el nodo sucesor de p es él mismo, significa que otros subprocesos salen de la cola para construir nodos centinela y omitir este ciclo.

  4. En otros casos, retroceda

      public  E  poll () { 
 //Conveniente para salir del bucle doble restartFromHead : for (;;) { //h registrar el nodo principal //p nodo principal real //q es el nodo sucesor de p for ( Node < E > h = head , p = h , q ;;) { //Obtener los datos del nodo p E item = p.item ; //Caso 1: // Si los datos no están vacíos , el nodo p es el nodo principal real / / Pruebe CAS para establecer los datos en nulos, si los datos son elementos, reemplácelos con nulos. Si falla, indicará otros subprocesos y quitará la cola, y continuará el bucle if ( item ! = null && p . casItem ( item , null )) { // Si el nodo principal actual no es el nodo principal real, entonces actualice el nodo principal if ( p != h ) updateHead ( h , (( q = p . next ) != null ) ? q : p ) ; return item ;                  } //Caso 2: //El nodo sucesor de p está vacío. Tenga en cuenta que la cola está actualmente vacía. Pruebe CAS para cambiar el nodo principal a p (p puede ser un ganglio centinela en este momento) de lo contrario , si (( q = p . next ) == null ) { updateHead ( h , p ); return null ;                  } //Caso 3: //Si el nodo sucesor de p apunta al propio p, significa que se construirán otros subprocesos como nodos centinela cuando se retira el sondeo de la cola, y este ciclo se omitirá. else if ( p == q ) continue restartFromHead ; //Caso 4: // Posicionar p como nodo sucesor requiere atravesar else p = q ;              }      }          }         
          
          
              
              
              
                   
                  
                     
                  
                  
                  
                      
                      
                        
                              
                       
 
                  
                  
                      
                      
                       
 
                  
                  
                     
                       
                  
                  
                  
                        
 
 
 

En el método de actualización del nodo principal, se juzgará

Si el nodo principal actual no es el nodo principal real, intente CAS para configurar el nodo principal en p nodo principal real.

Una vez que CAS tenga éxito, señalará el siguiente nodo principal original hacia sí mismo y lo convertirá en un ganglio centinela.

 final  void  updateHead ( Nodo < E >  h , Nodo < E >  p ) { 
 if ( h != p && casHead ( h , p )) h . perezosoSetNext ( h ); }        
         
 

 

Implementación del diagrama de flujo

Los estudiantes que quieran seguir la depuración deben desactivar estas dos configuraciones en la idea; de lo contrario, la depuración será incorrecta.

imagen.png

Para una comprensión más sencilla, veamos un fragmento de código simple y su diagrama de flujo de implementación.

     public  void  testConcurrentLinkedQueue () { 
 ConcurrentLinkedQueue < String > cola = new ConcurrentLinkedQueue <> (); ​queue . oferta ( "h1" ); cola . oferta ( "h2" ); cola . oferta ( "h3" ); ​Cadena p1 = cola . encuesta (); Cadena p2 = cola . encuesta (); Cadena p3 = cola . encuesta (); Cadena p4 = cola . encuesta (); ​cola . oferta ( "h4" ); Sistema . fuera . println ( cola );     }            
 
         
         
         
 
            
            
            
            
 
         
         
 

[Declaración: si el elemento del nodo en el diagrama no escribe datos, significa que los datos almacenados están vacíos; si el siguiente nodo no dibuja una relación de puntería, también significa que está vacío]

Al ejecutar la construcción, el primer y último nodo se inicializarán para que apunten al mismo nodo con datos vacíos.

imagen.png

Al ingresar a la cola por primera vez, la primera situación se cumple tan pronto como se ingresa al bucle. En este momento, p es el nodo de cola real. Direct CAS establece next como el nuevo nodo. Sin embargo, dado que p es el mismo que tail, el nodo de cola tail no se actualizará.

Por lo tanto, el primer y último nodo siguen siendo ganglios centinela, y el siguiente ganglio centinela apunta al nodo recién unido.

imagen.png

Al unirse a la cola por segunda vez, dado que p (cola) no es el nodo de cola real en este momento, ocurrirá la tercera situación: dado que la cola no se ha modificado, p se cambiará a su nodo sucesor y el recorrido hacia atrás será continuar.

En el segundo ciclo, p es el nodo de cola real, por lo que CAS intenta agregar un nuevo nodo. Dado que p es diferente del nodo de cola en este momento, la cola se actualizará.

imagen.png

Al incorporarse al equipo por tercera vez, la situación es la misma que la primera.

imagen.png

En este momento, hay ganglios centinela y cuatro nodos h1, h2 y h3 en la cola.

Al sacar de la cola por primera vez, dado que el campo de datos del nodo centinela señalado por la cabeza está vacío, ocurrirá la cuarta situación, es decir, cambiar p a su nodo sucesor y continuar retrocediendo.

En el segundo ciclo, p es el nodo h1. Dado que los datos no están vacíos, CAS establece que los datos estén vacíos.

p.casItem(item, null)Establezca los datos originales del nodo h1 en vacío

imagen.png

En este momento, head no es el nodo principal real, por lo que se actualizará.

imagen.png

Luego apunte la cabeza original hacia sí misma y conviértala en un ganglio centinela para facilitar la GC de los dos ganglios intermedios que ya no se utilizan.

imagen.png

Al quitar la cola por segunda vez, si se cumple la primera situación, CAS configurará directamente los datos del nodo h2 para que estén vacíos y el nodo principal no se actualizará.

imagen.png

La tercera vez que te unes al equipo es similar a la primera vez que te unes al equipo, satisfaciendo la cuarta situación.

En el segundo ciclo, vaya a CAS para configurar los datos para que estén vacíos, actualice el nodo principal y configure el nodo principal original como un ganglio centinela.

imagen.png

La tercera situación se cumplirá cuando se retire la cola por cuarta vez, pero en este momento p es el primer nodo, por lo que el primer nodo no se actualizará y se devolverá Null.

En este punto podemos encontrar que la cola del nodo de cola está en el ganglio centinela, si retrocedemos nunca llegaremos a la cola.

Realice la operación de puesta en cola nuevamente y descubra que satisface la segunda situación. El siguiente de p apunta a sí mismo. Como no ha sido modificado, p es igual al nodo principal y regresa a la cola.

Al ingresar a otro ciclo, CAS agregará h4 y luego actualizará el nodo de cola.

imagen-20230913220256751

Llegados a este punto, este sencillo ejemplo cubre la mayoría de los procesos de entrada y salida del equipo, hablemos del ganglio centinela.

En este proceso, el nodo centinela puede evitar la contienda cuando solo hay un nodo en la cola.

 

 

Resumir

ConcurrentLinkedQueueBasado en la implementación de una lista vinculada unidireccional, se usa volátil para garantizar la visibilidad, de modo que no hay necesidad de usar otros mecanismos de sincronización en escenarios de lectura; se usa bloqueo optimista CAS + reintento de falla para garantizar la atomicidad de las operaciones en escenarios de escritura.

ConcurrentLinkedQueueUsar la idea de retrasar la actualización del primer y último nodo puede reducir en gran medida la cantidad de CAS y mejorar el rendimiento de concurrencia; use nodos centinela para reducir la complejidad del código y evitar la competencia por un nodo.

Durante la operación de la cola, el nodo de cola real se encontrará en el bucle, se usa CAS para agregar un nuevo nodo y luego se determina si CAS actualiza el nodo de cola.

Durante el ciclo de la operación de puesta en cola, los nodos generalmente se atraviesan hacia atrás. Dado que la operación de eliminación de la cola construirá un ganglio centinela, cuando se determina que es un ganglio centinela (el siguiente apunta a sí mismo), el nodo de cola o el nodo principal se colocan de acuerdo con a la situación ("saltar")

Durante la operación de eliminación de la cola, también encontramos el nodo principal real en el bucle, usamos CAS para configurar los datos del nodo principal real en vacío, luego determinamos si CAS actualiza el nodo principal y luego dejamos que el nodo principal anterior apunte a sí mismo. para construir un ganglio centinela, conveniente para GC

Durante el ciclo de la operación de eliminación de la cola, los nodos generalmente se atraviesan hacia atrás. Dado que la eliminación de la cola creará un ganglio centinela, cuando se detecte el nodo centinela actual, este bucle también se omitirá.

ConcurrentLinkedQueueBasado en características como nodos centinela, actualización CAS retrasada de los nodos principales y finales y garantía de visibilidad volátil, tiene un rendimiento muy alto y es relativamente adecuado para escenarios con grandes cantidades de datos, alta concurrencia, lectura y escritura frecuentes y operaciones. al principio y al final de la cola.CopyOnWriteArrayList

 

Finalmente (no lo hagas gratis, solo presiona tres veces seguidas para pedir ayuda~)

Este artículo se incluye en la columna " De punto a línea y de línea a superficie" para construir un sistema de conocimiento de programación concurrente Java en términos simples . Los estudiantes interesados ​​​​pueden seguir prestando atención.

Las notas y los casos de este artículo se han incluido en gitee-StudyJava y github-StudyJava . Los estudiantes interesados ​​pueden continuar prestando atención en stat ~

Dirección del caso:

Gitee-JavaConcurrentProgramming/src/main/java/F_Collections

Github-JavaConcurrentProgramming/src/main/java/F_Collections

Si tiene alguna pregunta, puede discutirla en el área de comentarios. Si cree que la escritura de Cai Cai es buena, puede darle me gusta, seguirla y recopilarla para respaldarla ~

Siga a Cai Cai y comparta más información útil, cuenta pública: la cocina privada back-end de Cai Cai

Lei Jun: La versión oficial del nuevo sistema operativo de Xiaomi, ThePaper OS, ha sido empaquetada. Una ventana emergente en la página de lotería de la aplicación Gome insulta a su fundador. El gobierno de Estados Unidos restringe la exportación de la GPU NVIDIA H800 a China. La interfaz de Xiaomi ThePaper OS está expuesto. Un maestro usó Scratch para frotar el simulador RISC-V y se ejecutó con éxito. Kernel de Linux Escritorio remoto RustDesk 1.2.3 lanzado, soporte mejorado para Wayland Después de desconectar el receptor USB de Logitech, el kernel de Linux falló Revisión aguda de DHH de "herramientas de empaquetado ": no es necesario construir la interfaz en absoluto (Sin compilación) JetBrains lanza Writerside para crear documentación técnica Herramientas para Node.js 21 lanzadas oficialmente
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/6903207/blog/10111678
Recomendado
Clasificación