고성능 동시 컨테이너 ConcurrentLinkedQueue를 한 번에 이해하기 위한 12장의 그림

고성능 동시 컨테이너 ConcurrentLinkedQueue를 한 번에 이해하기 위한 12장의 그림

 

머리말

이전 글에서는 동시 컬렉션의 구현과 특징에 대해 이야기했는데, 쓰기량이 많고 동시성이 큰 시나리오에는 적합하지 않다는 단점이 있습니다.CopyOnWeiteArrayList

이 문서에서는 동시 시나리오의 고성능에 대해 설명합니다.ConcurrentLinkedQueue

이 글을 읽는 데 약 10분 정도 소요됩니다.

이 기사를 읽기 전에 CAS, 휘발성 및 기타 지식을 이해해야 합니다.

CAS를 이해하지 못한다면 15,000단어, 6개의 코드 케이스, 5개의 회로도가 포함된 이 기사를 확인하여 동기화 의 두 번째 섹션을 완전히 이해하는 데 도움을 받을 수 있습니다.

휘발성을 이해하지 못한다면 5가지 사례와 순서도가 포함된 이 기사를 확인하여 0에서 1까지의 휘발성 키워드를 이해하는 데 도움을 받을 수 있습니다.

데이터 구조

ConcurrentLinkedQueue이름에서 알 수 있듯이 연결 목록으로 구현된 동시성과 대기열을 지원합니다.

이미지.png

소스 코드를 통해 필드를 사용하여 첫 번째 노드와 마지막 노드를 기록하고 노드 구현이 단방향 연결 목록임을 알 수 있습니다 .ConcurrentLinkedQueue

그리고 이러한 주요 필드는 모두 휘발성으로 수정됩니다. "잠금" 없이 읽기 시나리오에서 가시성을 보장하려면 휘발성을 사용하세요.

CAS 및 일부 오프셋 정보를 사용하여 안전하지 않은 필드 등 여기에 나열되지 않은 다른 필드도 있습니다.

   public  class  ConcurrentLinkedQueue < E >  확장  AbstractQueue < E > 
 구현 Queue < E > , java .io 직렬화 가능 { private     static class Node < E > { // 데이터 휘발성 E 항목 기록 ; //후속 노드 휘발성 Node < E > next ;       } //첫 번째 노드 private temporary 휘발성 Node < E > head ; //테일 노드 private temporary 휘발성 Node < E > tail ;   }              
   
          
           
             
           
             
 
       
           
       
           
 

초기화 중에 첫 번째 노드와 마지막 노드는 동시에 빈 스토리지 데이터가 있는 노드를 가리킵니다.

       공개  ConcurrentLinkedQueue () { 
 head = tail = new Node < E > ( null );       }               
 

 

 

디자인적 사고

첫 번째 및 마지막 노드의 지연된 업데이트

구현 원리를 살펴보기 전에 먼저 디자인 아이디어에 대해 이야기해 보겠습니다 . 그렇지 않으면 구현 원리를 이해하지 못할 수도 있습니다.ConcurrentLinkedQueue

ConcurrentLinkedQueue시나리오 작성에는 낙관적 잠금 아이디어가 채택되었으며 CAS+실패 재시도는 작업의 원자성을 보장하는 데 사용됩니다.

과도한 CAS 오버헤드를 피하기 위해 첫 번째와 마지막 노드의 업데이트를 지연시키는 아이디어를 채택하여 CAS 수를 줄였습니다.ConcurrentLinkedQueue

즉, 의 첫 번째 및 마지막 노드가 반드시 최신의 첫 번째 및 마지막 노드일 필요는 없습니다.ConcurrentLinkedQueue

 

 

센티넬 노드

ConcurrentLinkedQueue디자인에 센티넬 노드를 사용하세요

센티넬 노드란 무엇입니까?

Sentinel 노드는 가상 노드라고도 하며, Linked List와 같은 데이터 구조에 자주 사용됩니다.

단방향 연결 목록에서 노드를 추가하거나 삭제하려면 작업을 수행하기 전에 이 노드의 선행 노드를 가져와야 합니다.

첫 번째 노드를 운영할 때 첫 번째 노드 앞에 가상 노드(센티넬 노드)를 추가하면 특별한 처리가 필요하지 않다.

즉, 센티넬 노드를 사용하면 코드 복잡도를 줄일 수 있으며 , 링크드 리스트 관련 알고리즘을 공부한 학생들이라면 이에 대한 깊은 이해가 있을 것이라 믿습니다.

Sentinel 노드는 노드가 하나만 있는 경우에도 동시성 충돌을 줄일 수 있습니다.

이 기능은 후속 구현 및 흐름도를 읽은 후에만 이해할 수 있습니다.

 

 

소스코드 구현

ConcurrentLinkedQueue주요 업무는 팀에 들어오고 나가는 것인데, 우리는 이를 이용 하고 분석합니다.offerpoll

권하다

소스코드를 분석하기 전에 먼저 복잡한 변수의 역할을 설명하겠습니다.

t 레코드 테일 노드 테일

p는 노드의 루프 순회에 사용되며 p 노드가 실제 꼬리 노드인 경우에만 새 노드를 추가할 수 있습니다.

q는 p의 후속 노드를 기록하는 데 사용됩니다.

팀에 합류할 때 세 가지 상황이 있습니다.

  1. p의 후속 노드가 비어 있는 경우(p는 실제 tail 노드) CAS에서 새 노드를 추가하고 성공 후 tail 노드 tail을 업데이트해 봅니다.

  2. p가 p의 후속 노드와 동일한 경우(p의 다음은 자신을 가리키며 센티넬 노드로 구성됨을 나타내며, poll을 위해 dequeuing할 때 센티넬 노드가 구성될 수 있음을 나타냄) 이때 tail이 있는지 여부를 판단합니다. 노드가 수정되었으며, 테일 노드가 수정된 경우 이를 찾습니다. 테일 노드가 수정되지 않은 경우(next를 사용하여 계속 순회할 수 없음) 헤드 노드만 찾을 수 있습니다.

  3. 다른 경우에는 이때의 p가 실제 꼬리 노드가 아니므로 실제 꼬리 노드에 위치해야 함을 의미하며, 이때 p가 원래 꼬리 노드가 아니고 꼬리 노드가 수정된 경우 위치를 찾습니다. 그렇지 않으면 후속 노드를 찾습니다. 노드는 계속 순회됩니다.

두 번째와 세 번째 경우의 코드는 보기에는 매우 좋지만 가독성이 좋지 않습니다. 소스 코드 분석과 함께 요약을 볼 수 있습니다. 그래도 이해가 되지 않는 경우 이해하기 쉽도록 순서도가 있습니다.

       public  boolean  Offer ( E  e ) { 
 //널 포인터 확인 checkNotNull ( e ); // 새 노드 구축 final Node < E > newNode = new Node < E > ( e ); // 재시도 루프 실패 //t: 현재 레코드의 테일 노드 //p: 실제 테일 노드 //q: p의 후속 노드 for ( Node < E > t = tail , p = t ;;) { Node < E > q = p . next ; // 사례 1: p의 후속 노드가 비어 있어 현재 p가 실제 꼬리 노드임을 나타냅니다. if ( q == null ) { // p의 후속 노드를 새 노드로 수정하기 위해 CAS를 시도합니다 // If p의 다음이 null이면 새 노드로 교체합니다. newNode //실패하면 다른 스레드가 노드를 성공적으로 추가하고 계속 루프를 수행한다는 의미입니다. 성공하면 꼬리 노드를 업데이트할지 여부를 결정합니다. tail if ( p .casNext ( null , newNode )) { //p가 t와 같지 않으면 이때의 tail 노드가 Real tail 노드가 아니라는 뜻 //Try CAS: 현재 tail 노드가 t이면 새 값을 설정합니다. 노드를 꼬리 노드로 if ( p != t ) casTail ( t , newNode );   return true ;                   }               } //사례 2: p p의 후속 노드와 동일함 (p는 자신을 가리킴) else if ( p == q ) //t: 이전 꼬리 노드 // (t = tail): 새 꼬리 노드 //t != (t = tail): 설명 꼬리 노드가 수정되었습니다. p는 새 꼬리 노드와 동일합니다. 수정되었으므로 p는 헤드 노드 p = ( t != ( t = tail )) ? t : head ; //사례 3: p는 현재 노드에서 실제 꼬리가 아니므로 실제 꼬리를 찾아야 합니다. node else //p!=t:p는 더 이상 원래 꼬리 노드가 아닙니다. //t != (t = tail): 꼬리 노드가 수정되었습니다. //p는 더 이상 원래 꼬리 노드가 아닙니다. 그리고 꼬리 노드가 수정된 경우 p를 수정된 꼬리 노드와 동일하게 하고, 그렇지 않으면 p를 후속 노드 q와 동일하게 둡니다. p = ( p ! = t && t != ( t = tail )) ? t : q ;           }       }          
           
           
                
           
           
           
           
                
                  
               
                 
                   
                   
                   
                   
                       
                       
                         
                           
                        
 
                   
 
               
                  
                   
                   
                   
                        
               
               
               
                   
                   
                   
                            
 
 

 

투표

대기열에 넣기 제안의 변수를 이해하면 대기열에서 빼기 폴링도 이해하기 쉽습니다. 여기서 p와 q는 유사합니다.

h 레코드 헤드 노드 헤드

p는 노드의 루프 순회에 사용되며 p 노드가 실제 헤드 노드인 경우에만 대기열에서 제거될 수 있습니다.

q는 p의 후속 노드를 기록하는 데 사용됩니다.

팀을 떠나는 경우에는 네 가지 상황이 있습니다.

  1. p가 실제 헤드 노드인 경우 CAS는 데이터를 공백으로 설정한 후 head가 실제 헤드 노드인지 판단하고, 그렇지 않은 경우 헤드 노드를 업데이트한 후 자신 옆에 있는 원래 헤드 노드를 가리켜 센티넬 노드를 구축합니다. .

  2. p의 후속 노드가 비어 있으면 큐가 비어 있다는 의미이므로 CAS를 사용해 헤드 노드를 p로 변경해 보세요.

  3. p의 후속 노드가 그 자체인 경우 다른 스레드가 대기열에서 폴링하여 센티넬 노드를 구축하고 이 주기를 건너뛴다는 의미입니다.

  4. 다른 경우에는 뒤로 이동

      public  E  poll () { 
 //이중 루프를 종료하는 것이 편리합니다 restartFromHead : for (;;) { //h 레코드 헤드 노드 //p 실제 헤드 노드 //q는 p의 후속 노드입니다 for ( Node < E > h = head , p = h , q ;;) { //p 노드의 데이터를 가져옵니다. E item = p .item ; //사례 1: // 데이터가 비어 있지 않으면 p 노드가 실제 헤드 노드입니다. / /CAS를 사용하여 데이터를 null로 설정하고, 데이터가 item인 경우 null로 바꿉니다. 실패하면 다른 스레드와 대기열에서 제외되고 루프를 계속 진행합니다. if ( item ! = null && p . casItem ( item , null )) { //현재 헤드 노드가 실제 헤드 노드가 아닌 경우 헤드 노드를 업데이트합니다. if ( p != h ) updateHead ( h , (( q = p . next ) != null ) ? q : p ) ; return item ;                  } //사례 2: //p의 후속 노드가 비어 있음, 큐가 현재 비어 있음에 유의하세요. CAS를 시도하여 헤드 노드를 p로 변경해 보세요(p는 현재 센티널 노드일 수 있음) else if (( q = p . next ) == null ) { updateHead ( h , p ); return null ;                  } / /사례 3: //p의 후속 노드가 p 자체를 가리키는 경우 다른 스레드가 구성된다는 의미입니다. 폴링이 대기열에서 제거될 때 감시 노드로 사용되며 이 주기는 건너뜁니다. else if ( p == q ) continue restartFromHead ; //사례 4: // p를 후속 노드로 배치하려면 순회가 필요합니다. else p = q ;              }          }      }         
          
          
              
              
              
                   
                  
                     
                  
                  
                  
                      
                      
                        
                              
                       
 
                  
                  
                      
                      
                       
 
                  
                  
                     
                       
                  
                  
                  
                        
 
 
 

헤드노드를 업데이트하는 방식으로 판단하게 됩니다.

현재 헤드 노드가 실제 헤드 노드가 아닌 경우 CAS를 시도하여 헤드 노드를 p 실제 헤드 노드로 설정합니다.

CAS가 성공하면 원래 헤드 노드의 다음 노드를 자신을 가리키고 이를 센티널 노드에 구축합니다.

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

 

흐름도 구현

디버깅을 따르려는 학생은 아이디어에서 이 두 가지 설정을 꺼야 합니다. 그렇지 않으면 디버깅이 잘못됩니다.

이미지.png

이해를 돕기 위해 간단한 코드와 구현 흐름도를 살펴보겠습니다.

     공개  무효  testConcurrentLinkedQueue () { 
 ConcurrentLinkedQueue < String > queue = new ConcurrentLinkedQueue <> (); ​queue . 제안 ( "h1" ); 대기열 . 제안 ( "h2" ); 대기열 . Offer ( "h3" ); ​String p1 = queue . 설문조사 (); 문자열 p2 = 대기열 . 설문조사 (); 문자열 p3 = 대기열 . 설문조사 (); 문자열 p4 = 대기열 .  (); ​큐 . 제안 ( "h4" ); 시스템 . 밖으로 . println (  );     }            
 
         
         
         
 
            
            
            
            
 
         
         
 

[설명: 다이어그램의 노드 항목이 데이터를 쓰지 않으면 저장된 데이터가 비어 있음을 의미하고, 다음 노드가 포인팅 관계를 그리지 않으면 비어 있음을 의미합니다.]

생성을 실행할 때 첫 번째 노드와 마지막 노드는 빈 데이터가 있는 동일한 노드를 가리키도록 초기화됩니다.

이미지.png

처음으로 queue에 들어갈 때 루프에 들어가자마자 첫 번째 상황이 만족되는데, 이때 p가 실제 tail node이고 Direct CAS는 next를 새로운 node로 설정한다. tail, tail 노드 tail은 업데이트되지 않습니다.

따라서 첫 번째와 마지막 노드는 여전히 감시 노드이고 다음 감시 노드는 새로 가입된 노드를 가리킵니다.

이미지.png

두 번째로 queue에 합류하게 되면 이때 p(tail)가 실제 tail 노드가 아니기 때문에 세 번째 상황이 발생하는데, tail은 수정되지 않았기 때문에 p는 후속 노드로 변경되고 역방향 순회가 됩니다. 계속하다.

두 번째 루프에서는 p가 실제 tail 노드이므로 CAS는 새로운 노드를 추가하려고 하는데, 이때 p는 tail 노드 tail과 다르기 때문에 tail이 업데이트됩니다.

이미지.png

세 번째로 팀에 합류해도 상황은 처음과 같다.

이미지.png

이때 큐에는 sentinel 노드와 4개의 노드 h1, h2, h3이 있습니다.

처음 dequeue할 때 head가 가리키는 sentinel 노드의 데이터 필드가 비어 있으므로 네 번째 상황, 즉 p를 후속 노드로 변경하고 계속해서 뒤로 순회하는 상황이 발생합니다.

두 번째 루프에서 p는 h1 노드이며, 데이터가 비어 있지 않으므로 CAS는 데이터를 비어 있도록 설정합니다.

p.casItem(item, null)원래 h1 노드 데이터를 비어 있게 설정합니다.

이미지.png

이때 head는 실제 헤드 노드가 아니므로 head가 업데이트됩니다.

이미지.png

그런 다음 원래 헤드를 자체적으로 가리키고 더 이상 사용되지 않는 두 중간 노드의 GC를 용이하게 하기 위해 센티넬 노드에 구축합니다.

이미지.png

두 번째로 대기열에서 제거할 때 첫 번째 상황이 충족되면 CAS는 h2 노드 데이터를 비어 있는 상태로 직접 설정하고 헤드 노드는 업데이트되지 않습니다.

이미지.png

세 번째 팀 합류는 첫 번째 팀 합류와 유사하여 네 번째 상황을 만족시킵니다.

두 번째 주기에서는 CAS로 이동하여 데이터를 비우고, 헤드 노드를 업데이트하고, 원래 헤드 노드를 센티넬 노드로 설정합니다.

이미지.png

세 번째 상황은 네 번째로 dequeue하면 충족되지만 이때 p가 첫 번째 노드이므로 첫 번째 노드는 업데이트되지 않고 Null이 반환됩니다.

이 시점에서 우리는 tail node tail이 sentinel node에 있다는 것을 알 수 있는데, 만약 우리가 뒤로 순회한다면 우리는 결코 queue에 도달하지 못할 것입니다.

다시 enqueue 연산을 수행하여 두 번째 상황을 만족하는지 확인하고, p의 다음은 자신을 가리키며, 수정되지 않았으므로 p는 헤드 노드와 동일하여 큐로 돌아간다.

다른 주기에 들어가면 CAS는 h4를 추가한 다음 꼬리 노드 꼬리를 업데이트합니다.

이미지-20230913220256751

이 시점에서 이 간단한 예제는 팀에 합류하고 탈퇴하는 대부분의 프로세스를 다루고 있습니다. 센티넬 노드에 대해 이야기해 보겠습니다.

이 과정에서 센티넬 노드는 대기열에 노드가 하나만 있을 때 경합을 피할 수 있습니다.

 

 

요약하다

ConcurrentLinkedQueue단방향 연결 목록 구현을 기반으로 휘발성을 사용하여 가시성을 보장하므로 읽기 시나리오에서 다른 동기화 메커니즘을 사용할 필요가 없으며, 낙관적 잠금 CAS + 실패 재시도를 사용하여 쓰기 시나리오에서 작업의 원자성을 보장합니다.

ConcurrentLinkedQueue첫 번째와 마지막 노드의 업데이트를 지연시키는 아이디어를 활용하면 CAS 수를 크게 줄이고 동시성 성능을 향상시킬 수 있으며, 센티넬 노드를 사용하면 코드 복잡성을 줄이고 한 노드에 대한 경쟁을 피할 수 있습니다.

대기열 작업 중에 실제 꼬리 노드가 루프에서 발견되고 CAS를 사용하여 새 노드를 추가한 다음 CAS가 꼬리 노드 꼬리를 업데이트하는지 여부가 결정됩니다.

Enqueuing 작업의 루프 동안 노드는 일반적으로 뒤로 이동하며 Dequeuing 작업은 Sentinel 노드를 구성하므로 Sentinel 노드(다음 자신을 가리킴)로 결정되면 tail 노드 또는 헤드 노드가 위치에 따라 결정됩니다. 상황에 맞춰("점프 아웃")

대기열 제거 작업 중에 루프에서 실제 헤드 노드를 찾고 CAS를 사용하여 실제 헤드 노드의 데이터를 비워둔 다음 CAS가 헤드 노드를 업데이트하는지 여부를 결정하고 다음 이전 헤드 노드가 자신을 가리키도록 합니다. 센티넬 노드를 구축하는데, GC에 편리합니다.

큐 제거 작업 루프 동안 노드는 일반적으로 뒤로 이동합니다. 큐 제거는 센티넬 노드를 구축하므로 현재 센티넬 노드가 감지되면 이 루프도 건너뜁니다.

ConcurrentLinkedQueueSentinel 노드, 헤드 및 테일 노드의 CAS 업데이트 지연, 가시성 휘발성 보장 등의 기능을 기반으로 매우 높은 성능을 가지며 대용량 데이터, 높은 동시성, 빈번한 읽기 및 쓰기 및 작업이 있는 시나리오에 상대적으로 적합합니다. 대기열의 머리와 꼬리에 있습니다.CopyOnWriteArrayList

 

마지막으로 (공짜로 하지 마시고 3번 연속으로 눌러 도움을 청하세요~)

이 글은 Java 동시 프로그래밍 지식 시스템을 간단한 용어로 구축하기 위해 "점에서 선으로, 선에서 표면으로" 칼럼에 포함되어 있습니다 . 관심 있는 학생들은 계속해서 관심을 가져주시기 바랍니다.

본 글의 노트와 사례는 gitee-StudyJavagithub-StudyJava 에 수록되어 있으며 , 관심 있는 학생들은 stat~에서 계속 관심을 가져주세요~

케이스 주소:

Gitee-JavaConcurrentProgramming/src/main/java/F_Collections

Github-JavaConcurrentProgramming/src/main/java/F_Collections

궁금한 사항은 댓글로 토론 가능하며, 까이까이의 글이 좋다고 생각하시면 좋아요, 팔로우, 모아서 응원해주시면 됩니다~

까이까이를 팔로우하고 더 유용한 정보를 공유해보세요, 공개 계정: 까이까이의 백엔드 프라이빗 주방

레이쥔: 샤오미의 새로운 운영 체제인 ThePaper OS의 공식 버전이 패키징되었습니다. Gome App 복권 페이지의 팝업 창이 창업자를 모욕합니다. 미국 정부는 NVIDIA H800 GPU의 중국 수출을 제한합니다. Xiaomi ThePaper OS 인터페이스 마스터가 스크래치 를 사용하여 RISC-V 시뮬레이터를 작동시켰고 성공적으로 실행되었습니다. Linux 커널 RustDesk 원격 데스크톱 1.2.3 출시, 향상된 Wayland 지원 Logitech USB 수신기를 분리한 후 Linux 커널이 충돌했습니다. DHH "패키징 도구에 대한 날카로운 검토 ": 프런트 엔드를 전혀 구축할 필요가 없습니다(빌드 없음) JetBrains는 기술 문서를 작성하기 위해 Writerside를 출시합니다. Node.js 21용 도구 공식 출시
{{o.이름}}
{{이름}}

рекомендация

отmy.oschina.net/u/6903207/blog/10111678
рекомендация