Java의 AQS 소스 코드를 분석하기 위한 6K 단어

최근 인터뷰에서 많은 지원자들에게 Java 잠금에 대한 지식에 대한 질문을 받았습니다.모든 사람의 이해는 기본적으로 "고정 에세이"의 단계에 있음을 느낄 수 있습니다.본질적으로 Java 잠금 및 다중 스레드 동기화 메커니즘의 기본 원칙은 아주 잘 이해했습니다. 이미 인터넷에 이런 종류의 기사가 많이 있지만 많은 기사를 읽은 후에는 오래된 것입니다.AQS의 addWaiter 메소드와 같은 일반적인 것은 JDK16에서 볼 수 없습니다.코드가 리팩토링되었을 수 있습니다.

글을 통해 저도 소스코드를 읽는 일반적인 습관을 정리했습니다. AQS의 전체 이름은 AbstractQueuedSynchronizer입니다.첫 번째는 추상 클래스입니다.두 번째는 큐잉을 위해 큐를 사용하고 스레드 간의 동기화를 위해 사용합니다. 그는 CountDownLatch, 읽기-쓰기 잠금, 재진입 잠금 등을 포함하여 Java의 모든 잠금의 기반이며 모두 AQS를 기반으로 구현됩니다. 표범을 엿볼 수 있는 ReentrantLock부터 시작하겠습니다. AQS의 소스 코드를 살펴봐야 할 것 같습니다.

가장 먼저 이해해야 할 것은 사용 방법인데, ReentrantLock의 일반적인 사용 방법은 이 클래스의 주석에 작성되어 있습니다.

이미지.png

다음으로 조감도를 갖고 구현을 큰 수준에서 살펴봐야 합니다.ReentrantLock은 실제로 패키징의 외부 레이어에 불과합니다.실제로 내부는 실제로 Sync 클래스에 의해 구현되며 이 클래스에는 두 개의 하위 클래스가 있습니다. 클래스는 각각 FairSync와 NonfairSync인데, 이 두 서브클래스는 initialTryLock과 trayAcquire의 두 메서드를 오버라이드하는 것을 볼 수 있는데, 이는 이 두 메서드의 구현에 약간의 차이가 있을 것임을 보여주며, 이 두 메서드의 구현에는 약간의 차이만 있을 뿐입니다. .

이미지.png

이제 두 가지 방법, 즉 잠금 및 tryLock 방법으로 시작합니다. 두 방법의 차이점은 try가 있는 방법은 시도에 불과하다는 것입니다. 최선을 얻을 수 있으면 얻을 수 없으면 잊어버리십시오. 반환 당신이 그것을 얻지 못했다고 말하는 거짓. 잠금. 잠금 구현은 다음과 같습니다.

이미지.png

그가 호출한 것이 하위 클래스의 initialTryLock 메서드임을 알 수 있고 initialTryLock 메서드를 보면 그의 기본 구현인 NoFairSync를 사용합니다. 방법도 매우 간단한데 CAS를 통해 락 획득을 시도해보고 성공하면 현재 오너를 코스트 쓰레드로 설정하고 실패하면 현재 오너가 쓰레드인지 확인하고 맞으면 직접 state+1 , Counting이 구현됨, 즉 재진입 기능이 구현되지 않은 경우 false를 반환하고 잠금 획득에 실패했습니다. 실패하면 취득(1)이 호출됩니다.

이미지.png

이 메서드는 AQS 추상 클래스에서 제공하는 메서드로 서브 클래스의 tryAcquire 메서드를 호출하고 NoFiairSync 구현을 살펴보자.

이미지.png

여기서 CAS는 다시 시도하기 위해 다시 호출됩니다.

이미지.png

여전히 실패하면 AQS 획득 구현에 들어갑니다.이 방법은 가장 복잡하고 이해하기 어렵습니다.AQS의 핵심입니다.한눈에 매개 변수에서 매우 복잡해 보입니다.공유와 호환되는지 여부 공유, 중단 가능 여부, 시간 초과 등으로 인해 복잡성이 발생하므로 여전히 현재 장면, 즉 획득(null, arg, false, false, false, 0L); 공유 없음, 중단 없음 , 타임아웃 없음....

이미지.png

메서드의 본문은 다음과 같으며 일부만 붙여넣고 전부는 붙여넣지 않습니다.

 
 

이것

코드 복사

for (;;) { if (!first && (pred = (node == null) ? null : node.prev) != null && !(first = (head == pred))) { if (pred.status < 0) { cleanQueue(); // predecessor cancelled continue; } else if (pred.prev == null) { Thread.onSpinWait(); // ensure serialization continue; } } if (first || pred == null) { boolean acquired; try { if (shared) acquired = (tryAcquireShared(arg) >= 0); else acquired = tryAcquire(arg); } catch (Throwable ex) { cancelAcquire(node, interrupted, false); throw ex; } if (acquired) { if (first) { node.prev = null; head = node; pred.next = null; node.waiter = null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } return 1; } } if (node == null) { // allocate; retry before enqueue if (shared) node = new SharedNode(); else node = new ExclusiveNode(); } else if (pred == null) { // try to enqueue node.waiter = current; Node t = tail; node.setPrevRelaxed(t); // avoid unnecessary fence if (t == null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); // back out else t.next = node; } else if (first && spins != 0) { --spins; // reduce unfairness on rewaits Thread.onSpinWait(); } else if (node.status == 0) { node.status = WAITING; // enable signal and recheck } else { long nanos; spins = postSpins = (byte)((postSpins << 1) | 1); if (!timed) LockSupport.park(this); else if ((nanos = time - System.nanoTime()) > 0L) LockSupport.parkNanos(this, nanos); else break; node.clearStatus(); if ((interrupted |= Thread.interrupted()) && interruptible) break; } }

그는 다중 루프 + if 조건을 통해 함수의 다중 기능 목적을 달성하기 위해 무한 루프를 만들었습니다.

첫 번째 루프는 먼저 여기로 이동하고 먼저 노드를 초기화합니다.

 
 

이것

코드 복사

if (node == null) { // allocate; retry before enqueue if (shared) node = new SharedNode(); else node = new ExclusiveNode(); }

여기서 두 번째 주기는 tail에 현재 노드를 추가하고 prev가 이전 노드를 가리키고 이전 노드의 다음 노드가 현재 노드를 가리키도록 하면 연결된 목록을 구현하는 것을 볼 수 있습니다.

 
 

이것

코드 복사

else if (pred == null) { // try to enqueue node.waiter = current; Node t = tail; node.setPrevRelaxed(t); // avoid unnecessary fence if (t == null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); // back out else t.next = node; }

설명을 위해 네트워크에 그림을 삽입하면 이 코드를 이해하는 데 더 도움이 됩니다. AQS에는 데이터 구조 Node가 있습니다. 이 데이터 구조에는 prev 및 next 속성이 있어 이중 연결 목록의 기능을 실현합니다. 각 호출은 잠금 스레드가 들어오고 잠금을 얻을 수 없으면 큐에 합류하고 대기한 다음 이 큐의 작업 프로세스는 위의 코드에 있습니다.

이미지.png

대기열에 노드가 없으면 현재 헤드 노드이며 잠금을 다시 획득하려고 시도하는 코드는 다음과 같습니다. 그렇지 않으면 park를 호출하여 현재 스레드를 일시 중단합니다.

 
 

이것

코드 복사

if (first || pred == null) { boolean acquired; try { if (shared) acquired = (tryAcquireShared(arg) >= 0); else acquired = tryAcquire(arg); } catch (Throwable ex) { cancelAcquire(node, interrupted, false); throw ex; } if (acquired) { if (first) { node.prev = null; head = node; pred.next = null; node.waiter = null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } return 1; } }

위 내용이 그 안에 있는 핵심 내용인데 쓰시면 아주 간단하죠? 쓰시면 어떻게 쓰실 건가요? 이 코드에 대한 비판은 동일한 함수가 노드 노드 초기화 및 대기열 업데이트를 포함하여 많은 작업을 수행하고 여러 루프 + if를 통해 구현되어 이해하기 어렵다는 것입니다.

마지막으로 우리는 스스로에게 몇 가지 질문을 던졌습니다.

1. 잠금이 해제되면 어떻게 됩니까? 코드도 비교적 간단합니다. 다음과 같이 잠금을 해제할 때 1을 전달합니다.

이미지.png

release(1)가 호출될 때마다 한 번 차감되며 이는 재진입 잠금 카운터의 감소에 해당합니다. 동시에 예외 호출이 고려되며, 다른 스레드가 현재 잠금을 해제하려고 하면 오류가 발생합니다. 동시에 잠금이 해제되면 상태를 0으로 설정하고 현재 스레드를 비우고 잠금을 완전히 해제합니다.

이미지.png

다음은 LockSupport.unpark(s.waiter);를 사용하여 큐에 삽입된 첫 번째 노드인 헤드 노드를 깨우는 SinalNext(head)입니다.

2. 공정한 잠금과 불공평한 잠금의 차이점은 무엇입니까? 오랫동안 지켜본 결과 공정한 잠금이든 불공평한 잠금이든 논리는 같다는 것을 알았습니다.그들은 대기열에 있어야하고 대기열은 유지됩니다.그래서 좋은 불공평한 잠금은 어디에 반영됩니까?

이미지.png

밝혀진 바에 따르면 공정한 잠금을 위한 잠금을 획득하는 과정에서 항상 큐가 비어 있는지 미리 판단하고 비어 있지 않으면 대기 큐에 합류합니다.비공정 잠금은 다릅니다.아니요 대기열이 비어 있든 없든 하나를 먼저 잡고 대화하면 현재 스레드가 잠금을 직접 잡고 대기 대기열에 들어가지 않을 가능성이 높으며 이는 확실히 먼저 도착한 스레드에 불공평합니다. 너무 많은 지원자들이 큐를 유지해야 하기 때문에 페어 락이 성능이 더 나쁘다는 것은 명백히 부정확하다고 대답했습니다. 불공평한 잠금이 있더라도 대기열에 들어가 대기열을 유지할 가능성이 높으며 유일한 차이점은 잠금을 얻기 위한 대기 시간과 기회가 다르다는 것입니다.

요약하면 전체 기사의 내용은 주로 AQS 구현 메커니즘의 일부를 소개하고 ReentrantLock 구현을 통해 AQS의 소스 코드를 간략하게 설명합니다. Java의 잠금 메커니즘도 휘발성 수정 상태 변수를 통해 잠금 기능을 구현하고 기본 CAS 작업을 통해 이 값을 설정합니다. 동시에 잠금을 획득할 수 없는 스레드는 양방향 대기열을 통해 유지되며 이러한 스레드는 Java 자체 park의 도움으로 대기하도록 설정됩니다. 잠금이 해제되면 큐의 헤드로 이동하여 스레드를 깨우고 unpark를 통해 작업을 계속합니다.

마지막으로 chatgpt에서 생성한 코드 주석을 사용하여 이해를 돕습니다.

 
 

이것

코드 복사

/** * 尝试获取锁或信号量,如果成功获取则返回1,否则继续尝试获取直至成功或被中断或超时 * * @param node 当前节点 * @param arg 获取锁或信号量时的参数 * @param shared 是否是共享模式 * @param interruptible 是否允许被中断 * @param timed 是否使用定时等待 * @param time 定时等待的超时时间 * @return 成功获取返回1,超时返回0,被中断返回负数 */ final int acquire(Node node, int arg, boolean shared, boolean interruptible, boolean timed, long time) { // 获取当前线程 Thread current = Thread.currentThread(); // 用于重试计数的变量 byte spins = 0, postSpins = 0; // 用于标记当前线程是否被中断以及当前节点是否是队列中的首节点 boolean interrupted = false, first = false; // 当前节点的前驱节点 Node pred = null; // 循环尝试获取锁或信号量 for (;;) { // 检查是否是队列中的首节点 if (!first && (pred = (node == null) ? null : node.prev) != null && !(first = (head == pred))) { // 非首节点,检查前驱节点是否已取消或是有新的前驱节点 if (pred.status < 0) { // 前驱节点已取消,清理队列 cleanQueue(); continue; // 重新开始循环尝试获取锁或信号量 } else if (pred.prev == null) { // 确保串行化,避免过度自旋 Thread.onSpinWait(); continue; // 重新开始循环尝试获取锁或信号量 } } // 尝试获取锁或信号量 if (first || pred == null) { // 首节点或前驱节点为null,说明当前节点还未入队列 boolean acquired; try { // 尝试获取锁或信号量 if (shared) acquired = (tryAcquireShared(arg) >= 0); else acquired = tryAcquire(arg); } catch (Throwable ex) { // 取消获取操作,抛出异常 cancelAcquire(node, interrupted, false); throw ex; } // 如果成功获取锁或信号量 if (acquired) { // 更新头节点和前驱节点的引用,设置节点状态,唤醒其他等待线程 if (first) { node.prev = null; head = node; pred.next = null; node.waiter = null; if (shared) signalNextIfShared(node); if (interrupted) current.interrupt(); } // 返回成功 return 1; } } // 尝试入队或重试 if (node == null) { // 未分配节点,分配新节点并重试 if (shared) node = new SharedNode(); else node = new ExclusiveNode(); } else if (pred == null) { // 前驱节点为null,说明当前节点还未入队列,尝试将当前节点入队列 node.waiter = current; Node t = tail; node.setPrevRelaxed(t); // 避免不必要的内存屏障 if (t == null) tryInitializeHead(); else if (!casTail(t, node)) node.setPrevRelaxed(null); // 入队失败,回退 else t.next = node; } else if (first && spins != 0) { // 重试次数限制,减少对先前线程的不公平竞争 --spins; // 进行自旋等待 Thread.onSpinWait(); } else if (node.status == 0) { // 设置状态为等待中,以便其他线程进行唤醒 node.status = WAITING; } else { // 需要定时等待 long nanos; spins = postSpins = (byte) ((postSpins << 1) | 1); if (!timed) LockSupport.park(this); // 非定时等待 else if ((nanos = time - System.nanoTime()) > 0L) LockSupport.parkNanos(this, nanos); // 定时等待 else break; // 超时,结束等待 // 清除节点的状态 node.clearStatus(); // 检查线程是否被中断,并根据需要退出循环 if ((interrupted |= Thread.interrupted()) && interruptible) break; } } // 取消获取操作,并根据中断状态返回相应结果 return cancelAcquire(node, interrupted, interruptible); }

추천

출처blog.csdn.net/wdj_yyds/article/details/131982091