blog

ReentrantLock 기본 구현

동기화와의 차이점, 잠금 및 잠금 해제 프로세스에 대한 자세한 설명, 관련 핵심 메서드의 세부 사항과 비교하여 Java에서 ReentrantLock의 구현을 심층적으로 분석하여 독...

Nov 1, 2025 · 10 min. read
シェア

ReentrantLock

Locking

private volatile int state; //잠금 상태, 잠금 성공 시 1, 재진입 시 +1, 잠금 해제 시 0

비공정 잠금 상태를 직접 설정할 수 있는지 확인하고, 설정할 수 있으면 바로 가져가고, 그렇지 않으면 잠금을 기다립니다.

java
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }

공정 잠금. 바로 대기실로 이동합니다.

java
final void lock() { acquire(1); }

acquire잠금 메서드

먼저 tryAcquire(arg)로 이동하여 잠금을 획득하려고 시도합니다. 그래도 잠금을 얻을 수 없다면 acquireQueued(addWaiter(Node.obj), arg); 대한 대기열에 자신을 추가하세요.

java
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

잠금을 추가하려는 시도 시도

이 글에서 '이'는 ReentrantLock 잠금입니다.

먼저 현재 스레드를 가져온 다음 재진입 잠금 상태가 0인지 확인합니다. 0은 잠겨 있지 않음을 의미하며, 잠금을 추가해 볼 수 있습니다. 시도하는 과정에서 다른 스레드를 강탈하지 못하고 잠금이 해제될 가능성이 분명히 있습니다.

java
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; }

hasQueuedPredecessors; 거꾸로 되어 있으므로 다음 항목으로 이동하여 확인하시기 바랍니다.

hasQueued전임자를 대기열에 넣어야 하는지 여부를 확인합니다.

java
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; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }

는 거짓을 반환하는 것으로만 간주됩니다.

이 진술에는 두 가지 판단이 있는데, 첫 번째 판단을 먼저 살펴 보겠습니다.

java
//t는 양방향 체인 테이블의 끝, h는 거짓이라고 생각하면 다음 문장을 읽을 필요가 없습니다. h == t

위의 경우 중 하나만 h! = t는 거짓을 반환합니다.

이 양방향 체인 테이블의 헤드 노드는 나중에 이를 탐색하기 위해 모두 null이므로 다음은 첫 번째 설명입니다.

이 문이 언제 거짓을 반환하는지 살펴봅시다.

java
(s = h.next) == null;

작업이기 때문에 그 뒤에 오는 문도 살펴봐야 합니다.

java
s.obj != Thread.a();

두 문이 모두 거짓을 반환하려면 Node 클래스의 스레드가 현재 스레드이고 헤드 노드의 다음 노드이므로 대기열이 필요하지 않다는 것과 동일합니다.

tryAcquire 메서드로 돌아갑니다.

java
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && //위의 단계가 끝나면 false가 true로 반전되어 CAS 연산이 수행됩니다. 위에서 말했듯이, 많은 스레드가 동시에 스레드라고 생각할 수 있으며, 동시에 잡을 수 있으며, cas는 하나의 스레드 만 잡을 수 있음을 보장 할 수 있습니다. 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; } }

addWaiter

이 메서드는 현재 스레드를 노드 객체로 캡슐화하여 리트란트락의 양방향 체인 테이블에 추가하는 것입니다.

Node.EXCLUSIVE 매개 변수에 대해서는 여전히 이해가 잘 안 됩니다.

java
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; } --------------------- //이 메서드는 체인 테이블의 끝에 있는 노드에 추가될 뿐만 아니라 체인 테이블의 초기화, 즉 여기서 NULL 노드 객체에 대한 스레드를 생성하는 역할도 합니다! private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize // if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }

위의 작업 후 acquireQueued(addWaiter(Node.a), arg); 메서드를 살펴봐야 하는데, 이 메서드가 참을 반환하면 현재 스레드는 이전 스레드가 잠금을 해제할 때까지 기다리는 WAIT 상태로 들어갑니다.

acquireQueued

이 방법의 주요 기능은 무엇인가요?

  1. 먼저 현재 노드의 부모 노드를 가져옵니다. 위에서 말했듯이 이 ReentrantLock 잠금은 큐 방식, 즉 깨어나는 순서에 따르므로 먼저 이런 상황을 생각해 봅시다. 모두 잠금을 잡으려는 스레드가 5개이고 이 시점에서 첫 번째 스레드가 이미 잠금을 잡았다고 가정해 봅시다. 따라서 이후의 모든 스레드는 잠금이 해제될 때까지 대기열에 대기해야 하며, 위의 addWaiter 메서드는 잠금을 얻지 못한 스레드를 체인 테이블에 넣습니다.

  2. 체인 테이블에 진입한 후에도 이 획득 대기 메서드가 실행되어 잠금을 획득하지 못한 스레드를 실제로 대기 상태로 만들고, 첫 번째 스레드가 잠금을 해제하면 체인 테이블의 다음 스레드를 순서대로 깨웁니다.

  3. 스레드를 대기 상태로 전환해도 여전히 두 번 회전합니다. 잠금을 잡고 있는 스레드가 잠금을 해제했는지 확인하고 해제했다면 획득을 시도하고, 해제하지 않았다면 대기 상태로 전환합니다.

    재입국자 잠금

    UNSAFE.park()는 스레드를 대기 상태로 전환합니다.

    UNSAFE.unpark()는 대기 상태 위치에 있는 스레드를 깨웁니다.

java
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 데드 루프는 첫 번째 if 루프에서 아직 잠금을 획득하지 못한 경우 대기 상태로 전환됩니다. for (;;) { // 현재 스레드 노드 객체의 부모 노드 가져오기 final Node p = node.predecessor(); /* * 부모 노드가 전체 체인 테이블의 헤드 노드라는 것은 현재 스레드가 두 번째 위치에 있고 첫 번째 노드가 잠금을 소유하고 있으며 이번에는 성공할 경우 잠금을 가져올 수 있다는 것을 의미합니다. * 그러나 부모 노드는 헤드 노드가 아니며, 현재 스레드 앞에 있는 스레드가 잠금을 잡으려고 대기하고 있기 때문에 현재 스레드는 잠금을 얻을 자격이 없습니다. * 현재 스레드가 두 번째 스레드인 경우, 잠금을 얻기 위해 tryAcquire를 실행하고 잠금을 얻은 후 자신을 헤드 노드로 설정한 다음 해당 노드 객체 스레드와 부모 노드를 비웁니다. * 부모 노드의 자식 노드가 비워지면 이때 체인 테이블의 첫 번째 노드가되고 스레드의 노드는 NULL이되며 이는 위와도 일치하며 체인 테이블 스레드의 첫 번째 요소는 NULL이어야합니다! */ 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); } }

shouldParkAfterFailedAquire 잠금을 획득하지 못했으니 대기해야 합니다.

위에서 언급했듯이 acquireQueued가 실행되면 두 번 회전하게 되는데, 두 번 회전할 때 무엇을 하고 어떻게 두 번 회전을 제어하는지는 모두 이 메서드로 조작됩니다.

  • 첫 번째 스핀에서 잠금을 획득할 수 없는 경우 이 방법을 사용합니다. 먼저 부모 노드의 대기 상태를 획득하는 것부터 시작합니다.
  • 이 값은 0이어야 합니다. 처음부터 지금까지 waitStatus에 대한 작업이 수행되지 않았으므로 0이어야 합니다. 그러면 다음 로직은 부모 노드의 waitStatus를 -1로 설정하여 전체 스레드가 절전 모드로 전환됨을 나타냅니다.
  • 처음에 -1로 설정하면 반환되는 값은 거짓이며 스레드를 parkAndCheckInterrupt; 대기 상태로 전환하지 않습니다.
  • 그런 다음 두 번째 회전을 수행하여 여전히 잠금을 획득할 수 없음을 확인하고 메서드에 다시 들어갑니다. 이제 부모 노드의 waitStatus가 -1이 되면 참을 반환하고 parkAndCheckInterrupt; 메서드로 이동하여 UNSAFE를 호출하여 스레드를 대기시킵니다.
java
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 첫 번째 입력은 0, 두 번째 입력은 2, 마지막 입력은 -1로 변경됩니다. int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * this” node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; // waitStatus 0이면 상태를 -1로 변경하고 false를 반환합니다. } 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. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

공원 및 체크 인터럽트는 스레드를 대기 상태로 전환합니다.

이 방법은 더 간단하며, LockSupport.park(this””) 호출하여 스레드를 대기 상태로 전환하는 것입니다.

java
private final boolean parkAndCheckInterrupt() { LockSupport.park(this”); return Thread.interrupted(); }

요약

이 시점에서 스레드는 잠금 획득과 관련하여 거의 모든 내용을 다루었으므로 여기서 다시 살펴보겠습니다.

우선, 스레드가 재입성 잠금의 잠금을 얻으려는 경우 몇 가지 방법이 있으며, 첫 번째는 재입성 잠금의 체인 테이블이 아직 초기화되지 않은 경우 직접 잠금 상태 CAS 수정으로 직접 가져오고 수정이 성공하면 잠금을 성공적으로 가져 오는 것입니다. 동시에 하나 이상의 스레드가 체인 테이블이 초기화되지 않은 단어가 발견되면 동시에 CAS, 체인 테이블의 초기화 인 잠금을 잡을 수없는 스레드가있을 수 있습니다.

체인 테이블은 NULL 스레드가 있는 노드를 헤드 노드로 인스턴스화한 다음 대기해야 하는 스레드를 헤드 노드 뒤에 새 노드 객체로 캡슐화하여 초기화합니다. 두 번 회전한 후에는 자체적으로 대기합니다.

잠금을 잡은 스레드가 잠금을 해제하면 대기열의 다음 스레드를 깨우도록 전환하고, 깨어난 스레드는 대기 상태였던 코드에서 깨어나 코드를 실행합니다. 그런 다음 깨어난 스레드는 데드 루프를 계속 진행하여 잠금을 획득할 수 있는지 알아낸 다음 이 스레드의 자체 로직을 실행합니다.

잠금 해제

잠금 해제 프로세스는 잠금을 획득하지 않은 스레드가 대기 상태에 있고 많은 논리적 전처리를 설계할 필요가 없기 때문에 잠금 프로세스보다 간단합니다.

잠금 해제에는 두 가지 단계가 있습니다.

  1. 잠금 상태 변경하기
  2. 다음 대기 스레드 깨우기
java
public void unlock() { sync.release(1); }

릴리스 잠금 해제 방법

이 방법에서는 먼저 스레드 잠금을 해제하려고 시도하고 성공하면 대기 중인 스레드가 있는지 확인하고 헤드 노드의 상태 값이 0이 아닌 경우 다음 대기 중인 스레드를 깨웁니다.

java
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; // 헤드 노드와 상태 값은 NULL이 아닙니다!= 0,다음 스레드를 깨웁니다. if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

tryRelease

이 방법은 비교적 간단하며 설명하지 않습니다.

java
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; }

unparkSuccessor는 다음 스레드를 깨웁니다.

여기서는 이 방법이 실제로 어떤 기능을 하는지 설명하는 데 중점을 둡니다.

들어오는 노드는 체인 테이블의 헤드 노드입니다.

  1. 헤드 노드의 상태 값을 가져옵니다. 0보다 작으면 0으로 변경됩니다.
  2. 헤드 노드의 자식을 가져옵니다.
  3. 자식 노드가 비어 있지 않으면 UNSAFE를 호출하여 자식 노드의 스레드를 깨웁니다.
java
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); }

그러면 자식 노드의 스레드가 깨어나서 잠금을 잡으러 가고, 스레드의 잠금과 잠금 해제가 여기서 이루어집니다.

재입력 잠금에는 설명할 API가 많은데, 이번에는 이 두 가지 메서드만 설명합니다.

Read next

판다 상세 튜토리얼

판다스는 강력한 데이터 처리 도구로, 이 튜토리얼에서는 기본 데이터 구조, 설치 방법, 데이터 읽기 및 쓰기, 정리 및 처리, 일반적인 작업의 데이터 분석 및 통계에 대해 소개합니다.

Nov 1, 2025 · 2 min read