공정한 잠금 전략
재입력 잠금 클래스의 페어싱크 클래스 소스 코드 해석:
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;
}




