자바 에서 읽 기와 쓰기 자물쇠 의 디자인 및 구현

8620 단어 자바
읽 기와 쓰기 가 적은 장면 에 대해 자바 는 Lock 인 터 페 이 스 를 실현 하 는 읽 기와 쓰기 잠 금 ReentrantReadWriteLock(RRW)을 제공 했다.이전에 ReentrantLock 은 하나의 독점 잠 금 이 고 같은 시간 에 하나의 스 레 드 만 접근 할 수 있다 고 분석 한 적 이 있다.
한편,RRW 는 여러 개의 읽 기 스 레 드 를 동시에 방문 할 수 있 지만 스 레 드 와 읽 기 스 레 드,쓰기 스 레 드 와 쓰기 스 레 드 를 동시에 방문 할 수 없습니다.읽 기와 쓰기 자물쇠 내부 에 두 개의 자 물 쇠 를 유지 하고 있 습 니 다.하 나 는 읽 기 동작 에 사용 되 는 ReadLock 이 고 하 나 는 쓰기 동작 에 사용 되 는 WriteLock 입 니 다.
읽 기와 쓰기 자 물 쇠 는 다음 과 같은 세 가지 기본 원칙 을 준수 한다.
  • 여러 스 레 드 가 공유 변 수 를 동시에 읽 을 수 있 도록 합 니 다.
  • 하나의 스 레 드 만 공유 변 수 를 쓸 수 있 습 니 다.
  • 스 레 드 가 쓰기 작업 을 수행 하고 있 으 면 스 레 드 읽 기 공유 변 수 를 읽 지 못 합 니 다.

  • 읽 기와 쓰기 자 물 쇠 는 어떻게 실현 합 니까?
    RRW 도 AQS 를 기반 으로 이 루어 졌 습 니 다.사용자 정의 동기 화 장치(AQS 계승)는 동기 화 상태 state 에서 여러 개의 읽 기 스 레 드 와 쓰기 스 레 드 상 태 를 유지 해 야 합 니 다.RRW 의 방법 은 높낮이 를 사용 하여 하나의 성형 제어 두 가지 상 태 를 실현 하 는 것 이다.하 나 는 int 가 4 개의 바이트,하 나 는 8 개의 바이트 를 차지한다.그래서 고 16 위 는 읽 고,저 16 위 는 쓴다.
    abstract static class Sync extends AbstractQueuedSynchronizer {
    
      static final int SHARED_SHIFT   = 16;
    
      // 10000000000000000(65536)
      static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    
      // 65535
      static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    
      //1111111111111111
      static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    
      //   (   )   ,    16   
      static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    
      //   (   )   
      static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
     }
    

    읽 기 자물쇠 가 져 오기
    스 레 드 가 읽 기 자 물 쇠 를 가 져 올 때 먼저 동기 상태 가 16 비트 가 낮은 것 을 판단 하고,쓰기 자물쇠 가 존재 하면 자 물 쇠 를 가 져 오 는 데 실패 하고,CLH 대기 열 에 들 어가 서 막 아야 하 는 지,반대로 현재 스 레 드 가 막 혀 야 하 는 지 판단 하고,막 히 지 않 으 면 CAS 동기 화 상 태 를 시도 하여 동기 자 물 쇠 를 읽 기 상태 로 성공 적 으로 업데이트 합 니 다.
     protected final int tryAcquireShared(int unused) {
               
      Thread current = Thread.currentThread();
      int c = getState();
      //           ,     
      if (exclusiveCount(c) != 0 &&
          getExclusiveOwnerThread() != current)
          return -1;
      //       
      int r = sharedCount(c);
    
      //        readerShouldBlock()  true  CLH           
      // CAS        
      if (!readerShouldBlock() &&
          r < MAX_COUNT &&
          compareAndSetState(c, c + SHARED_UNIT)) {
    
          //       readLock     
    
          return 1;
      }
    
      //                
      return fullTryAcquireShared(current);
    }
    
    final int fullTryAcquireShared(Thread current) {
      
      //     
      for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
          //             ,   CLH    
          if (getExclusiveOwnerThread() != current)
            return -1;
        } 
    
        //   reader     
        else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                //           ,         。   CLH    
                if (rh.count == 0)
                    return -1;
            }
        }
    
        //            
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    
        // CAS     
        if (compareAndSetState(c, c + SHARED_UNIT)) {
          
          //     readLock     
    
          return 1;
        }
      }
    
    }
    SHARED_UNIT의 값 은 65536 이다.즉,읽 기 자 물 쇠 를 처음 얻 은 후에 state 의 값 은 65536 이 되 었 다.공정 한 잠 금 실현 에서 CLH 대기 열 에 줄 을 서 는 스 레 드 가 있 으 면readerShouldBlock()방법 은 true 로 돌아 갑 니 다.불공평 한 잠 금 의 실현 은 CLH 대기 열 에 잠 금 을 가 져 오 기 를 기다 리 는 스 레 드 가 존재 하면 true 로 돌아 갑 니 다.
    또한 주의해 야 할 것 은 읽 기 자 물 쇠 를 가 져 올 때 현재 스 레 드 에 쓰기 자 물 쇠 를 가지 고 있다 면 읽 기 자 물 쇠 를 성공 적 으로 가 져 올 수 있 습 니 다.뒤에 자물쇠 의 강등 이 언급 될 것 입 니 다.만약 당신 이 그곳 의 코드 에 대해 의문 이 있다 면,여기 서 자 물 쇠 를 신청 하 는 코드 를 돌아 볼 수 있 습 니 다.
    읽 기 잠 금 해제
    protected final boolean tryReleaseShared(int unused) {
               
      for (;;) {
        int c = getState();
        //   65536
        int nextc = c - SHARED_UNIT;
        //    state    0        
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
    }

    자 물 쇠 를 풀 때 state 의 값 은 65536 을 빼 야 합 니 다.읽 기 자 물 쇠 를 처음 가 져 온 후에 state 값 은 65536 이 되 었 기 때 문 입 니 다.
    모든 스 레 드 가 읽 기 자 물 쇠 를 풀 때state==0에 만 자 물 쇠 를 풀 었 습 니 다.예 를 들 어 100 개의 스 레 드 가 읽 기 자 물 쇠 를 가 져 왔 고 마지막 스 레 드 가 실행tryReleaseShared방법 을 실행 할 때 만 자 물 쇠 를 풀 었 습 니 다.이때 CLH 대기 열 에 있 는 줄 서기 스 레 드 를 깨 울 것 입 니 다.
    쓰기 잠 금 가 져 오기
    하나의 스 레 드 가 자 물 쇠 를 가 져 오 려 고 시도 할 때 동기 상태 state 가 0 인지 먼저 판단 합 니 다.state 가 0 이면 다른 스 레 드 가 잠 금 을 가 져 오지 않 았 음 을 설명 합 니 다.state 가 0 이 아니라면 다른 스 레 드 가 자 물 쇠 를 가 져 왔 다 는 뜻 입 니 다.
    이때 state 의 낮은 16 비트(w)가 0 인지 여 부 를 판단 합 니 다.w 가 0 이면 다른 스 레 드 가 읽 기 자 물 쇠 를 가 져 왔 음 을 표시 합 니 다.이때 CLH 대기 열 에 들 어가 서 차단 대기 합 니 다.
    만약 w 가 0 이 아니라면 다른 스 레 드 가 쓰기 자 물 쇠 를 가 져 왔 다 는 것 을 설명 합 니 다.이 때 는 쓰기 자 물 쇠 를 가 져 온 것 이 현재 스 레 드 인지 아 닌 지 판단 해 야 합 니 다.그렇지 않 으 면 CLH 대기 열 에 들 어가 서 차단 하고 기 다 립 니 다.만약 에 쓰기 자 물 쇠 를 가 져 온 것 이 현재 스 레 드 라면 현재 스 레 드 가 쓰기 자 물 쇠 를 가 져 온 것 이 최대 횟수 를 초과 하 였 는 지 판단 하고 초과 하면 이상반대로 동기 화 상 태 를 업데이트 합 니 다.
    //     
    protected final boolean tryAcquire(int acquires) {
               
      Thread current = Thread.currentThread();
      int c = getState();
      int w = exclusiveCount(c);
    
      //   state   0
      if (c != 0) {
          //      
          if (w == 0 || current != getExclusiveOwnerThread())
              return false;
    
          //                    65535
          if (w + exclusiveCount(acquires) > MAX_COUNT)
              throw new Error("Maximum lock count exceeded");
          
          //    
          setState(c + acquires);
          return true;
      }
      //        writerShouldBlock()     false
      // CAS  state  
      if (writerShouldBlock() ||
          !compareAndSetState(c, c + acquires))
          return false;
    
      // CAS   ,               
      setExclusiveOwnerThread(current);
      return true;
    }

    공정 한 잠 금 실현 중 CLH 대기 열 에 줄 을 서 는 스 레 드 가 존재 하면writerShouldBlock()방법 은 true 로 되 돌아 갑 니 다.이때 잠 금 을 쓰 는 스 레 드 를 가 져 오 는 것 이 막 힙 니 다.
    자 물 쇠 를 풀다
    자 물 쇠 를 풀 어 주 는 논 리 는 비교적 간단 하 다.
     protected final boolean tryRelease(int releases) {
      //            
      if (!isHeldExclusively())
          throw new IllegalMonitorStateException();
      
      int nextc = getState() - releases;
      boolean free = exclusiveCount(nextc) == 0;
    
      //           
      if (free)
          setExclusiveOwnerThread(null);
      setState(nextc);
      return free;
    }
    

    자물쇠 의 업그레이드?
    //      
    readLock.lock();
    try {
      v = map.get(key);
      if(v == null) {
        writeLock.lock();
        try {
          if(map.get(key) != null) {
            return map.get(key);
          }
    
          //       ,  
        } finally {
          writeLock.unlock();
        }
      }
    } finally {
      readLock.unlock();
    }
    

    위 에서 캐 시 데이터(이것 도 RRW 의 응용 장면)를 가 져 오 는 코드 에 대해 먼저 읽 기 자 물 쇠 를 가 져 온 다음 에 자 물 쇠 를 쓰 는 것 으로 업그레이드 하 는 행 위 를 자물쇠 의 업그레이드 라 고 합 니 다.안 타 깝 게 도 RRW 는 지원 하지 않 습 니 다.그러면 자 물 쇠 를 영구적 으로 기다 리 게 되 고 결국은 스 레 드 가 영구적 으로 막 힐 수 있 습 니 다.그래서 자물쇠 의 업 그 레이 드 는 허용 되 지 않 습 니 다.
    자물쇠 의 강등
    자물쇠 의 업 그 레이 드 는 허용 되 지 않 지만 자물쇠 의 강등 은 가능 하 다.
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    ReadLock readLock = lock.readLock();
    
    WriteLock writeLock = lock.writeLock();
    
    Map dataMap = new HashMap();
    
    public void processCacheData() {
      readLock.lock();
    
      if(!cacheValid()) {
    
        //     ,     
        readLock.unlock();
    
        writeLock.lock();
    
        try {
          if(!cacheValid()) {
              dataMap.put("key", "think123");
          }
    
          //      
          readLock.lock();
        } finally {
            writeLock.unlock();
        }
      }
    
      try {
        //       
        System.out.println(dataMap);
      } finally {
          readLock.unlock();
      }
    }
    
    public boolean cacheValid() {
        return !dataMap.isEmpty();
    }
    

    RRW 가 주의해 야 할 문제
  • 읽 기 가 많 고 쓰기 가 적은 경우 RRW 는 쓰기 스 레 드 에 배 고 픔(Starvation)문 제 를 겪 게 됩 니 다.즉,쓰기 스 레 드 는 잠 금 까지 경쟁 이 지연 되 어 대기 상태 에 있 습 니 다.
  • 잠 금 지원 조건 변 수 를 쓰 고 읽 기 잠 금 은 지원 되 지 않 습 니 다.읽 기 잠 금 호출 new Condition()은 Unsupported Operation Exception 이상
  • 을 던 집 니 다.
    추천
    이전에 AQS 의 실현,ReentrantLock 의 실현 이 라 고 쓴 적 이 있 습 니 다.아래 의 글 을 참고 하 시기 바 랍 니 다.
  • AQS 소스 코드 분석
  • ReentrantLock 분석

  • 나 를 지 켜 봐,길 을 잃 지 않 아.
    형님,저 에 게 좋아요 와 관심 을 주 시 겠 어 요?

    좋은 웹페이지 즐겨찾기