blog

AQS 소스 코드 이해 - ReentrantLock

FairLockPolicy 클래스의 FairSync 클래스: 위의 코드를 클릭하면 소스 코드의 획득 메서드로 이동하여 잠금을 해제할 수 있습니다....

Oct 10, 2025 · 12 min. read
シェア

공정한 잠금 전략

재입력 잠금 클래스의 페어싱크 클래스 소스 코드 해석:

static final class FairSync extends Sync {
 private static final long serialVersionUID = -3000897897090466540L;
 // 공정 잠금 항목
 // 인터럽트에 응답하지 않고 잠금 해제하기
 final void lock() {
 acquire(1);		// 
 }
 /**
 * Fair version of tryAcquire. Don't grant access unless
 * recursive call or no waiters or is first.
 * 탈취 성공: 재진입 잠금을 포함하여 참을 반환합니다.
 * 획득 실패: false 반환
 */
 protected final boolean tryAcquire(int acquires) {
 // 현재 스레드 가져오기
 final Thread current = Thread.currentThread();
 // AQS에서 현재 상태를 가져옵니다.
 int c = getState();
 // 조건 설정: c == 0 현재 AQS가 잠금 해제 상태임을 나타냅니다.
 if (c == 0) {
 //  : hasQueuedPredecessors()
 // 페어싱크는 공정한 잠금이므로 잠금을 획득할 때마다 잠금을 획득해야 하는 스레드보다 먼저 대기열에 대기 중인 대기자가 있는지 확인해야 합니다.
 // hasQueuedPredecessors()메서드가 참을 반환하면 현재 스레드 앞에 대기자가 있고 현재 스레드가 대기열에 대기해야 한다는 의미입니다.
 // hasQueuedPredecessors()이 메서드는 거짓을 반환하며, 이는 현재 스레드 앞에 대기자가 없으므로 잠금을 직접 획득할 수 있음을 의미합니다.
 //  : compareAndSetState(0, acquires) CAS여기에 입력할 조건 1에서 대기열에 스레드가 없으므로 CAS의 예상 값은 0이므로 상태 값을 설정합니다.
 //  : 현재 스레드가 잠금을 성공적으로 점유했음을 의미합니다.
 //  : 경합이 발생하여 현재 스레드가 잠금에 대한 경합에 실패했음을 의미합니다.
 if (!hasQueuedPredecessors() &&
 compareAndSetState(0, acquires)) {
 // 현재 스레드 앞에 대기 중인 스레드가 없고 잠금이 성공적으로 획득된 경우
 // 현재 스레드를 독점 잠금 스레드로 설정합니다.
 setExclusiveOwnerThread(current);
 return true;
 }
 }
 // 이 지점에 도달하면 어떻게 되나요?!= 0 또는 c> 0
 // 이 경우 ReentrantLock은 재진입 잠금이므로 현재 스레드가 독점 잠금 스레드인지 확인해야 합니다.
 else if (current == getExclusiveOwnerThread()) {
 // 재진입자 잠금 로직
 // nextc 의 상태 값을 업데이트하는 메서드입니다.
 int nextc = c + acquires;
 // 재진입의 깊이가 매우 깊을 때 범위를 벗어난 판단은 다음c<0, state잠금의 최대값은 int이며, int 값이 최대값에 도달하면 잠금이 해제됩니다.+1... ..
 if (nextc < 0)
 throw new Error("Maximum lock count exceeded");
 // 상태 값 설정하기
 setState(nextc);
 return true;
 }
 // 여기서 일어나는 일
 // 1. c==0CAS가 잠금을 해제하지 못하면 다른 스레드를 선점하지 않고 상태를 수정합니다.
 // 2. c!=0 >0현재 스레드가 독점 잠금 스레드가 아닌 경우
 return false;
 }
}
public final boolean hasQueuedPredecessors() {
 // The correctness of this depends on head being initialized
 // before tail and on head.next being accurate if the current
 // thread is first in queue.
 Node t = tail; // Read fields in reverse initialization order
 Node h = head;
 Node s;
 //  : h!=t 가 설정된 경우, 즉 큐에 노드가 있는 경우, 노드가 있는데 왜 조건 2 판단이 있을까요?
 //  : h.next == null, 빈 큐에 새 노드를 삽입할 때 새로 삽입된 노드의 이전 노드는 이전 노드와 연결을 설정하지만, 노드의 헤드가 될 때까지 잠금이 해제되지 않습니다..next아직 새 노드에 연결되지 않은 경우 b는 다음과 같이 표시됩니다.
 return h != t &&
 ((s = h.next) == null || s.thread != Thread.currentThread());
}

위의 코드를 클릭하면 AbstractQueuedSynchronizer 소스 코드의 획득 메서드로 이동합니다.

public final void acquire(int arg) {
 //  : tryAcquire(arg) 이 메서드는 ReentrantLock 클래스에서 재정의되며 잠금을 획득하려고 시도하여 획득에 성공하면 true를 반환하고 실패하면 false를 반환합니다.
 //  : acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
 // 2.1: addWaiter(Node.EXCLUSIVE) 현재 스레드를 노드로 래핑하여 대기열에 추가합니다.
 // 
 if (!tryAcquire(arg) &&
 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
 //인터럽트 플래그를 다시 true로 설정합니다.
 //여기에 selfInterrupt()가 필요한 이유는 무엇인가요??,그 이유는 acquireQueued()가 참을 반환하면 스레드가 언파크가 아니라 인터럽트에 의해 깨어났음을 의미하기 때문입니다.,
 //스레드가 인터럽트에 의해 깨어난 경우, 획득Queued는 Thread를 사용하여 스레드가 깨어난 방법을 결정하기 때문입니다..interrupted(),이 메서드는 판단 후 인터럽트 플래그도 지우므로 여기에 인터럽트 플래그를 다시 추가해야 합니다. "https://..com/question/399039232"
 selfInterrupt();
}
/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 * 현재 스레드가 형성한 노드를 반환합니다.
 */
private Node addWaiter(Node mode) {
 // 노드를 빌드하고 노드에 현재 스레드를 캡슐화합니다.
 Node node = new Node(Thread.currentThread(), mode);
 // Try the fast path of enq; backup to full enq on failure
 // 큐 노드
 // 대기열 끝에 있는 노드를 가져와 pred 변수에 저장합니다.
 Node pred = tail;
 // 조건 설정: 대기열에 이미 노드가 있는 경우
 if (pred != null) {
 // 프레드가 테일 노드이므로 프레드 지점이 이전 노드를 가리키면 잠금이 해제됩니다.
 node.prev = pred;
 // CAS 성공하면 노드가 성공적으로 대기열에 추가됩니다.
 if (compareAndSetTail(pred, node)) {
 // 이전 노드가 현재 지점을 가리키며 양방향 바인딩을 완료합니다.
 pred.next = node;
 return node;
 }
 }
 // 여기에서 실행할 때?
 // 1. 현재 대기열은 빈 꼬리입니다.= null
 // 2. CAS경쟁 실패
 enq(node);
 return node;
}
/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
private Node enq(final Node node) {
 // 스핀 큐잉, 포인트가 성공적으로 큐에 진입했을 때만 루프에서 점프합니다.
 for (;;) {
 Node t = tail;
 // 1. 현재 대기열은 빈 꼬리입니다.= null
 // 이는 현재 잠금이 점유되고 있으며 현재 스레드가 잠금 획득에 실패한 첫 번째 스레드일 수 있음을 의미합니다.
 if (t == null) { // Must initialize
 // 현재 잠금을 유지하는 스레드의 첫 번째 후속 스레드로서 무엇을 해야 할까요?
 // 1. 현재 잠금을 보유하고 있는 스레드가 잠금을 획득할 때 직접 tryAcquire가 성공하여 차단 대기열에 노드를 추가하지 않았으므로 차단 대기열에 진입하는 첫 번째 스레드로서 몇 가지 추가 처리가 필요합니다.
 // 2. 자신의 노드에 빈 노드를 추가합니다.
 // CAS성공, 포인트가 헤드가 될 때.nextNode
 if (compareAndSetHead(new Node()))
 tail = head;
 // 실행이 끝날 때 for 루프가 없다는 점에 주목하세요.....
 } else {
 // 큐가 null이 아닌 경우 일반 큐 진입, for는 무한 루프이므로 항상 큐에 성공적으로 진입할 수 있습니다.
 // 큐에 대기할 노드의 선행과 후행을 바인딩합니다.
 node.prev = t;
 // CAS테일 포인터 설정
 if (compareAndSetTail(t, node)) {
 // 폰과 전임자 노드의 백워드 바인딩
 t.next = node;
 return t;
 }
 }
 }
}
/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node 현재 스레드에 의해 래핑되어 현재 성공적으로 대기열에 들어간 노드입니다.
 * @param arg the acquire argument 이 메서드는 현재 스레드가 리소스를 성공적으로 점유한 후 상태 값을 설정하는 데 사용됩니다.
 * @return {@code true} if interrupted while waiting
 * true:현재 스레드가 잠금을 성공적으로 점유했습니다. 정상적인 상황에서는 현재 스레드가 조만간 잠금을 해제합니다.
 * false:실패 및 큐 아웃 로직을 수행해야 함을 나타냅니다.
 */
final boolean acquireQueued(final Node node, int arg) {
 boolean failed = true;
 try {
 // 현재 스레드가 중단되었는지 여부
 boolean interrupted = false;
 for (;;) { 
 // 왜 여기서 실행되나요?
 // 1. for 루프에서 스레드가 파킹되기 전에는
 // 2. 파킹 후 스레드가 깨어나면 여기에서도 실행됩니다.
 // 현재 이전 노드 가져오기
 final Node p = node.predecessor();
 // 조건 1 유지: p==head, 포인트가 헤드일 때 잠금을 해제하는 방법을 설명합니다..nextNode, head.next노드는 언제든지 잠금에 대해 경쟁할 수 있습니다.
 // 조건 2 보유: tryAcquire(arg) 헤드에 해당하는 스레드가 잠금을 해제했음을 의미합니다., head.next노드에 해당하는 스레드가 잠금을 획득합니다.
 // 조건 2가 유지되지 않음: head에 해당하는 스레드가 아직 잠금을 해제하지 않았습니다... head.next여전히 공원이 필요합니다.
 if (p == head && tryAcquire(arg)) {
 // 잠금을 획득한 후에는 어떻게 해야 하나요?
 // 자신을 헤드 노드로 설정
 setHead(node);
 // 이전 스레드의 노드에 대한 다음 참조를 null로 지정하여 해당 노드가 대기열에서 제외되도록 합니다.
 p.next = null; // help GC
 // 현재 스레드는 예외 없이 잠금을 획득합니다.
 failed = false;
 // 현재 스레드의 인터럽트 플래그 반환
 return interrupted;
 }
 //shouldParkAfterFailedAcquire이 메서드는 어떤 기능을 하나요? 현재 스레드가 잠금 리소스를 획득하지 못했을 때 중단해야 하나요?
 //반환 값: true ->현재 스레드는 false->  
 //parkAndCheckInterrupt()역할: 현재 스레드를 중단하고 깨어난 후 현재 스레드의 인터럽트 플래그를 반환합니다.
 //깨우기: 1: 일반 깨우기 다른 스레드 잠금 해제 2:다른 스레드는 현재 중단된 스레드에 인터럽트 신호를 보냅니다.
 if (shouldParkAfterFailedAcquire(p, node) &&
 parkAndCheckInterrupt())
 //interruped=true현재 노드에 해당하는 스레드가 인터럽트 신호에 의해 깨어났음을 나타냅니다.
 interrupted = true;
 }
 } finally { // 언제 여기로 갈까요? 그럴 것 같지는 않습니다.
 if (failed)
 cancelAcquire(node);
 }
}
/**
 *  
 * 1이 메서드에 처음 오면 취소된 상태의 노드를 교차하고, 두 번째로 오면 참을 반환한 다음 현재 스레드를 파킹합니다.
 * 2점의 이전 노드의 상태가 0이면 현재 스레드는 이전 노드의 상태를 -1로 설정하고, 이 메서드에 두 번째로 스핀이 오면 참을 반환한 다음 현재 스레드를 파킹합니다.
 * @param pred 현재 스레드 노드의 이전 노드
 * @param node 현재 스레드는 노드
 * @return {@code true} if thread should block
 * 반환값: boolean true는 현재 스레드가 중단되어야 함을 의미합니다.
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 // 이전 노드의 상태 가져오기
 // waitStatus: 0 기본 상태 -1 포인트가 잠금을 해제하여 첫 번째 후속 노드를 깨우는 신호 상태입니다.>0 포인트가 취소되었음을 나타냅니다.
 int ws = pred.waitStatus;
 // 조건은 참: 이전 노드가 현재 노드를 깨울 수 있는 노드이고, 참을 반환합니다.==>parkAndCheckInterrupt() park현재 스레드
 if (ws == Node.SIGNAL)
 /*
 * This node has already set status asking a release
 * to signal it, so it can safely park.
 */
 return true;
 //조건이 유지됩니다:>0,이전 노드가 취소되었음을 나타냅니다.
 if (ws > 0) {
 /*
 * Predecessor was cancelled. Skip over predecessors and
 * indicate retry.
 */
 // waitStatus가 0보다 큰 노드를 찾으면 이러한 노드는 취소됩니다.
 do {
 node.prev = pred = pred.prev;
 } while (pred.waitStatus > 0);
 // waitStatus가 0보다 크지 않은 노드를 찾은 후 waitStatus를 0으로 설정합니다.> 0 AbstractQueuedSynchronizer의 노드 컬링 대기열
 pred.next = node;
 } else {
 /*
 * waitStatus must be 0 or PROPAGATE. Indicate that we
 * need a signal, but don't park yet. Caller will need to
 * retry to make sure it cannot acquire before parking.
 */
 // 포인트의 이전 노드가 0이거나<0 
 // 현재 스레드 노드의 이전 노드의 상태를 SIGNAL로 강제 설정하여 이전 노드가 잠금 해제 후 나를 깨워야 함을 나타냅니다....
 // 명확하게 말하면, 스레드의 노드가 큐에 대기하려는 경우 이전 노드의 상태를 -1로 설정합니다.
 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
 }
 return false;
}
/**
 * Convenience method to park and then check if interrupted
 * 현재 스레드를 중단하고, 깨어나서 "인터럽트 신호" 깨우기 여부에 대해 현재 스레드로 돌아갑니다.
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
 // 현재 스레드 중단
 LockSupport.park(this);
 // 해제 후 다음을 수행합니다.
 return Thread.interrupted();
}
/**
 * Cancels an ongoing attempt to acquire.
 * 컨텐션에서 노드 제거하기
 * @param node the node
 */
private void cancelAcquire(Node node) {
 // Ignore if node doesn't exist
 if (node == null)
 return;
 // 대기열이 취소되었으므로 노드와 연결된 현재 스레드가 null로 설정됩니다.
 node.thread = null;
 // Skip cancelled predecessors
 // 현재 대기열에 없는 노드의 이전 노드를 가져옵니다.
 Node pred = node.prev;
 // 큐에 대기해야 하는 노드 건너뛰기
 while (pred.waitStatus > 0)
 node.prev = pred = pred.prev;
 // predNext is the apparent node to unsplice. CASes below will
 // fail if not, in which case, we lost race vs another cancel
 // or signal, so no further action is necessary.
 // 전임자의 다음 노드를 가져오려면 다음과 같은 경우가 있습니다.
 // 1.현재 노드
 Node predNext = pred.next;
 // Can use unconditional write instead of CAS here.
 // After this atomic step, other Nodes can skip past us.
 // Before, we are free of interference from other threads.
 // 현재 노드 상태를 취소된 상태 1로 설정합니다.
 node.waitStatus = Node.CANCELLED;
 // If we are the tail, remove ourselves.
 /**
 * 현재 대기열 취소 노드가 다른 대기열 위치에 있으며, 대기열 전략의 구현이 동일하지 않으며 세 가지 경우로 나뉩니다.
 * 1:현재 노드는 큐 꼬리의 꼬리입니다.> node
 * 2:현재 노드는 헤드가 아닙니다..next노드가 아닌
 * 3:현재 노드는 head.next 
 */
 // 첫 번째 경우
 //  : node=tail설정된 현재 노드는 큐 꼬리의 꼬리입니다.> node
 //  :compareAndSetTail(node,pred)성공하면 꼬리 수정이 완료된 것, 즉 현재 지점의 이전 지점이 꼬리로 설정되었음을 의미합니다.
 if (node == tail && compareAndSetTail(node, pred)) {
 // pred.next -> null,대기열에서 노드 완료하기
 compareAndSetNext(pred, predNext, null);
 } else {
 // If successor needs signal, try to set pred's next-link
 // so it will get one. Otherwise wake it up to propagate.
 // 노드 상태 저장
 int ws;
 // 두 번째 경우: 현재 노드가 헤드가 아닌 경우..next꼬리가 아닌 노드
 // 조건 1: pred!=head 이는 현재 노드가 헤드가 아니라는 것을 의미합니다..next노드가 아닌
 // 조건 2: ((ws= pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
 //  .1 (ws = pred.waitStatus) == Node.SIGNAL  :이는 노드의 이전 상태가 Signal임을 의미합니다.. 유효하지 않음: 이전 상태가 0일 수 있습니다.,
 // 극단적인 경우: 전임자도 대기열을 취소합니다.
 //  .2: (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))
 // 이전 상태가 ws 상태라고 가정합니다.<=0 그런 다음 전임자 상태를 Signal로 설정하여 후속 노드가 깨어나야 함을 나타냅니다.
 // if이 메서드가 하는 일은.next -> node.next, 프레드 노드 상태가 Signal인지 확인하기만 하면 됩니다.
 if (pred != head &&
 ((ws = pred.waitStatus) == Node.SIGNAL ||
 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
 pred.thread != null) {
 // 사례 2: 현재 노드가 헤드가 아닌 경우.next꼬리가 아닌 노드
 //  : pred.next -> node.next 노드에서.next노드가 깨어난 후 shouldParkAfterFailedAcquire를 호출하면 노드.next노드는 취소된 상태의 노드로 넘어가 실제 큐 아웃을 완료합니다.
 Node next = node.next;
 if (next != null && next.waitStatus <= 0)
 compareAndSetNext(pred, predNext, next);
 } else {
 // 현재 노드는 head.nextNode
 // 마찬가지로 사례 2에서도 후속 노드가 깨어날 때 shouldParkAfterFailedAcquire를 호출하여 노드를 해제합니다..next취소 상태를 교차하는 노드
 // 큐의 세 번째 노드는 이중 가리키기 관계로 헤드에 직접 연결됩니다.,head.next->세 번째 노드는 대기열의 헤드입니다..next노드 세 번째 노드.prev -> head
 unparkSuccessor(node);
 }
 node.next = node; // help GC
 }
}
/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
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)
 //-1 Signal 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.
 */
 // 조건 1:
 // s언제 잠금이 null과 같을까요?
 // 1.포인트가 테일 노드인 경우,s==null
 // 조건 2: s.waitStatus>0 전제 조건: s!=null
 // 확립됨: 현재 노드의 후속 노드가 취소되었음을 나타냅니다.... 깨울 수 있는 적절한 노드를 찾아야 합니다. 여기에서 다이어그램을 그려서 확인할 수 있습니다.
 /*
 head는 노드 0을 가리키고, s는 노드 1을 가리키며, t는 노드 5를 가리키고, 결국 s는 노드 3을 가리킵니다.
 0 1 2 3 4 5
 node -> [1] -> [1] -> [-1] -> [-1] -> [0]
 */
 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;
 // 위의 루프는 현재 노드에서 깨울 수 있는 가장 가까운 노드를 찾거나, 노드를 찾을 수 없거나, 노드가 null일 수 있습니다.
 }
 // 깨울 수 있는 적절한 노드가 발견되면 해당 노드가 깨어납니다...,찾을 수 없고 아무것도 하지 않습니다.
 if (s != null)
 LockSupport.unpark(s.thread);
}

잠금 해제를 위한 소스 코드

public final boolean release(int arg) {
 // 잠금을 해제하려고 시도하면 tryRelease는 현재 스레드가 잠금을 완전히 해제했음을 나타내는 true를 반환합니다.
 // 현재 스레드가 잠금을 완전히 해제하지 않았음을 나타내는 false를 반환합니다.
 if (tryRelease(arg)) {
 // head언제 생성되나요?
 // 잠금을 보유한 스레드가 잠금을 해제하지 않고 잠금을 보유하는 동안 잠금을 획득하려는 다른 스레드가 있고, 다른 스레드가 잠금을 획득할 수 없어 큐가 비어 있는 경우 후속 스레드는 현재 잠금을 보유한 스레드의 헤드 노드를 생성합니다. 후속 스레드는 헤드 노드에 추가됩니다.
 Node h = head;
 // 조건 1이 참입니다: 대기열의 헤드가 초기화되었고, ReentrantLock이 사용 중 멀티스레드 경합이 발생했습니다.
 // 조건 2가 참입니다: 헤드가 뒤에 노드를 삽입했어야 합니다.
 if (h != null && h.waitStatus != 0)
 // 후속 노드 깨우기
 unparkSuccessor(h);
 return true;
 }
 return false;
}
Read next

데이터 마술사: 손실된 데이터를 복구하고 ClkLog에서 데이터 업데이트를 구현하는 방법

이러한 유형의 문제에 대한 강력한 솔루션이 ClkLog에 있습니다. 시나리오 1: 네트워크 및 기타 이유로 인해 누락된 데이터에 의해 생성된 타이밍 스크립트가 실행되지 않는 경우 예를 들어 누락된 데이터의 테이블에 지정된 값을 채우기 위해

Oct 10, 2025 · 2 min read