높 은 병발 (24) 자바 util. concurrent 각 구성 요소 분석 (6) AQS 깊이 이해 (4)

9203 단어 병발 하 다
최근 전체적으로 AQS 구 조 를 넘 어 인터넷 에서 도 AQS 를 다 루 는 글 을 봤 는데 대부분 글 이 일반적이다.AQS 코드 를 다시 보고 새로운 요점 을 꺼 내 말 해 보 세 요.
AQS 는 하나의 파이프라인 으로 기본 적 인 동기 화 기 를 제공 하 는 능력 을 제공 합 니 다. 하나의 상태, 상 태 를 수정 하 는 원자 조작, 그리고 동기 화 라인 의 일련의 조작 을 포함 합 니 다.CLHLock 의 변종 입 니 다. CLHLock 은 대기 열 잠 금 을 기반 으로 하 는 자동 잠 금 알고리즘 입 니 다.AQS 도 동기 화 스 레 드 의 구조 로 대기 열 을 사 용 했 습 니 다. 하 나 는 스 레 드 동기 화 된 동기 화 대기 열 이 고 다른 하 나 는 Unsafe 를 기반 으로 차단 / 깨 우기 작업 을 하 는 조건 부 대기 열 입 니 다.그래서 대기 열 조작 을 이해 하 는 것 이 AQS 를 이해 하 는 관건 이다.
1. 헤드 이해, tail 인용
2. next 이해, prev 인용
3. 대기 열 노드 가 언제 입 대 했 는 지, 언제 출전 했 는 지 이해 하기
헤드 인용 에 대해 기억 해 야 할 것 은
1. 헤드 인용 은 항상 자 물 쇠 를 얻 은 노드 를 가리 키 며 취소 되 지 않 습 니 다.acquire 작업 이 성공 하면 자 물 쇠 를 얻 었 다 는 뜻 입 니 다. acquire 과정 에서 중단 되면 acquire 가 실 패 했 습 니 다. 이때 head 는 다음 노드 를 가리 키 게 됩 니 다.
* because the head node is never cancelled: A node becomes
         * head only as a result of successful acquire. A
         * cancelled thread never succeeds in acquiring, and a thread only
         * cancels itself, not any other node.

자 물 쇠 를 얻 은 후에 스 레 드 가 중단 되면 release 로 head 노드 를 풀 어야 합 니 다.스 레 드 가 중단 되 어 자 물 쇠 를 풀 지 않 으 면 문제 가 생 길 수 있 습 니 다.따라서 명시 적 자 물 쇠 를 사용 할 때 는 finally 에서 자 물 쇠 를 풀 어야 합 니 다.
Lock lock = new ReentrantLock();
		lock.lock();
		try{
			//     ,        ,    finally     
		}finally{
			lock.unlock();
		}

자 물 쇠 를 얻 을 때 헤드 에 대한 인용 처 리 를 살 펴 보 자. 노드 의 전구 노드 가 헤드 일 때 만 자 물 쇠 를 얻 을 수 있 고 자 물 쇠 를 얻 은 후에 자신 을 헤드 노드 로 설정 하 는 동시에 오래된 헤드 의 next 를 null 로 설정 해 야 한다.
여기에 몇 가지 의미 가 있다.
1. 처음부터 끝까지 헤드 노드 부터 잠 금 획득
2. 새로운 스 레 드 에서 자 물 쇠 를 얻 은 후, 이전에 자 물 쇠 를 얻 은 노드 가 대기 열 에서 나 옵 니 다.
3. 자 물 쇠 를 얻 으 면 acquire 방법 은 반드시 되 돌아 갑 니 다. 이 과정 에서 중단 되 지 않 습 니 다.
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                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);
        }
    }

tail 인용 에 대해 서 는 체인 구 조 를 잠 금 없 이 실현 하고 CAS + 폴 링 방식 을 사용 합 니 다.노드 의 입단 작업 은 모두 tail 노드 에 있다.
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;
                }
            }
        }
    }

next 인용 은 대기 열 에서 매우 중요 한 역할 을 하 는데 나타 나 는 빈도 가 매우 높다.next 참조 에 대하 여 몇 가지 값 이 있 습 니 다.
1. next = null
2. next 는 null 이 아 닌 다음 노드 를 가리킨다.
3. next = 노드 자신
next = null 의 경 우 는 세 가지 가 있 습 니 다.
1. 팀 끝 노드, 팀 끝 노드 의 next 는 명시 적 으로 설정 되 지 않 았 기 때문에 null 입 니 다.
2. 팀 의 끝 노드 가 대기 열 에 들 어 갈 때 이전 팀 의 끝 노드 next 노드 는 null 일 수 있 습 니 다. enq 는 원자 조작 이 아니 기 때문에 CAS 다음 에 복합 작업 입 니 다.
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)) {
                    //     next   null
                    t.next = node;
                    return t;
                }
            }
        }
    }

3. 자 물 쇠 를 가 져 올 때 이전에 자 물 쇠 를 가 져 온 노드 의 next 는 null 로 설정 합 니 다.
if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }

next 는 null 이 아 닌 다음 노드 를 가리 키 고 있 습 니 다. 이러한 상황 은 동기 화 대기 열 에서 기다 리 는 노드 입 니 다. 입 대 를 할 때 이전 노드 의 next 값 을 설정 하면 자 물 쇠 를 풀 때 다음 노드 에 자 물 쇠 를 가 져 올 수 있 습 니 다.
 private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 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;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

next 는 자신 을 가 리 킵 니 다. 이것 은 작업 을 취소 할 때 노드 의 앞 노드 를 뒤의 노드 로 가리 키 고 마지막 으로 next 도 메 인 을 자신 으로 설정 합 니 다.
private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        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.
        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.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            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;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

prev 인용 은 비교적 간단 합 니 다. 주로 링크 구 조 를 유지 합 니 다.CLHLock 은 이전 노드 의 상태 에서 자전 합 니 다. AQS 안의 노드 는 이전 상태 에서 기다 리 는 것 이 아니 라 풀 릴 때 이전 노드 알림 대기 열 에서 다음 깨 울 노드 를 찾 습 니 다.
마지막 으로 노드 가 대기 열 에 들 어가 고 대기 열 에 나 가 는 상황 을 말 합 니 다.
노드 가 대기 열 에 들 어 가 는 것 은 한 가지 상황 입 니 다. 그것 은 바로 try Acquire 작업 이 실 패 했 고 자 물 쇠 를 얻 지 못 하면 동기 화 대기 열 에 들 어가 기 를 기다 리 는 것 입 니 다. try Acquire 가 성공 하면 동기 화 대기 열 에 들 어가 지 않 아 도 됩 니 다.AQS 는 충분 한 유연성 을 제공 합 니 다. try Acquire 와 try Relase 방법 을 제공 하여 하위 클래스 의 확장 을 제공 합 니 다. 기본 클래스 는 대기 열 작업 을 유지 하고 하위 클래스 는 스스로 대기 열 에 들 어 갈 지 여 부 를 결정 할 수 있 습 니 다.
그래서 실제 하위 클래스 가 확 장 될 때 두 가지 유형 이 있 는데 하 나 는 공평 한 동기 화 장치 이 고 하 나 는 불공평 한 동기 화 장치 이다.여기 서 주의해 야 할 것 은 이른바 불공평 하 다 는 것 이다. 대기 열 을 사용 하지 않 고 차단 작업 을 유지 하 는 것 이 아니 라 경쟁 을 얻 을 때 먼저 오 는 스 레 드 를 고려 하지 않 고 나중에 스 레 드 는 자원 을 직접 경쟁 할 수 있다 는 것 이다.불공평 하고 공평 한 동기 화기 경쟁 이 실패 한 후 에는 AQS 동기 화 대기 열 에 들 어가 기 를 기 다 려 야 하 며, 동기 화 대기 열 은 먼저 서 비 스 를 하 는 공평 한 대기 열 이다.
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            return nonfairTryAcquireShared(acquires);
        }
    }

    /**
     * Fair version
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        FairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

대열 을 나 가 는 데 는 두 가지 상황 이 있 는데,
1. 다음 스 레 드 에서 자 물 쇠 를 얻 는 것 은 head 가 현재 자 물 쇠 를 얻 은 스 레 드 를 참조 하고 이전 자 물 쇠 를 얻 은 노드 가 자동 으로 대기 열 을 나 오 는 것 입 니 다.
2. 작업 을 취소 할 때 노드 가 대기 열 에 나 가 고 취 소 는 두 가지 상황 만 있 습 니 다. 하 나 는 스 레 드 가 중단 되 고 다른 하 나 는 시간 이 초과 되 기 를 기다 리 는 것 입 니 다.

좋은 웹페이지 즐겨찾기