Java 동시 프로그래밍의 초석 잠금 해제: AQS의 신비에 대한 심층 탐구

잠금 소개

Java에서 synchronized 키워드에 대한 심층 분석을 통해 배웠습니다 synchronized.동시 프로그래밍 스레드 동기화의 또 다른 중요한 구현에 대해 알아보겠습니다 Lock.

Lock인터페이스는 다음과 같이 공유 리소스에 대한 스레드 액세스를 제어하기 위한 메서드 집합을 정의합니다. 

Synchronized JVM이 암시적으로 잠금을 획득하고 해제해야 하는 동기화 잠금 과 비교할 때  Lock 동기화 잠금(이하 잠금이라고 함  Lock )은 잠금을 획득하고 해제하는 데 더 많은 유연성을 제공하는 잠금을 명시적으로 획득하고 해제해야 합니다. Lock 잠금의 기본 동작은 낙관적 잠금을 통해 구현되지만  Lock 잠금도 차단되면 일시 중지되므로 여전히 비관적 잠금입니다. 각각의 특성을 이해하기 위해 그림을 통해 다음 두 동기화 잠금을 간단히 비교할 수 있습니다.

Lock잠금 해제 및 선점을 위한 추상 메서드만 제공하는 인터페이스이며 다음과 같은 특정 구현이 JUC에서 제공됩니다.

  • ReentrantLock, 재진입 잠금은 배타적 잠금 유형에 속하며 synchronized유사한 기능을 가지고 있습니다.
  • ReentrantReadWriteLock(RRW), 재진입 읽기-쓰기 잠금, 이 클래스에는 두 개의 잠금이 유지되며 하나는 ReadLock인터페이스 WriteLock구현하는 것 입니다.Lock
  • StampedLockReentrantReadWriteLock의 향상된 버전인 Java 8에 도입된 새로운 잠금 메커니즘입니다 .

이들은 모두 종속  클래스 AbstractQueuedSynchronizer(AQS)에 의해 구현됩니다 .

AQS 소개

AQS는 추상 클래스로 주로 리소스 동기화 상태 변수의 값(상태) 대기열 스레드를 저장하기 위한 CLH 양방향 대기열을 유지 하는 동시에 스레드 차단 대기 및 깨어날 때 잠금 할당 메커니즘입니다. 구체적인 구조는 다음과 같습니다.

AQSint 유형의 멤버 변수를 사용하여 volatile동기화 상태를 나타내고, 내장된 선입선출 큐를 통해 리소스 획득 스레드의 큐잉 작업을 완료하고, 리소스를 CLH점유하려는 각 스레드를 노드 노드로 캡슐화하여 실현합니다. 잠금 할당 State 값 수정은 CAS를 통해 완료됩니다.

CLH 잠금은 실제로 논리 큐 비 스레드 기아에 기반한 스핀 페어 잠금의 일종으로 Craig, Landin 및 Hagersten 세 명의 마스터가 발명했기 때문에 CLH 잠금이라고 합니다.

AQS에는 내부 클래스가 있습니다 Node. 노드 노드는 리소스 획득을 기다리는 각 스레드를 캡슐화합니다. 여기에는 스레드 자체와 동기화가 필요한 대기 상태(예: 차단 여부, 깨우기를 기다리고 있는지 여부, 등이 취소되었습니다.

여기서 노드 노드가 현재 차단된 요청 자원 쓰레드를 저장할 것임을 직관적으로 알 수 있으며 변수 waitStatus는 현재 노드 노드의 대기 상태를 나타냅니다.다음과 같은 다섯 가지 값이 있습니다.

  • CANCELLED (1): 현재 노드가 스케줄링을 취소했음을 나타냅니다. 시간이 초과되거나 중단되면(응답이 중단된 경우) 이 상태에 대한 변경을 트리거하고 이 상태에 들어간 후 노드는 더 이상 변경되지 않습니다.
  • SIGNAL (-1): 후속 노드가 현재 노드가 깨어나기를 기다리고 있음을 나타냅니다. 후속 노드가 대기열에 합류하면 선행 노드의 상태가 SIGNAL로 업데이트됩니다.
  • CONDITION (-2): 노드가 Condition을 기다리고 있음을 나타내며, 다른 스레드가 Condition의 signal() 메서드를 호출하면 CONDITION 상태의 노드는 대기 큐에서 동기화 큐로 이동 하여 . 동기화 잠금.
  • PROPAGATE (-3): 공유 모드에서 선행 노드는 후속 노드를 깨울 뿐만 아니라 후속 노드도 깨울 수 있습니다.
  • 0 : 새 노드가 대기열에 추가될 때의 기본 상태입니다.

AQS의 주요 방법은 다음과 같습니다.

  1. Acquire(): 쓰레드가 싱크로나이저의 상태를 얻기 위해 사용하며, 획득할 수 없는 경우 스레드는 차단 상태에 들어가고 다른 쓰레드가 싱크로나이저를 해제할 때까지 기다립니다.
  2. release(): 스레드가 동기화 상태를 해제하고 대기 큐에 있는 다른 스레드를 깨우는 데 사용됩니다.
  3. tryAcquire(int): 독점 모드. 리소스 가져오기를 시도하고 성공하면 true를 반환하고 실패하면 false를 반환합니다.
  4. tryRelease(int): 독점 모드. 리소스 해제를 시도하고 성공하면 true를 반환하고 실패하면 false를 반환합니다.
  5. tryAcquireShared(int): 공유 방법. 자원을 가져오십시오. 음수는 실패를 나타내고 0은 성공을 나타내지만 남은 리소스가 없으며 양수는 리소스가 남아 있는 성공을 나타냅니다.
  6. tryReleaseShared(int): 공유 방법. 리소스 해제를 시도하고 후속 대기 노드가 해제 후 깨어나도록 허용되면 true를 반환하고 그렇지 않으면 false를 반환합니다.

ReentrantLock을 통한 AQS 구현 분석

ReentrantLockJUC의 동시성 패키지에서 중요하고 일반적으로 사용되는 동기화 장치이며 하단 레이어는 AQS에 의존하여 실현됩니다. ReentrantLock아래에서 구현을 자세히 살펴보겠습니다 .

잠금 프로세스 획득

ReentrantLock 선제적 잠금 상호 작용 다이어그램:

ReentrantLockSyncNofairSyncAQSlocklockacquiretryAcquirenofairTryAcquiretrue/false선점 잠금 성공 및 선점 add 실패 판단 addWaiterReentrantLockSyncNofairSyncAQS

전체 프로세스는 다음과 같습니다.

잠금 소스 코드 분석 가져오기

이러한 메서드의 소스 코드를 살펴보겠습니다.

lock.lock()이때 점프하는 ReentrantLock내부 클래스 NonfairSync(NonfairSync继承自AQS)의 메서드를 엽니다 lock()

습득하다

acquire()방법은 AQS의 방법입니다. 여기서 스레드는 리소스 잠금 및 점유에 실패하고 스레드는 CLH 대기열에 배치되어 알림이 코어 입구를 깨울 때까지 기다립니다.

 핵심 프로세스 단계는 다음과 같습니다.

  1. tryAcquire()는 리소스를 직접 획득하려고 시도하고 성공하면 직접 반환합니다( 여기서는 불공평한 잠금을 반영합니다. 각 스레드는 잠금을 획득할 때 한 번 선점 및 차단을 시도하며 CLH 대기열에서 대기 중인 다른 스레드가 있을 수 있습니다 ).
  2. addWaiter()는 스레드를 대기 큐의 끝에 추가하고 독점 모드로 표시합니다.
  3. AcquireQueued()는 리소스를 획득하기 위해 대기 큐에서 스레드를 차단하고 리소스를 획득한 후에만 반환합니다. 전체 대기 프로세스 중에 중단되면 true를 반환하고 그렇지 않으면 false를 반환합니다.
  4. 대기 중에 스레드가 중단되면 응답하지 않습니다. 자체 인터럽트 selfInterrupt()는 인터럽트를 보충할 자원을 확보한 후에만 수행됩니다.

시도획득

FairSync.tryAcquire의 구현은 다음과 같습니다.

 
 

자바

코드 복사

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

이 코드는 AQS(AbstractQueuedSynchronizer)에서 nonfairTryAcquire 메서드를 구현한 것입니다. 이 방법은 동기화 장치의 상태를 얻기 위해 부당하게 시도하는 데 사용됩니다.

  1. 먼저 현재 스레드를 가져오고 현재 상태 값을 가져옵니다. c.
  2. 현재 상태 값 c가 0이면 동기화 장치가 현재 다른 스레드에 의해 점유되고 있지 않음을 의미합니다. 이 경우 직접 compareAndSetState 메서드를 사용하여 상태를 0에서 획득으로 수정합니다(예상 값은 0이고 업데이트 값은 획득). 수정에 성공하면 현재 스레드가 동기화 장치를 성공적으로 획득했음을 의미하며 현재 스레드가 배타적 잠금의 소유자로 설정되고 true를 반환합니다.
  3. 현재 상태 값 c가 0이 아니면 동기화 장치가 다른 스레드에 의해 점유되었음을 의미합니다. 이때, 현재 쓰레드가 싱크로나이저의 배타적 잠금 소유자인지 판단해야 하는데, 그렇다면 현재 상태 값에 획득을 더하고(잠금 유지 횟수 증가) 참을 반환한다.
  4. 위의 조건 중 하나라도 충족되지 않으면 현재 스레드가 동기화 장치의 상태를 얻을 수 없음을 의미하며 false를 반환합니다.

addWaiter

대기 큐 CLH의 끝에 현재 쓰레드를 추가하고 현재 쓰레드가 위치한 노드를 반환하는 메소드

 
 

자바

코드 복사

private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }

  1. 먼저 현재 스레드와 지정된 대기 모드 모드를 포함하는 새 노드 Node를 만듭니다.
  2. 빠른 경로를 사용하여 대기열에 새 노드를 추가하려고 시도합니다. 먼저 현재 대기열의 꼬리 노드 pred를 가져옵니다. 꼬리 노드가 null이 아니면 새 노드의 prev를 pred로 가리키고 compareAndSetTail 메서드를 사용하여 꼬리 노드를 새 노드로 업데이트합니다. 업데이트가 성공하면 pred된 원래 테일 노드의 다음 노드가 새 노드를 가리키고 새 노드를 반환합니다.
  3. 빠른 경로가 실패하면, 즉 꼬리 노드가 null이거나 compareAndSetTail이 실패하면 다른 스레드가 큐를 동시에 수정하고 있음을 의미합니다. 이때 완전한 enqueue 작업(enq)을 사용하여 새 노드를 대기열에 추가해야 합니다.
  4. enq 방법에서 새 노드는 스핀 작업에 의해 대기열의 꼬리에 먼저 추가됩니다.
  5. 새 노드를 반환합니다.

이 메서드의 기능은 현재 스레드를 대기 중인 스레드로 동기화 큐에 추가하는 것입니다. 낙관적 전략을 사용하여 먼저 빠른 경로를 사용하여 대기열의 꼬리에 새 노드를 추가하려고 시도하고 전체 대기열 작업이 사용되는 데 실패합니다. 이러한 방식으로 대부분의 경우 경쟁 및 스레드 차단을 피할 수 있으며 동시성 성능을 향상시킬 수 있습니다.

대기 중 획득

이 메소드는 동기화 큐에서 잠금을 획득하는 데 사용되며 잠금을 직접 획득할 수 없는 경우 현재 스레드를 대기 큐에 추가하고 회전하고 대기합니다.

 
 

이것

코드 복사

final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

  1. 먼저 잠금을 획득하는 과정에서 예외가 발생했는지 여부를 기록하기 위해 플래그 비트 failed를 참으로 설정합니다.
  2. 잠금이 성공적으로 획득되거나 중단될 때까지 try-catch-finally 블록을 반복합니다.
  3. 루프에서 먼저 현재 노드의 선행 노드 p를 획득하고 현재 노드의 선행 노드가 헤드 노드인지 여부를 판단하고 성공적으로 잠금 획득을 시도합니다(tryAcquire 메서드 호출). 그렇다면 현재 노드를 새로운 헤드 노드로 설정하고 현재 노드에서 원래 헤드 노드를 연결 해제하고 선행 노드의 다음 포인터를 null로 설정하고 마지막으로 실패 플래그 비트를 거짓으로 설정하고 중단된 상태로 돌아갑니다. .
  4. 전구체 노드 p가 조건에 맞지 않으면 shouldParkAfterFailedAcquire 메서드를 호출하여 현재 스레드를 차단해야 하는지 여부를 결정하고 parkAndCheckInterrupt 메서드를 호출하여 스레드를 차단하고 스레드가 중단되었는지 확인합니다. 중단된 경우 중단된 상태를 true로 설정합니다.
  5. 2단계로 돌아가 잠금 또는 차단 획득을 계속 시도합니다.
  6. 루프 종료 시에도 잠금을 획득할 수 없는 경우, 즉 잠금 획득 과정에서 예외가 발생하면 cancelAcquire 메서드를 호출하여 현재 노드의 획득 작업을 취소합니다.

shouldParkAfterFailed획득

이 메서드는 잠금 획득이 실패한 후 현재 스레드를 차단해야 하는지 여부를 결정하는 데 사용됩니다.

 
 

이것

코드 복사

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

  1. 먼저 전구체 노드 pred의 waitStatus를 가져옵니다.
  2. 대기 상태 waitStatus가 Node.SIGNAL과 같으면 선행 노드가 신호를 보내기 위해 잠금을 해제하도록 상태를 설정했기 때문에 현재 노드가 안전하게 차단하고 true를 반환할 수 있음을 의미합니다.
  3. 대기 상태 waitStatus가 0보다 크면 선행 노드가 취소되었음을 의미합니다. 대기 상태가 0보다 작거나 같은 선행 노드 pred가 발견될 때까지 건너뛴 선행 노드와 그 이전의 취소된 노드를 반복합니다. 그런 다음 현재 노드의 prev 포인터를 발견된 선행 노드 pred로 업데이트하고 선행 노드 pred의 다음 포인터가 현재 노드 노드를 가리키도록 합니다.
  4. 대기 상태 waitStatus가 0 또는 Node.PROPAGATE이면 현재 노드를 깨우기 위해 신호가 필요함을 나타내지만 즉시 차단되지는 않습니다. compareAndSetWaitStatus 메서드를 호출하여 Node.SIGNAL로 pred된 선행 노드의 대기 상태를 변경하여 신호가 필요함을 나타냅니다.
  5. 현재 스레드를 차단할 필요가 없음을 나타내는 false를 반환합니다.

소스 코드 분석 잠금 해제

터놓다

NonfairSync(NonfairSync继承自AQS)방법 unlock():

 
 

자바

코드 복사

public void unlock() { sync.release(1); }

풀어 주다

 
 

자바

코드 복사

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

시도 릴리스

이 메서드는 잠금 리소스를 해제하는 데 사용됩니다.

 
 

자바

코드 복사

protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }

  1. 먼저, 상태 값 c = getState()를 얻습니다. - 잠금이 해제된 후 새 상태를 나타내는 현재 잠금 해제.

  2. 현재 스레드가 배타적 잠금의 소유자인지 확인하고 그렇지 않은 경우 IllegalMonitorStateException을 발생시킵니다.

  3. 새로운 상태 c에 따른 프로세스:

    • 새로운 상태 c가 0이면 잠금이 완전히 해제되었음을 의미합니다. exclusiveOwnerThread를 null로 설정하여 현재 소유자가 없음을 나타내고 free 플래그를 true로 설정합니다.
    • 그렇지 않으면 업데이트 잠금의 상태는 새 상태입니다. c.
  4. 잠금이 완전히 해제되었는지 여부를 나타내는 free 플래그를 반환합니다.

unparkSuccessor

위의 리소스 해제가 성공적으로 true를 반환하면 unparkSuccessor()대기 큐의 다음 스레드가 실행되어 깨어납니다.

 
 

scss

코드 복사

private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

  1. 먼저 노드의 대기 상태 ws를 판단하여 ws가 0보다 작으면 0으로 설정해 봅니다. 이것은 신호가 필요할 수 있는 사전 클리어 상태입니다. 이 작업이 실패하거나 대기 중인 스레드가 상태를 변경하더라도 후속 작업에는 영향을 미치지 않습니다.
  2. 노드의 후속 노드를 얻으십시오. 일반적으로 후속 노드는 현재 노드의 다음 노드입니다. 그러나 후속 노드가 취소되었거나 비어 있으면 취소되지 않은 후속 노드를 찾기 위해 꼬리에서 앞으로 트래버스합니다. 이것은 정말로 깨워야 할 노드를 찾는 것입니다.
  3. 깨울 필요가 있는 후속 노드를 찾으면 LockSupport.unpark(s.thread) 메서드를 호출하여 해당 노드에 해당하는 스레드를 깨웁니다.

공정한 잠금 및 불공평한 잠금

ReentrantLock공정한 잠금과 불공평한 잠금으로 나뉘며 기본 구성 방법은 불공평한 잠금입니다.공정한 잠금을 구축해야 하는 경우 true 매개변수만 전달하면 됩니다.

 
 

자바

코드 복사

Lock lock = new ReentrantLock(true);

잠금 및 잠금 해제 프로세스는 위의 불공평한 잠금과 거의 동일하지만 몇 가지 세부 사항이 약간 다릅니다.

 
 

이것

코드 복사

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

여기서 부당한 잠금과 같이 리소스를 잠그고 점유하려고 시도하는 CAS를 사용하지 않고 큐에 들어가는 lock()직접 호출입니다 . acquire(1)잠금에 더 많은 hasQueuedPredecessors 논리가 있습니다.

 
 

자바

코드 복사

public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());

추천

출처blog.csdn.net/BASK2312/article/details/131305744