JUC 패키지 의 분할 치료 전략-성능 향상 을 위 한 생 성

머리말
이번 공 유 는 JUC 가방 의 재 미 있 는 종 류 를 함께 연구 하고 자 합 니 다.AtomicLong&LongAdder,ThreadLocalRandom 원 리 를 포함 합 니 다.
2.AtomicLong&LongAdder
2.1 AtomicLong 류
AtomicLong 은 JUC 패키지 가 제공 하 는 원자 성 조작 류 로 그 내 부 는 CAS 를 통 해 계수 에 대한 원자 성 업데이트 작업 을 보장 한다.
원본 코드 를 뒤 져 보면 내 부 는 UnSafe(rt.jar)와 같은 CAs 작업 을 통 해 내부 의 카운터 변수 인 long value 를 원자 적 으로 업데이트 할 수 있 습 니 다.예 를 들 어 JDK 8 에서:
    public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }

그 중에서 unsafe.getAndAddLong 의 코드 는 다음 과 같다.
  public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
    {
      l = getLongVolatile(paramObject, paramLong1);//(1)
    } while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));//(2)
    return l;
  }

최종 호출 은 native 방법 compare AndSwapLong 원자 조작 임 을 알 수 있 습 니 다.
여러 스 레 드 가 같은 AtomicLong 인 스 턴 스 의 increment AndGet 방법 을 호출 하면 여러 스 레 드 가 unsafe.getAndAddLong 방법 으로 실 행 됩 니 다.그리고 여러 스 레 드 가 코드(1)에 실 행 됩 니 다.계수기 의 값 을 가 져 온 다음 에 코드(2)를 실행 합 니 다.여러 스 레 드 가 동시에 코드(2)를 실행 하면 CAS 가 원자 성 을 가지 기 때문에그래서 하나의 스 레 드 만 업데이트 에 성공 하고 트 루 로 돌아 가 순환 을 종료 합 니 다.전체 업데이트 작업 은 OK 입 니 다.다른 스 레 드 는 CAS 가 false 로 돌아 가지 못 하면(1)에서 현재 계수기 의 값 을 한 번 반복 해서 가 져 온 다음(2)실행 을 시도 합 니 다.이것 은 CAS 의 자전 작업 이 라 고 합 니 다.본질은 Cpu 자원 을 사용 하여 자물쇠 로 가 져 온 컨 텍스트 전환 등 비용 으로 바 꾸 는 것 입 니 다.
2.2 LongAdder 류
AtomicLong 류 는 개발 자 에 게 스 레 드 안전 한 계수 기 를 사용 하 는 데 편 의 를 제공 합 니 다.그러나 AtomicLong 은 높 고 보 내 는 데 문제 가 있 습 니 다.상기 와 같이 대량의 스 레 드 가 같은 AtomicLong 의 인 스 턴 스 를 호출 하 는 방법 을 사용 할 때 하나의 스 레 드 만 CAS 카운터 의 값 이 성공 하고 실패 한 스 레 드 는 제자리 에서 cpu 를 차지 하여 자전 재 시도 합 니 다.이번 에는 대량의 스 레 드 가 cpu 를 헛되이 낭비 하여 제자리 에서 자전 하 게 만 들 었 다.
JDK 8 에 LongAdder 류 가 추가 되 었 습 니 다.이 는 분 리 된 전략 으로 같은 변수의 합병 경쟁 도 를 줄 이 는 것 입 니 다.LongAdder 의 핵심 사상 은 하나의 원자 변 수 를 여러 변수 로 분해 하여 같은 여러 개의 스 레 드 로 여러 개의 자원 을 경쟁 하 게 하 는 것 입 니 다.그러면 모든 자원 의 스 레 드 수 를 경쟁 하 는 것 입 니 다.다음은 도형 을 통 해 두 디자인 의 차이 점 을 이해한다.
위의 그림 과 같이 AtomicLong 은 여러 스 레 드 가 같은 원자 변 수 를 동시에 경쟁 합 니 다.
위의 그림 에서 LongAdder 내부 에서 여러 개의 Cell 변 수 를 유지 하고 똑 같은 병발 량 의 상황 에서 하나의 변수 업데이트 작업 을 쟁탈 하 는 스 레 드 량 이 줄 어 들 것 이다.이것 은 공유 자원 을 쟁탈 하 는 병발 량 을 변형 적 으로 감소 시 킨 것 이다.
다음은 셀 의 구 조 를 살 펴 보 겠 습 니 다.
    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe   
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

LongAdder 는 초기 화 지연 원자 업데이트 배열(기본 적 인 상황 에서 Cell 배열 은 null)과 기본 값 변수 base 를 유지 합 니 다.Cells 가 메모 리 를 사용 하 는 것 이 상대 적 으로 크기 때문에 처음에는 만 들 지 않 고 필요 할 때 만 듭 니 다.즉,타성 생 성 입 니 다.
처음에 cell 배열 이 null 이 고 동시 다발 스 레 드 가 적 을 때 모든 누적 작업 은 base 변 수 를 진행 하 는 것 이 라 고 판 단 했 을 때 AtomicLong 으로 퇴화 되 었 다.cell 배열 의 크기 는 2 의 N 제곱 크기 를 유지 합 니 다.초기 화 할 때 Cell 배열 의 중 Cell 요소 개 수 는 2 이 고 배열 안의 변수 실 체 는 Cell 형식 입 니 다.
여러 스 레 드 가 같은 Cell 원자 변 수 를 쟁탈 할 때 실패 하면 현재 cell 변수 에서 CAS 재 시도 가 아니 라 다른 Cell 변수 에서 CAS 시 도 를 시도 합 니 다.이 변 화 는 현재 스 레 드 재 시도 시 CAS 가 성공 할 가능성 을 증가 시 킵 니 다.마지막 으로 LongAdder 현재 값 을 가 져 올 때 모든 Cell 변수의 value 값 을 누적 한 후 base 를 추가 하여 되 돌려 줍 니 다.다음 코드:
    public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

위의 코드 에서 알 수 있 듯 이 먼저 base 의 값 을 sum 변수 에 할당 한 다음 에 순환 을 통 해 모든 cell 요소 의 value 값 을 sum 변수 에 누적 한 다음 에 sum 으로 되 돌려 줍 니 다.
사실은 이것 은 분 리 된 전략 이다.먼저 병발 량 을 여러 개의 원자 변수 에 분담 하고 여러 개의 스 레 드 가 동시에 서로 다른 원자 변 수 를 조작 한 다음 에 계 수 를 얻 을 때 모든 원자 변수의 계수 와 누적 을 얻는다.
사고 문제:
  • cell 배열 을 언제 초기 화 합 니까?
  • 현재 스 레 드 는 cell 의 요 소 를 선택 하여 접근 하 는 방법
  • cell 에서 요소 업데이트 의 스 레 드 안전 을 확보 하면
  • cell 배열 은 언제 확장 되 고 cell 요소 의 개 수 는 무한 확장 할 수 있 습 니까?

  • 성능 대비 http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/
    3.랜 덤&ThreadLocalRandom
    3.1 랜 덤 류 의 원리 와 한계 성
    JDK 7 이전에 현재 자바 util.Random 을 포함 하여 비교적 광범 위 한 난수 생 성 도구 류 를 사용 해 야 합 니 다.다음은 간단 한 코드 를 통 해 자바 util.Random 이 어떻게 사용 하 는 지 보 겠 습 니 다.
    public class RandomTest {
        public static void main(String[] args) {
    
            //(1)               
            Random random = new Random();
            //(2)  10  0-5(  0,   5)      
            for (int i = 0; i < 10; ++i) {
                System.out.println(random.nextInt(5));
            }
        }
    }
  • 코드(1)기본 난수 생 성 기 를 만 들 고 기본 피 드 를 사용 합 니 다.
  • 코드(2)출력 10 개가 0-5(0 포함,5 포함 하지 않 음)사이 의 임 의 수 입 니 다.
  •     public int nextInt(int bound) {
            //(3)    
            if (bound <= 0)
                throw new IllegalArgumentException(BadBound);
            //(4)            
            int r = next(31);
            //(5)           
            ...
            return r;
        } 

    위 코드 에서 알 수 있 듯 이 새로운 난수 생 성 은 두 단계 가 필요 합 니 다.
  • 먼저 오래된 씨앗 의 계산 에 따라 새로운 씨앗 을 만들어 야 한다.
  • 그리고 새로운 피 드 와 bound 변수 에 따라 일정한 알고리즘 을 통 해 새로운 무 작위 수 를 계산 합 니 다.

  • 다음은 next()코드 를 보 겠 습 니 다.
        protected int next(int bits) {
            long oldseed, nextseed;
            AtomicLong seed = this.seed;
            do {
                //(6)            
                oldseed = seed.get();
                //(7)             
                nextseed = (oldseed * multiplier + addend) & mask;
                //(8)           
            } while (!seed.compareAndSet(oldseed, nextseed));
            //(9)
            return (int)(nextseed >>> (48 - bits));
        }
  • 코드(6)원자 변수의 get 방법 으로 현재 원자 변수 피 드 의 값 을 가 져 옵 니 다
  • 코드(7)구체 적 인 알고리즘 에 따라 현재 피 드 값 으로 새로운 피 드 를 계산 합 니 다
  • 코드(8)는 CAS 작업 을 사용 하고 새로운 피 드 를 사용 하여 오래된 피 드 를 업데이트 합 니 다.다 중 스 레 드 에서 여러 스 레 드 가 코드(6)를 동시에 실행 할 수 있 습 니 다.그러면 여러 스 레 드 가 모두 받 을 수 있 는 현재 피 드 의 값 은 같 습 니 다.그리고 실행 절차(7)가 계산 한 새 피 드 도 똑 같 습 니 다.그러나 절차(8)의 CAS 작업 은 하나의 스 레 드 만 오래된 피 드 를 새로운 것 으로 업데이트 할 수 있 도록 보장 합 니 다.실패 한 스 레 드 는 순환 을 통 해 업 데 이 트 된 피 드 를 현재 피 드 로 계산 합 니 다.이것 은 무 작위 수의 무 작위 성 을 보장 합 니 다.
  • 코드(9)는 고정 알고리즘 을 사용 하여 새로운 피 드 에 따라 무 작위 수 를 계산 하고 되 돌려 줍 니 다.

  • 3.2 ThreadLocalRandom
    Random 클래스 생 성 난수 원리 및 부족:모든 Random 인 스 턴 스 에는 현재 피 드 의 값 을 기록 하 는 원자 변 수 를 가지 고 있 습 니 다.새로운 랜 덤 수 를 생 성 하려 면 현재 피 드 에 따라 새로운 피 드 를 계산 하고 원자 변 수 를 업데이트 해 야 합 니 다.
    다 중 스 레 드 에서 하나의 Random 인 스 턴 스 를 사용 하여 랜 덤 수 를 생 성 할 때 여러 스 레 드 가 동시에 랜 덤 수 를 계산 하여 새로운 피 드 를 계산 할 때 여러 스 레 드 는 같은 원자 변수의 업데이트 작업 을 경쟁 합 니 다.원자 변수의 업 데 이 트 는 CAS 작업 이 고 하나의 스 레 드 만 성공 하기 때문에 CAS 작업 에 실패 한 대량의 스 레 드 는 자전 재 시도 합 니 다.한편,대량의 스 레 드 의 자전 재 시 도 는 병행 성능 을 낮 추고 CPU 자원 을 소모 할 수 있 습 니 다.이 문 제 를 해결 하기 위해 ThreadLocal Random 류 가 생 겨 났 습 니 다.
    public class RandomTest {
    
        public static void main(String[] args) {
            //(10)          
            ThreadLocalRandom random =  ThreadLocalRandom.current();
    
            //(11)  10  0-5(  0,   5)      
            for (int i = 0; i < 10; ++i) {
                System.out.println(random.nextInt(5));
            }
        }
    }

    위의 코드(10)와 같이 ThreadLocalRandom.current()를 호출 하여 현재 스 레 드 의 난수 생 성 기 를 가 져 옵 니 다.ThreadLocal Random 의 실현 원 리 를 분석 해 보 겠 습 니 다.
    이름 으로 보면 ThreadLocal 류 를 연상 시 킬 수 있 습 니 다.ThreadLocal 은 모든 스 레 드 에 변 수 를 복사 하도록 합 니 다.모든 스 레 드 가 변 수 를 조작 할 때 실제 적 으로 자신의 로 컬 메모리 에 있 는 복사 본 을 조작 하여 공유 변 수 를 동기 화 하 는 것 을 피 합 니 다.실제로 스 레 드 로 컬 랜 덤 의 실현 도 이 원리 다.Random 의 단점 은 여러 스 레 드 가 원자 피 드 변 수 를 사용 하여 원자 변 수 를 업데이트 하 는 경쟁 을 초래 할 수 있다 는 것 이다.이 원 리 는 아래 그림 을 통 해 표현 할 수 있다.
    그러면 모든 스 레 드 가 자신의 피 드 변 수 를 유지 하고 모든 스 레 드 가 랜 덤 수 를 생 성 할 때 자신의 로 컬 메모리 에 있 는 오래된 피 드 에 따라 새로운 피 드 를 계산 하고 새로운 피 드 를 사용 하여 오래된 피 드 를 업데이트 한 다음 에 새로운 피 드 에 따라 랜 덤 수 를 계산 하면 경쟁 문제 가 존재 하지 않 습 니 다.이것 은 병행 성능 을 크게 향상 시 킬 것 입 니 다.아래 그림 과 같이 ThreadLocalRandom 원 리 는 다음 그림 으로 표현 할 수 있 습 니 다.
    Thread 클래스 에는 몇 개의 변수 가 있 습 니 다.
        /** The current seed for a ThreadLocalRandom */
        @sun.misc.Contended("tlr")
        long threadLocalRandomSeed;
    
        /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
        @sun.misc.Contended("tlr")
        int threadLocalRandomProbe;
    

    사고 문제:
  • 각 스 레 드 의 초기 피 드 는 어떻게 생 성 됩 니까?
  • 여러 스 레 드 에서 발생 하 는 씨앗 이 다 르 면
  • 총화
    졸작 자바 병렬 프로 그래 밍 의 아름다움 한 책의 장절 추출이번 공 유 는 먼저 AtomicLong 의 내부 실현 과 존재 하 는 단점 을 설명 한 다음 에 LongAdder 가 분 리 된 전략 으로 여러 개의 원자 변 수 를 사용 하여 하나의 원자 변수 경쟁의 병발 도 를 줄 이 는 것 을 설명 했다.그 다음 에 Random 과 그 단점 을 간단하게 소개 했다.마지막 으로 ThreadLocal Random 은 ThreadLocal 의 사상 을 빌려 다 중 스 레 드 가 같은 원자 변수 경쟁 자물쇠 에 가 져 온 성능 손실 을 해결 했다.사실 JUC 가방 에는 포크-join 프레임 워 크 등 다른 전형 적 인 구성 요소 도 있다.
    저자:더하기
    원문 을 읽다
    본 고 는 운 서 지역사회 의 오리지널 내용 으로 허락 없 이 전재 할 수 없다.

    좋은 웹페이지 즐겨찾기