JDK 자물쇠 의 기초 - AQS 실현 원리 (2)

15343 단어
이 글 은 CLH 자물쇠 의 원리 와 AQS 의 데이터 구 조 를 포함 한 AQS 의 기초 지식 을 소개 했다. 이 글 에서 AQS 의 방법 을 분석 해 보 자.AQS 는 추상 적 인 유형 으로 몇 가지 템 플 릿 방법 을 하위 클래스 에 맡 겨 실현 하도록 정 의 했 습 니 다. 각각:
  • protected boolean tryAcquire(int arg)
  • protected boolean tryRelease(int arg)
  • protected int tryAcquireShared(int arg)
  • protected boolean tryReleaseShared(int arg)

  • 본 고 는 공유 자 물 쇠 를 얻 는 상황 을 분석 하지 않 는 다.
    먼저 비교적 간단 한 하위 클래스 실현 ReentrantLock 중의 내부 클래스 FairSync 를 살 펴 보 자. 다음은 ReentrantLock 의 실현 을 직접 분석 해 보 자. ReentrantLock 한 필드 private final Sync sync; 가 있 는데 Sync 는 바로 AQS 에서 계승 한 것 이다.ReentrantLock 의 구조 함 수 를 살 펴 보 자.
        public ReentrantLock() {
            sync = new NonfairSync();
        }
    
        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }

    그 중에서 NonfairSyncFairSync 는 모두 Sync 류 에서 계승 되 었 고 자 물 쇠 를 얻 는 방식 이 공평 한 지 의 차 이 는 다음 과 같다.
        //NonfairSync
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        
        //FairSync
        final void lock() {
            acquire(1);
        }

    이 를 통 해 알 수 있 듯 이 불공 정 자 물 쇠 는 자 물 쇠 를 가 져 올 때 자 물 쇠 를 직접 가 져 올 수 있 는 지, 가능 하 다 면 줄 을 서지 않 아 도 된다 acquire. 공정 자 물 쇠 는 대기 열 에 직접 들 어가 줄 을 서 는 것 이다.
    AQS 에서 acquire 방법의 실현 을 살 펴 보 자.
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    acquire 방법의 대체적인 논 리 는 먼저 호출 tryAcquire 하여 자 물 쇠 를 가 져 오 려 고 시도 하 는 것 이다. 실패 하면 Node 대상 을 새로 만 들 고 AQS 대기 열 에 추가 하 는 것 이다.
        private Node addWaiter(Node mode) {
            //          Node  ,         
            Node node = new Node(Thread.currentThread(), mode);
            // Try the fast path of enq; backup to full enq on failure
            //    compareAndSetTail   Node          ,        ,        ,    enq     
            Node pred = tail;
            if (pred != null) {
                node.prev = pred;
                if (compareAndSetTail(pred, node)) {
                    pred.next = node;
                    return node;
                }
            }
            //  Node  ,  enq         
            enq(node);
            return node;
        }
    addWaiter 방법 으로 Node 대상 을 새로 만 들 고 EXCLUSIVE 모드 로 설정 합 니 다. 즉, 자 물 쇠 를 배열 하 는 것 입 니 다.이 방법 에 서 는 먼저 원자 조작 을 사용 하여 새 노드 를 대기 행렬 의 끝 에 추가 하려 고 시도 합 니 다. 추가 에 성공 하면 바로 돌아 갑 니 다. 그렇지 않 으 면 호출 enq() 방법 으로 추가 합 니 다.
        private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                //           ,      ,     、                    
                if (t == null) { // Must initialize
                    //           ,        ,       if       ,   enq       node           
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    //                    ,              ,          
                    if (compareAndSetTail(t, node)) {
                        //          next     node  node                   
                        t.next = node;
                        return t;
                    }
                }
            }
        }

    AQS 대기 열 은 lazy 모드 로 초기 화 되 었 습 니 다. 즉, 스 레 드 가 없 으 면 대기 열 이 만들어 지지 않 는 다 는 것 입 니 다.대기 행렬 의 초기 화 는 enq() 방법 에서 이 루어 졌 다.addWaiter 과정 에서 다른 스 레 드 가 잠 금 을 풀 었 을 수 있 기 때문에 acquireQueued 방법 에서 새 노드 를 막 기 전에 잠 금 을 가 져 오 려 고 시도 할 수 있 습 니 다. 실패 하면 이 스 레 드 를 막 습 니 다.
    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);
            }
        }

    AQS 대기 열 에 있 는 헤드 노드 는 현재 자 물 쇠 를 가지 고 있 는 스 레 드 를 대표 합 니 다. 따라서 한 노드 의 전구 노드 가 헤드 노드 일 때 자 물 쇠 를 가 져 오 려 고 시도 할 수 있 습 니 다 (자 물 쇠 를 가지 고 있 는 스 레 드 가 자 물 쇠 를 풀 었 을 수 있 기 때 문 입 니 다). 자 물 쇠 를 가 져 오 는 데 성공 하면 이 노드 를 헤드 노드 로 설정 하고 되 돌려 줍 니 다.현재 노드 의 전구 노드 가 머리 노드 가 아니라면 잠 금 을 가 져 올 기 회 를 주지 않 습 니 다. AQS 대기 열 이 FIFO 이기 때문에 잠 금 을 가 져 올 차례 가 아 닙 니 다.
    이 노드 가 잠 금 을 가 져 오 는 데 실패 하면 호출 shouldParkAfterFailedAcquire 방법 으로 현재 노드 를 막 아야 하 는 지 판단 합 니 다.
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            //          SIGNAL,                      ,          
            if (ws == Node.SIGNAL)         
                return true;
            //  CANCELLED    0
            //          CANCELLED,                               
            if (ws > 0) {         
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                //           0  PROPAGATE(     ,          )
                //          SIGNAL
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
    shouldParkAfterFailedAcquire 방법 이 false 로 돌아 가면 acquireQueued 방법 에서 다음 순환 shouldParkAfterFailedAcquire 방법 은 현재 노드 의 전구 노드 나 전구 노드 의 상 태 를 수정 할 수 있 기 때 문) 을 할 수 있 습 니 다. 이때 현재 노드 의 전구 노드 는 머리 노드 나 전구 노드 의 상태 가 SIGNAL 로 변 할 수 있 습 니 다.shouldParkAfterFailedAcquire 방법 이 true 로 돌아 오 면 parkAndCheckInterrupt 방법 으로 현재 노드 대표 의 스 레 드 를 막 습 니 다.
        private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            return Thread.interrupted();
        }

    이로써 자 물 쇠 를 얻 는 방법 acquire 을 분석 했다.자 물 쇠 를 풀 어 주 는 방법 release 을 살 펴 보 자.
    public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;
                //           
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    release 방법 호출 tryRelease (템 플 릿 방법, 하위 클래스 에 맡 겨 실현) 은 해당 수량의 신 호 량 을 방출 합 니 다. 방법 호출 이 성공 하면 대기 행렬 헤드 노드 의 후계 노드 를 깨 웁 니 다.
    private void unparkSuccessor(Node node) {
            int ws = node.waitStatus;
            if (ws < 0)
                //      0,          
                compareAndSetWaitStatus(node, ws, 0);
    
            Node s = node.next;
            //       next                  ,                  next              ,              node   next                 ,        tail         node     
            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);
        }

    중단 되 지 않 은 경우 unparkSuccessor 방법 에서 unpark 의 스 레 드 는 방법 acquireQueued 방법 으로 계속 작 동 되 고 parkAndCheckInterrupt 방법 은 false 로 돌아 간 다음 에 다시 자 물 쇠 를 가 져 오 려 고 시도 할 것 이다. 이번에 자 물 쇠 를 가 져 오 면 성공 할 것 이다. 그리고 acquire 방법 은 돌아 올 것 이다. 이 스 레 드 는 임계 구역 코드 를 실행 할 수 있다 는 것 을 의미한다.
    마지막 으로 대기 취소 방법 을 살 펴 보 겠 습 니 다. 자 물 쇠 를 가 져 올 때 시간 초과 와 시간 초과 가 설정 되 어 있 으 면 대기 취소 가 필요 합 니 다. 이 때 cancelAcquire 방법 으로 이동 합 니 다.
    private void cancelAcquire(Node node) {
            if (node == null)
                return;
            node.thread = null;
            //          ,            
            //      ,                        ,                    (              ),               ,          prev  
            Node pred = node.prev;
            while (pred.waitStatus > 0)
                node.prev = pred = pred.prev;
    
    
            Node predNext = pred.next;
            node.waitStatus = Node.CANCELLED;
    
            //               ,               
            if (node == tail && compareAndSetTail(node, pred)) {
                compareAndSetNext(pred, predNext, null);
            } else {
                //  pred     ,               pred     
                //  pred    SIGNAL,   pred      SIGNAL        ,    pred                    ,             
                int ws;
                if (pred != head &&
                    ((ws = pred.waitStatus) == Node.SIGNAL ||
                     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                    pred.thread != null) {
                    // pred                ,             
                    Node next = node.next;
                    if (next != null && next.waitStatus <= 0)
                        compareAndSetNext(pred, predNext, next);
                } else {
                    //  pred    ,                ,        
                    //  pred              ,              ,    acquireQueued        ,    ,              SIGNAL     
                    unparkSuccessor(node);
                }
    
                node.next = node; // help GC
            }
        }
    

    좋은 웹페이지 즐겨찾기