목차
Introduction
1. 잠금 해제
1.1.배타적 잠금 해제
1.2. 공유 잠금 해제 releaseShared
2. 조건부 큐의 중요한 메소드
2.1. 대기열에 들어가서 wait
2.1.1, addConditionWaiter
가장 중요한 메서드인 transferForSignal을 살펴보겠습니다.
소스 코드에서 볼 수 있듯이 그 본질은 for 루프 호출입니다. 조건 대기열의 노드를 순환하는 transferForSignal 메소드. 동기화 대기열로 전송합니다.
Java java지도 시간 Java 동기화 장치 AQS 아키텍처는 어떻게 잠금을 해제하고 대기열을 동기화합니까?

Java 동기화 장치 AQS 아키텍처는 어떻게 잠금을 해제하고 대기열을 동기화합니까?

May 11, 2023 pm 05:16 PM
java aqs

    Introduction

    AQS의 내용이 너무 많아서 AQS 전반부를 읽지 않은 학생들이 다시 살펴볼 수 있도록 전반부에서는 AQS의 기본 개념에 대해 많이 이야기합니다. 잠금 기본 속성, 잠금 획득 방법 등 이 장에서는 잠금 및 동기화 대기열을 해제하는 방법에 대해 주로 설명합니다.

    1. 잠금 해제

    잠금 해제의 트리거 시간은 일반적으로 사용되는 Lock.unLock() 메서드입니다. 목적은 스레드가 리소스에 대한 액세스를 해제할 수 있도록 하는 것입니다(전체 아키텍처 다이어그램의 보라색 경로 참조). 과정).

    잠금 해제도 두 가지로 나뉘는데, 하나는 전용 잠금 해제이고, 다른 하나는 공유 잠금 해제입니다.

    1.1.배타적 잠금 해제

    배타적 잠금 해제는 비교적 간단합니다. 대기열의 선두에서 시작하여 다음 노드를 찾으면 끝에서 시작하여 다음 노드를 찾습니다. 상태가 취소된 노드가 아니고 노드를 해제합니다. 소스 코드는 다음과 같습니다:

    // unlock 的基础方法
    public final boolean release(int arg) {
        // tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明成功释放锁。
        if (tryRelease(arg)) {
            Node h = head;
            // 头节点不为空,并且非初始化状态
            if (h != null && h.waitStatus != 0)
                // 从头开始唤醒等待锁的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // 很有意思的方法,当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
    // 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
    private void unparkSuccessor(Node node) {
        // node 节点是当前释放锁的节点,也是同步队列的头节点
        int ws = node.waitStatus;
        // 如果节点已经被取消了,把节点的状态置为初始化
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 拿出 node 节点的后面一个节点
        Node s = node.next;
        // s 为空,表示 node 的后一个节点为空
        // s.waitStatus 大于0,代表 s 节点已经被取消了
        // 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 这里从尾迭代,而不是从头开始迭代是有原因的。
            // 主要是因为节点被阻塞的时候,是在 acquireQueued 方法里面被阻塞的,唤醒时也一定会在 acquireQueued 方法里面被唤醒,唤醒之后的条件是,判断当前节点的前置节点是否是头节点,这里是判断当前节点的前置节点,所以这里必须使用从尾到头的迭代顺序才行,目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。
            for (Node t = tail; t != null && t != node; t = t.prev)
                // t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 唤醒以上代码找到的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    로그인 후 복사

    1.2. 공유 잠금 해제 releaseShared

    공유 잠금을 해제하는 방법은 크게 두 가지로 나뉩니다. 단계:

    tryReleaseShared는 현재 공유 잠금을 해제하려고 시도하고 실패하면 false를 반환합니다. Go 2 성공적으로

    스레드가 공유 잠금을 얻을 때 이 방법을 본 적이 있습니다. 잠금을 해제하면 뒤에 있는 노드가 활성화됩니다. 메소드 이름은 doReleaseShared입니다.

    releaseShared의 소스코드를 살펴보겠습니다:

    // 共享模式下,释放当前线程的共享锁
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
            doReleaseShared();
            return true;
        }
        return false;
    }
    로그인 후 복사

    2. 조건부 큐의 중요한 메소드

    조건부 큐의 메소드를 살펴보기 전에 먼저 동기화 큐가 존재하는 이유와 조건부 큐가 존재하는 이유를 이해해야 합니다. 필요해?

    주로 동기화된 대기열로 모든 시나리오를 처리할 수는 없기 때문에 잠금 + 대기열이 결합된 시나리오가 발생하면 먼저 잠금을 사용하여 잠금을 얻을 수 있는 스레드와 대기열에 넣어야 하는 스레드가 필요합니다. 잠금을 획득한 여러 스레드가 대기열이 가득 차거나 비어 있는 경우 조건을 사용하여 이러한 스레드를 관리하고 이러한 스레드가 차단 및 대기한 다음 적절한 시간에 정상적으로 깨어날 수 있습니다.

    동기화 대기열 + 조건부 대기열을 함께 사용하는 시나리오는 잠금 + 대기열 시나리오에서 가장 자주 사용됩니다.

    그래서 조건부 큐도 빼놓을 수 없는 부분입니다.

    다음으로 조건 대기열의 더 중요한 메서드 중 일부를 살펴보겠습니다. 다음 메서드는 모두 ConditionObject 내부 클래스에 있습니다.

    2.1. 대기열에 들어가서 wait

    // 线程入条件队列
    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 加入到条件队列的队尾
        Node node = addConditionWaiter();
        // 标记位置 A
        // 加入条件队列后,会释放 lock 时申请的资源,唤醒同步队列队列头的节点
        // 自己马上就要阻塞了,必须马上释放之前 lock 的资源,不然自己不被唤醒的话,别的线程永远得不到该共享资源了
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        // 确认node不在同步队列上,再阻塞,如果 node 在同步队列上,是不能够上锁的
        // 目前想到的只有两种可能:
        // 1:node 刚被加入到条件队列中,立马就被其他线程 signal 转移到同步队列中去了
        // 2:线程之前在条件队列中沉睡,被唤醒后加入到同步队列中去
        while (!isOnSyncQueue(node)) {
            // this = AbstractQueuedSynchronizer$ConditionObject
            // 阻塞在条件队列上
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 标记位置 B
        // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中的数据结构中去了
        // 所以这里节点苏醒了,直接尝试 acquireQueued
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            // 如果状态不是CONDITION,就会自动删除
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }
    로그인 후 복사

    await 메서드를 기다립니다. 특별한 주의가 필요한 몇 가지 사항이 있습니다.

    위 코드는 위치 A를 표시합니다. 노드가 조건부 대기열에 들어갈 준비가 되기 전에 먼저 해제해야 합니다. 그렇지 않으면 조건 대기열이 조건 대기열에 있고 다른 스레드는 잠금을 얻을 수 없습니다. 위의 코드는 위치 B를 표시합니다. 이때 노드는 Condition.signal 또는 signalAll 메서드는 이 시점에서 노드가 동기화 큐로 성공적으로 전송되었으므로(아키텍처 다이어그램의 전체 파란색 프로세스), 소스 코드는 Waiter를 사용하여 조건에 노드 이름을 지정하는 것을 좋아합니다. 따라서 조건 큐에 실제로는 Node인 Waiter가 표시됩니다.

    await 메서드에는 addConditionWaiter와 unlinkCancelledWaiters라는 두 가지 중요한 메서드가 있습니다. 하나씩 살펴보겠습니다.

    2.1.1, addConditionWaiter

    addConditionWaiter 메소드는 주로 조건 큐에 노드를 넣습니다. 메소드 소스 코드는 다음과 같습니다.

    // 增加新的 waiter 到队列中,返回新添加的 waiter
    // 如果尾节点状态不是 CONDITION 状态,删除条件队列中所有状态不是 CONDITION 的节点
    // 如果队列为空,新增节点作为队列头节点,否则追加到尾节点上
    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        // 如果尾部的 waiter 不是 CONDITION 状态了,删除
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();
            t = lastWaiter;
        }
        // 新建条件队列 node
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        // 队列是空的,直接放到队列头
        if (t == null)
            firstWaiter = node;
        // 队列不为空,直接到队列尾部
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
    }
    로그인 후 복사

    전체 프로세스는 큐의 끝에 추가하는 것입니다. unlinkCancelledWaiters라는 중요한 메소드입니다. 이 메소드 상태가 CONDITION이 아닌 조건 대기열의 모든 노드는 다음과 같이 unlinkCancelledWaiters 메소드의 소스 코드를 살펴보겠습니다. 이 방법에 대한 모든 사람의 이해를 돕기 위해 다음과 같은 설명 다이어그램이 그려집니다.

    2.2. 단일 깨우기 신호

    Java 동기화 장치 AQS 아키텍처는 어떻게 잠금을 해제하고 대기열을 동기화합니까? 신호 방법은 이전에 대기열이 꽉 찼을 때 일부 스레드를 의미합니다. take 작업으로 인해 조건부 큐에서 차단되었으며 갑자기 큐의 요소가 스레드 A에 의해 소비되었습니다. 스레드 A는 이전에 차단된 스레드를 깨우기 위해 신호 메서드를 호출합니다. 조건의 헤드 노드에서 깨어납니다. queue(프로세스의 전체 아키텍처 다이어그램에서 파란색 부분 참조) 소스 코드는 다음과 같습니다.

    // 会检查尾部的 waiter 是不是已经不是CONDITION状态了
    // 如果不是,删除这些 waiter
    private void unlinkCancelledWaiters() {
        Node t = firstWaiter;
        // trail 表示上一个状态,这个字段作用非常大,可以把状态都是 CONDITION 的 node 串联起来,即使 node 之间有其他节点都可以
        Node trail = null;
        while (t != null) {
            Node next = t.nextWaiter;
            // 当前node的状态不是CONDITION,删除自己
            if (t.waitStatus != Node.CONDITION) {
                //删除当前node
                t.nextWaiter = null;
                // 如果 trail 是空的,咱们循环又是从头开始的,说明从头到当前节点的状态都不是 CONDITION
                // 都已经被删除了,所以移动队列头节点到当前节点的下一个节点
                if (trail == null)
                    firstWaiter = next;
                // 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉,然后把自己挂到上一个状态是 CONDITION 的节点上
                else
                    trail.nextWaiter = next;
                // 遍历结束,最后一次找到的CONDITION节点就是尾节点
                if (next == null)
                    lastWaiter = trail;
            }
            // 状态是 CONDITION 的 Node
            else
                trail = t;
            // 继续循环,循环顺序从头到尾
            t = next;
        }
    }
    로그인 후 복사

    가장 중요한 메서드인 transferForSignal을 살펴보겠습니다.

    // 唤醒阻塞在条件队列中的节点
    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 从头节点开始唤醒
        Node first = firstWaiter;
        if (first != null)
            // doSignal 方法会把条件队列中的节点转移到同步队列中去
            doSignal(first);
    }
    // 把条件队列头节点转移到同步队列去
    private void doSignal(Node first) {
        do {
            // nextWaiter为空,说明到队尾了
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            // 从队列头部开始唤醒,所以直接把头节点.next 置为 null,这种操作其实就是把 node 从条件队列中移除了
            // 这里有个重要的点是,每次唤醒都是从队列头部开始唤醒,所以把 next 置为 null 没有关系,如果唤醒是从任意节点开始唤醒的话,就会有问题,容易造成链表的割裂
            first.nextWaiter = null;
            // transferForSignal 方法会把节点转移到同步队列中去
            // 通过 while 保证 transferForSignal 能成功
            // 等待队列的 node 不用管他,在 await 的时候,会自动清除状态不是 Condition 的节点(通过 unlinkCancelledWaiters 方法)
            // (first = firstWaiter) != null  = true 的话,表示还可以继续循环, = false 说明队列中的元素已经循环完了
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }
    로그인 후 복사

    전체 소스 코드를 읽어보면 조건 큐에 있는 노드를 깨우면 실제로 조건 큐에 있는 노드가 동기화 큐로 전송되고 이전 노드의 상태가 SIGNAL로 설정되는 것을 알 수 있습니다.

    2.3. 모두 깨우기 signalAll

    signalAll은 조건 대기열의 모든 노드를 깨우는 데 사용됩니다. 소스 코드는 다음과 같습니다.

    // 返回 true 表示转移成功, false 失败
    // 大概思路:
    // 1. node 追加到同步队列的队尾
    // 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒
    // 可以看出来 node 的状态此时是 0 了
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点
        // 看命名是 p,实际是 pre 单词的缩写
        Node p = enq(node);
        int ws = p.waitStatus;
        // 状态修改成 SIGNAL,如果成功直接返回
        // 把当前节点的前一个节点修改成 SIGNAL 的原因,是因为 SIGNAL 本身就表示当前节点后面的节点都是需要被唤醒的
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
            LockSupport.unpark(node.thread);
        return true;
    }
    로그인 후 복사

    소스 코드에서 볼 수 있듯이 그 본질은 for 루프 호출입니다. 조건 대기열의 노드를 순환하는 transferForSignal 메소드. 동기화 대기열로 전송합니다.

    3. 요약

    AQS 소스 코드가 드디어 완성되었습니다. 이해가 되셨나요? 이제 AQS 아키텍처 다이어그램을 조용히 떠올려 보실 수 있습니다.

    Java 동기화 장치 AQS 아키텍처는 어떻게 잠금을 해제하고 대기열을 동기화합니까?

    위 내용은 Java 동기화 장치 AQS 아키텍처는 어떻게 잠금을 해제하고 대기열을 동기화합니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    본 웹사이트의 성명
    본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

    핫 AI 도구

    Undresser.AI Undress

    Undresser.AI Undress

    사실적인 누드 사진을 만들기 위한 AI 기반 앱

    AI Clothes Remover

    AI Clothes Remover

    사진에서 옷을 제거하는 온라인 AI 도구입니다.

    Undress AI Tool

    Undress AI Tool

    무료로 이미지를 벗다

    Clothoff.io

    Clothoff.io

    AI 옷 제거제

    AI Hentai Generator

    AI Hentai Generator

    AI Hentai를 무료로 생성하십시오.

    뜨거운 도구

    메모장++7.3.1

    메모장++7.3.1

    사용하기 쉬운 무료 코드 편집기

    SublimeText3 중국어 버전

    SublimeText3 중국어 버전

    중국어 버전, 사용하기 매우 쉽습니다.

    스튜디오 13.0.1 보내기

    스튜디오 13.0.1 보내기

    강력한 PHP 통합 개발 환경

    드림위버 CS6

    드림위버 CS6

    시각적 웹 개발 도구

    SublimeText3 Mac 버전

    SublimeText3 Mac 버전

    신 수준의 코드 편집 소프트웨어(SublimeText3)

    자바의 완전수 자바의 완전수 Aug 30, 2024 pm 04:28 PM

    Java의 완전수 가이드. 여기서는 정의, Java에서 완전 숫자를 확인하는 방법, 코드 구현 예제에 대해 논의합니다.

    자바의 웨카 자바의 웨카 Aug 30, 2024 pm 04:28 PM

    Java의 Weka 가이드. 여기에서는 소개, weka java 사용 방법, 플랫폼 유형 및 장점을 예제와 함께 설명합니다.

    Java의 스미스 번호 Java의 스미스 번호 Aug 30, 2024 pm 04:28 PM

    Java의 Smith Number 가이드. 여기서는 정의, Java에서 스미스 번호를 확인하는 방법에 대해 논의합니다. 코드 구현의 예.

    Java Spring 인터뷰 질문 Java Spring 인터뷰 질문 Aug 30, 2024 pm 04:29 PM

    이 기사에서는 가장 많이 묻는 Java Spring 면접 질문과 자세한 답변을 보관했습니다. 그래야 면접에 합격할 수 있습니다.

    Java 8 Stream foreach에서 나누거나 돌아 오시겠습니까? Java 8 Stream foreach에서 나누거나 돌아 오시겠습니까? Feb 07, 2025 pm 12:09 PM

    Java 8은 스트림 API를 소개하여 데이터 컬렉션을 처리하는 강력하고 표현적인 방법을 제공합니다. 그러나 스트림을 사용할 때 일반적인 질문은 다음과 같은 것입니다. 기존 루프는 조기 중단 또는 반환을 허용하지만 스트림의 Foreach 메소드는이 방법을 직접 지원하지 않습니다. 이 기사는 이유를 설명하고 스트림 처리 시스템에서 조기 종료를 구현하기위한 대체 방법을 탐색합니다. 추가 읽기 : Java Stream API 개선 스트림 foreach를 이해하십시오 Foreach 메소드는 스트림의 각 요소에서 하나의 작업을 수행하는 터미널 작동입니다. 디자인 의도입니다

    Java의 날짜까지의 타임스탬프 Java의 날짜까지의 타임스탬프 Aug 30, 2024 pm 04:28 PM

    Java의 TimeStamp to Date 안내. 여기서는 소개와 예제와 함께 Java에서 타임스탬프를 날짜로 변환하는 방법에 대해서도 설명합니다.

    캡슐의 양을 찾기위한 Java 프로그램 캡슐의 양을 찾기위한 Java 프로그램 Feb 07, 2025 am 11:37 AM

    캡슐은 3 차원 기하학적 그림이며, 양쪽 끝에 실린더와 반구로 구성됩니다. 캡슐의 부피는 실린더의 부피와 양쪽 끝에 반구의 부피를 첨가하여 계산할 수 있습니다. 이 튜토리얼은 다른 방법을 사용하여 Java에서 주어진 캡슐의 부피를 계산하는 방법에 대해 논의합니다. 캡슐 볼륨 공식 캡슐 볼륨에 대한 공식은 다음과 같습니다. 캡슐 부피 = 원통형 볼륨 2 반구 볼륨 안에, R : 반구의 반경. H : 실린더의 높이 (반구 제외). 예 1 입력하다 반경 = 5 단위 높이 = 10 단위 산출 볼륨 = 1570.8 입방 단위 설명하다 공식을 사용하여 볼륨 계산 : 부피 = π × r2 × h (4

    미래를 창조하세요: 완전 초보자를 위한 Java 프로그래밍 미래를 창조하세요: 완전 초보자를 위한 Java 프로그래밍 Oct 13, 2024 pm 01:32 PM

    Java는 초보자와 숙련된 개발자 모두가 배울 수 있는 인기 있는 프로그래밍 언어입니다. 이 튜토리얼은 기본 개념부터 시작하여 고급 주제를 통해 진행됩니다. Java Development Kit를 설치한 후 간단한 "Hello, World!" 프로그램을 작성하여 프로그래밍을 연습할 수 있습니다. 코드를 이해한 후 명령 프롬프트를 사용하여 프로그램을 컴파일하고 실행하면 "Hello, World!"가 콘솔에 출력됩니다. Java를 배우면 프로그래밍 여정이 시작되고, 숙달이 깊어짐에 따라 더 복잡한 애플리케이션을 만들 수 있습니다.

    See all articles