Java ConcurrentMap

Java ConcurrentMap

자바 프로그래밍 언어의 Java Collections Framework 버전 1.5 이상은 원래의 정규 단일 스레드 맵을 정의하고 구현하며, 또한 다른 동시 인터페이스들 간의 인터페이스를 구현하는 새로운 스레드 세이프 맵을 정의하고 구현한다. 자바 1.6에서는 인터페이스가 추가되어 확장되었으며, 하위 인터페이스 조합으로 인터페이스가 추가되었다.

Java Map 인터페이스

버전 1.8 맵 인터페이스 다이어그램은 아래와 같은 형태를 가지고 있다. 세트 API는 해당하지만 이름이 다른 방법을 사용하지만 값이 항상 무시할 수 있는 특정 상수인 해당 맵의 하위 케이스로 간주할 수 있다. 맨 아래에는 java.util.concurrent가 있다.다중 상속인 ConcurrentNavigableMap.

구현

ConcurrentHashMap

Java.util에 정의된 대로 순서가 지정되지 않은 액세스.맵 인터페이스, java.util.concurrent.ConcurrentHashMap은 java.util.concurrent를 구현한다.ConcurrentMap. 메커니즘은 해시 테이블에 대한 해시 액세스로서, 각 항목에는 키, 값, 해시 및 다음 참조가 포함되어 있다. Java 8 이전까지는 테이블의 '세그먼트'에 대한 액세스를 직렬화할 때마다 여러 개의 잠금이 있었다. 자바 8에서는 리스트의 헤드 자체에 네이티브 동기화를 사용하며, 불행한 해시 충돌로 인해 리스트가 너무 크게 자랄 것이라고 위협할 때 리스트가 작은 나무로 변이될 수 있다. 또한 자바 8은 비교해서 설정한 원시적인 것을 낙관적으로 사용하여 초기 헤드를 표에 배치하는데, 이것은 매우 빠르다. 성능은 O(n)이지만 재탕이 필요할 때 간혹 지연이 발생한다. 해시 테이블이 확장된 후에는 절대 축소되지 않으며, 항목이 제거된 후 메모리 '삭제'로 이어질 수 있다.

ConcurrentSkipListMap

java.util에 의해 정의된 순서에 따른 액세스.NavigableMap 인터페이스, java.util.concurrent.ConcurrentSkipListMap은 Java 1.6에서 추가되었으며, Java.util.concurrent를 구현한다.ConcurrentMap 및 Java.util.concurrent.ConcurrentNavigableMap. 이것은 나무를 만들기 위해 잠금 없는 기술을 사용하는 스킵 리스트다. 성능은 O(log(n))이다.

씨트리

  • Ctrie 3e-based Lock-free 트리.

동시수정문제

Java 1.5 java.util.concurrent 패키지로 해결된 한 가지 문제는 동시 수정의 문제다. 그것이 제공하는 수집 클래스는 복수의 스레드에 의해 신뢰성 있게 사용될 수 있다.

모든 스레드 공유 비동일 지도 및 기타 컬렉션은 동시 수정을 방지하기 위해 네이티브 동기화 등 어떤 형태의 명시적 잠금을 사용할 필요가 있거나, 또는 프로그램 로직으로부터 동시 변경이 발생할 수 없음을 증명할 방법이 있어야 한다. 여러 스레드에 의한 지도를 동시에 수정하면 지도 내부의 데이터 구조의 일관성이 파괴되어 버그가 거의 또는 예측 불가능하게 나타나며 탐지 및 고치기 어려운 버그가 발생할 수 있다. 또한, 다른 스레드 또는 스레드에 의한 읽기 액세스 권한을 가진 한 스레드에 의한 동시 수정은, 맵의 내부 일관성은 파괴되지 않지만, 때때로 독자에게 예측할 수 없는 결과를 제공할 것이다. 동시 수정을 방지하기 위해 외부 프로그램 로직을 사용하면 비동시 수집을 사용할 수 있지만, 코드 복잡성이 증가하고 기존 및 미래 코드에서 예측 불가능한 오류 위험이 발생한다. 그러나 잠금 장치 또는 프로그램 로직 중 하나가 컬렉션과 접촉할 수 있는 외부 스레드를 조정할 수 없다.

수정 카운터

동시 수정 문제를 해결하기 위해 비동시 Map 구현 및 기타 수집은 내부 수정 카운터를 사용하며, 변경사항을 보기 위해 읽기 전과 후에 참조된다. 작성자는 수정 카운터를 증분한다. 동시 수정은 이 메커니즘에 의해 감지되어 java.util을 발생시킨다.ConcurrentModification예외지만 모든 경우에 발생한다고 보장되는 것은 아니며 의존해서는 안 된다. 카운터 유지관리는 성능저하기도 하다. 성능상의 이유로 카운터는 휘발성이 없기 때문에 변경 사항이 스레드 간에 전파될 것이라고 보장할 수 없다.

컬렉션synchronedMap()

동시 수정 문제에 대한 한 가지 해결책은 컬렉션의 공장에서 제공하는 특정 래퍼 클래스를 사용하는 것이다. public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 내부 뮤텍스에서 동기화하는 메소드로 기존의 비 스레드 안전 맵을 감싼다. 다른 종류의 컬렉션에도 포장지가 있다. 이것은 부분적인 해결책이다. 왜냐하면 아직 포장되지 않은 참조를 유지하거나 얻는 스레드에 의해 기반 맵에 부주의하게 접근할 수 있기 때문이다. 또한 모든 컬렉션은 를 구현하지만 동기화된 포장 맵과 다른 래핑된 컬렉션은 동기화된 반복기를 제공하지 않으므로 동기화는 느리고 오류가 발생하기 쉬우며 동기화된 맵의 다른 소비자에 의해 중복될 것으로 예상할 수 없는 클라이언트 코드에 맡겨진다. 전체 반복 기간도 보호해야 한다. 또한 서로 다른 위치에서 두 번 포장된 맵에는 동기화가 작동하는 내부 뮤텍스 개체가 서로 달라 중첩될 수 있다. 대표단은 성능저하제지만 현대의 Just-in-Time 컴파일러는 종종 심하게 인라인으로 되어 있어 성능저하를 제한한다. 포장지 내부의 포장 방법은 다음과 같다. 뮤텍스는 최종 객체일 뿐이고 m은 최종 포장 지도일 뿐이다.

    공중의 V 놓다(K 핵심을, V 가치를 매기다) {         동기화된 (뮤텍스) {돌아오다 m.놓다(핵심을, 가치를 매기다);}     } 

Iteration의 동기화는 다음과 같이 권장되지만, 이것은 내부 뮤텍스보다는 래퍼에 동기화되어 중복이 허용된다.[1]

    지도<, > 래핑맵 = 컬렉션.synchronizedMap(지도를 그리다);           ...     동기화된 (래핑맵) {         을 위해 ( s : 래핑맵.keySet()) {             // 일부 장기 작업 실행 가능              // 다른 모든 액세스를 지연시키는 여러 번         }     } 

네이티브 동기화

모든 맵은 Java 동기화 메커니즘에 의해 모든 액세스가 처리되도록 보장함으로써 멀티스레드 시스템에서 안전하게 사용될 수 있다.

    지도<, > 지도를 그리다 = 새로운 해시맵<, >();     ...     // 스레드 A     // 지도 자체를 자물쇠로 사용한다. 합의된 어떤 물체도 대신 사용할 수 있다.     동기화된(지도를 그리다) {        지도를 그리다.놓다("키","가치");     }     ..     // 나사산 B     동기화된 (지도를 그리다) {          결과 = 지도를 그리다.얻다("키");          ...      }     ...     // 나사산 C     동기화된 (지도를 그리다) {         을 위해 (엔트리<, > s : 지도를 그리다.엔트리 세트()) {             /*              * 일부 느린 작동으로 추정되는 빠른 작동이 지연될 수 있음.                * 개별 반복 시 동기화가 불가능함.              */              ...         }     } 

ReentrantReadWriteLock

java.util.concurrent를 사용하는 코드.ReentRentReadWriteLock은 기본 동기화와 유사하다. 단, 안전을 위해 예외 던지기 또는 파손/계속과 같은 조기 출구가 반드시 잠금 해제를 통과할 수 있도록 시도/종료 블록에 잠금장치를 사용해야 한다. 이 기술은 읽기가 서로 겹칠 수 있기 때문에 동기화를 사용하는 것보다 더 낫고, 읽기와 관련하여 쓰기의 우선순위를 어떻게 정할 것인가에 새로운 문제가 있다. 단순성을 위해 java.util.concurrent.ReentrantLock을 대신 사용할 수 있으므로 읽기/쓰기 구별이 없다. 다음과 같은 동기화보다 잠금 장치에 대한 더 많은 작업이 가능하다. tryLock() 그리고 tryLock(long timeout, TimeUnit unit).

    최종의 ReentrantReadWriteLock 자물쇠를 채우다 = 새로운 ReentrantReadWriteLock();     최종의 ReadLock readlock = 자물쇠를 채우다.readlock();     최종의 WriteLock writeLock = 자물쇠를 채우다.writeLock();     ..     // 스레드 A     해보다 {         writeLock.자물쇠를 채우다();         지도를 그리다.놓다("키","가치");         ...     } 종국에는 {         writeLock.자물쇠를 열다();     }     ...     // 나사산 B     해보다 {         readlock.자물쇠를 채우다();          s = 지도를 그리다.얻다("키");         ..     } 종국에는 {         readlock.자물쇠를 열다();     }      // 나사산 C     해보다 {         readlock.자물쇠를 채우다();         을 위해 (엔트리<, > s : 지도를 그리다.엔트리 세트()) {             /*              * 일부 느린 작동으로 추정되는 빠른 작동이 지연될 수 있음.                * 개별 반복 시 동기화가 불가능함.              */              ...         }     } 종국에는 {         readlock.자물쇠를 열다();     } 

호위함

상호 배제는 자물쇠에 쓰레드가 쌓여 JVM이 값비싼 대기자 행렬을 유지하고 대기 중인 나사산을 '주차'해야 하는 Lock 호송 문제를 가지고 있다. 실을 주차를 하고 푸는 것은 비용이 많이 들고, 느린 컨텍스트 스위치가 발생할 수 있다. 컨텍스트 스위치는 마이크로초에서 밀리초까지 걸리는 반면 지도 자체의 기본 작동은 보통 나노초까지 걸린다. 경합이 증가할수록 성능은 단일 스레드 처리량의 극히 일부분으로 떨어질 수 있다. 그러나 잠금 장치에 대한 경합이 없거나 거의 없는 경우 잠금 장치의 경합 테스트를 제외하고는 성능에 거의 영향을 미치지 않는다. 현대의 JVM은 대부분의 잠금 코드를 인라인으로 하여 그것을 단지 몇 개의 지시사항으로 축소하여 무연장 케이스를 매우 빠르게 유지할 것이다. 네이티브 동기화 또는 Java.util.concurrent와 같은 reentrant 기술.그러나 ReentRantReadWriteLock은 리엔트런시 깊이의 유지보수를 위해 추가적인 성능 저하시킬 수 있는 수하물을 가지고 있어, 무첨부 사례에도 영향을 미친다. 수송기 문제는 현대적인 JVMS로 완화되는 것처럼 보이지만 느린 컨텍스트 전환으로 숨길 수 있다: 이 경우 지연 시간은 증가하지만 처리량은 계속 허용될 것이다. 수백 개의 스레드로 컨텍스트 스위치 시간 10ms는 대기 시간을 초 단위로 생성한다.

다중 코어

상호 배제 솔루션은 한 번에 하나의 스레드만 맵 코드 안에 허용되기 때문에 다중 코어 시스템의 모든 컴퓨팅 성능을 활용하지 못한다. Java Collections Framework 등에 의해 제공되는 특정 동시 맵의 구현은 Lock free 프로그래밍 기법을 사용하여 복수의 코어를 이용하는 경우가 있다. 잠금 없는 기법은 일부 지도 내부 구조의 조건부 업데이트를 원자적으로 수행하기 위해 AtomicReference와 같은 많은 Java 클래스에서 이용할 수 있는 CompareAndSet() 내인성 방법과 같은 작업을 사용한다. compareAndSet() president는 일부 알고리즘에 대해 일부 객체의 특수 내부 부분에서 AndSet를 비교할 수 있는 네이티브 코드에 의해 JCF 클래스에 증강된다('안전하지 않은' 액세스 사용). 그 기법은 복잡하여 휘발성 변수에 의해 제공되는 스레드 간 통신의 규칙, 발생 전 관계, 특별한 종류의 잠금 없는 '재순환'(항상 진전을 만든다는 점에서 스핀 잠금장치와 같지 않음)에 의존한다. CompareAndSet()는 특수 프로세서별 지침에 의존한다. 모든 Java 코드가 다양한 동시 클래스에 대한 CompareAndSet() 방법을 다른 목적으로 사용하여 Lock-free 또는 심지어 유한한 대기 시간을 제공하는 Wait-free 동시성을 달성할 수 있다. 잠금 없는 기법은 많은 일반적인 경우에서 단순하며 스택과 같은 간단한 컬렉션을 가지고 있다.

이 다이어그램은 컬렉션을 사용하여 동기화하는 방법을 나타낸다.일반 해시맵(퍼플)을 감싸는 syncedMap()은 ConcurrentHashMap(빨간색)만큼 잘 확장되지 않을 수 있다. 그 외는 주문된 ConcurrentMaps AirConcurrentMap(파란색) 및 ConcurrentSkipListMap(CSLM 녹색)이다. (평탄한 지점은 보육원보다 더 큰 테이블을 생성하는 재시스트일 수 있으며 ConcurrentHashMap은 더 많은 공간을 차지한다. 참고 y축은 'puts K'라고 해야 한다. 시스템은 8코어 i7 2.5GHz로 GC를 방지하기 위해 -Xms5000m이다. GC 및 JVM 프로세스 확장은 곡선을 상당히 변화시키며, 일부 내부 잠금 없는 기술은 경합 시 가비지를 발생시킨다.

The hash tables are both fast

Only the ordered Maps are scaling, and the synchronized Map is falling back The synchronized Map has fallen back to be similar to the scaled ordered Maps

예측 가능한 지연 시간

그러나 상호 배제 접근법의 또 다른 문제는 어떤 단일 스레드 코드에 의한 완전한 원자성의 가정은 동시 환경에서 수용할 수 없을 정도로 긴 스레드 간 지연을 산발적으로 유발한다는 점이다. 특히, putAll()과 같은 Iterator 및 대량 작업은 지도 크기에 비례하는 시간이 오래 걸릴 수 있어, bulk가 아닌 작업에 대해 예측 가능한 낮은 지연 시간을 예상하는 다른 스레드를 지연시킬 수 있다. 예를 들어, 다중 스레드 웹 서버는 특정 값을 검색하는 다른 요청을 실행하는 다른 스레드의 반복 실행으로 인해 일부 응답이 지연되는 것을 허용할 수 없다. 이와 관련하여 맵을 잠그는 스레드는 실제로 잠금을 포기해야 하는 어떠한 요구사항도 가지고 있지 않으며, 소유자의 스레드의 무한 루프는 영구적인 차단을 다른 스레드에 전파할 수 있다. 느린 소유자 스레드는 때때로 중단될 수 있다. 해시 기반 지도는 재시동 중 자연 발생적인 지연을 겪게 된다.

약한 일관성

동시 수정 문제, 수송 문제, 예측 가능한 지연 시간 문제, 멀티 코어 문제에 대한 Java.util.conconcontinuity 패키지의 해결책은 약한 일관성이라고 불리는 아키텍처 선택을 포함한다. 이 선택은 업데이트가 진행 중인 경우에도 get() 같은 읽기가 차단되지 않고 업데이트 자체와 읽기가 겹치는 경우에도 허용된다는 것을 의미한다. 예를 들어, 일관성이 약하면 ConcurrentMap의 내용이 단일 스레드에 의해 반복되는 동안 변경될 수 있다. Iterator는 한 번에 한 스레드에 의해 사용되도록 설계된다. 따라서 예를 들어 상호 종속적인 두 개의 항목이 포함된 맵은 다른 스레드에 의해 수정되는 동안 판독기 스레드에 의해 일관되지 않은 방식으로 보일 수 있다. 항목(k1,v)의 키를 항목(k2,v)으로 원자로 변경해야 하는 업데이트는 제거(k1)를 수행한 다음 퍼트(k2,v)를 수행해야 하며, 반복은 항목을 놓치거나 두 위치에서 볼 수 있다. 검색은 해당 키에 대해 이전에 완료된 최신 업데이트를 반영하는 지정된 키의 값을 반환한다. 따라서 '해프전' 관계가 있다.

ConcurrentMaps가 전체 테이블을 잠글 수 있는 방법은 없다. ConcurrentModify의 가능성이 없음비동일 지도에 대한 우발적인 동시 수정이 있는 경우는 예외. 크기() 방법은 어떤 식으로든 전체 지도를 스캔해야 할 수 있기 때문에, 해당 비유동 지도 및 빠른 접근을 위해 크기 필드를 포함하는 다른 컬렉션과는 달리 오랜 시간이 걸릴 수 있다. 동시 수정이 발생하는 경우, 결과는 지도 상태를 반영하지만 반드시 단일 일치 상태는 아니므로 크기(), isVery() 및 includValue()를 모니터링하는 데만 사용할 수 있다.

ConcurrentMap 1.5 방법

ConcurrentMap이 제공하는 일부 운영은 수정의 원자성을 허용하기 위해 Map(확장되는 지도)에 있지 않다. 교체품(K, v1, v2)은 K가 식별한 엔트리에 v1이 있는지 테스트하며, 발견된 경우에만 v1이 원자적으로 v2로 대체된다. 새로운 교체품(k,v)은 k가 지도에 이미 있는 경우에만 put(k,v)을 할 것이다. 또한 putIfAbsent(k,v)는 k가 지도에 아직 없는 경우에만 put(k,v)을 하고 remove(k,v)는 v가 있는 경우에만 Entry for v를 제거한다. 이러한 원자성은 일부 다중 스레드 사용 사례에 중요할 수 있지만 약한 일관성 제약과는 관련이 없다.

ConcurrentMaps의 경우, 다음은 원자적이다.

m.putIfAbsent(k, v)는 원자적이지만 다음과 같다.

    만일 (k == 무효의    v == 무효의)             던지다 새로운 널포인터예외();     만일 (!m.포함키(k)) {         돌아오다 m.놓다(k, v);     } 다른 {         돌아오다 m.얻다(k);     } 

m replace(k, v)는 원자적이지만 다음과 같다.

    만일 (k == 무효의    v == 무효의)             던지다 새로운 널포인터예외();     만일 (m.포함키(k)) {         돌아오다 m.놓다(k, v);     } 다른 {         돌아오다 무효의;     } 

m.dv(k, v1, v2)는 원자적이지만 다음과 같다.

    만일 (k == 무효의    v1 == 무효의    v2 == 무효의)             던지다 새로운 널포인터예외();      만일 (m.포함키(k) && 물건들.대등하다(m.얻다(k), v1)) {         m.놓다(k, v2);         돌아오다 진실의;      } 다른         돌아오다 거짓의;      } 

m.remove(k, v)는 원자적이지만 다음과 같다.

    // Map이 null 키 또는 값을 지원하지 않는 경우(확실히 독립적으로)     만일 (k == 무효의    v == 무효의)             던지다 새로운 널포인터예외();     만일 (m.포함키(k) && 물건들.대등하다(m.얻다(k), v)) {         m.제거하다(k);         돌아오다 진실의;     } 다른        돌아오다 거짓의;     } 

ConcurrentMap 1.8 방법

Map과 ConcurrentMap은 인터페이스이기 때문에 구현을 중단하지 않고서는 새로운 방법을 추가할 수 없다. However, Java 1.8 added the capability for default interface implementations and it added to the Map interface default implementations of some new methods getOrDefault(Object, V), forEach(BiConsumer), replaceAll(BiFunction), computeIfAbsent(K, Function), computeIfPresent(K, BiFunction), compute(K,BiFunction), and merge(K, V, BiFunction). 맵의 기본 구현은 원자성을 보장하지는 않지만, ConcurrentMap 재정의 기본값에서는 원자성을 달성하기 위해 Lock free 기법을 사용하며, 기존의 ConcurrentMap 구현은 자동으로 원자성이 될 것이다. 잠금 없는 기법은 콘크리트 등급의 오버라이드보다 느릴 수 있으므로 콘크리트 등급은 이를 원자적으로 구현하거나 구현하지 않고 동시성 특성을 문서화할 수 있다.

잠금 없는 원자성

충분히 높은 컨센서스 번호, 즉 무한대의 방법을 포함하기 때문에 ConcurrentMaps와 함께 Lock-free 기법을 사용할 수 있다. 이 예는 Java 8 병합()으로 구현될 수 있지만, 전체적인 잠금 없는 패턴을 보여주는데, 이는 보다 일반적이다. 이 예는 ConcurrentMap의 내부용이 아니라 클라이언트 코드의 ConcurrentMap 사용과 관련이 있다. 예를 들어 지도에 있는 값을 원자적으로 상수 C로 곱하려는 경우:

    정태의 최종의 장기의 C = 10;     공허하게 하다 원자 멀티플라이(ConcurrentMap.<, > 지도를 그리다,  핵심을) {         을 위해 (;;) {              oldValue = 지도를 그리다.얻다(핵심을);             // oldValue가 null이 아니라고 가정. 이것은 'payload' 작업이며, 충돌에 대한 재계산 가능성으로 인한 부작용을 가져서는 안 된다.              newValue = oldValue * C;             만일 (지도를 그리다.대체하다(핵심을, oldValue, newValue))                 부숴뜨리다;         }     } 

putIfAbsent(k, v)는 키에 대한 입력이 없을 수 있는 경우에도 유용하다. 이 예는 Java 8 컴퓨팅()으로 구현될 수 있지만, 전반적인 잠금 없는 패턴을 보여주는데, 이는 보다 일반적이다. 교체(k,v1,v2)는 null 매개변수를 수용하지 않으므로 이들 매개변수의 조합이 필요할 때도 있다. 즉, v1이 null이면 putIfAbsent(k, v2)가 호출되고, 그렇지 않으면 replace(k,v1,v2)가 호출된다.

    공허하게 하다 원자성멀티플라이눌러블(ConcurrentMap.<, > 지도를 그리다,  핵심을) {         을 위해 (;;) {              oldValue = 지도를 그리다.얻다(핵심을);             // 이것은 'payload' 작업이며, 충돌에 대한 재계산 가능성으로 인한 부작용은 없어야 한다.              newValue = oldValue == 무효의 ? 이니셜_VALUE : oldValue * C;             만일 (replaceNullable(지도를 그리다, 핵심을, oldValue, newValue))                 부숴뜨리다;         }     }     ...     정태의 부울 replaceNullable(ConcurrentMap.<, > 지도를 그리다,  핵심을,  v1,  v2) {         돌아오다 v1 == 무효의 ? 지도를 그리다.putIfAbsent(핵심을, v2) == 무효의 : 지도를 그리다.대체하다(핵심을, v1, v2);     } 

역사

자바 컬렉션 프레임워크는 주로 Joshua Bloch에 의해 설계되고 개발되었으며, JDK 1.2에 도입되었다.[2] 원래의 동시성 수업은 더그 리아의 수집 패키지에서 나왔다.

참고 항목

참조

  1. ^ "java.util.Collections.synchronizedMap". Java / Java SE / 11 / API / java.base. Oracle Help Center. September 19, 2018. Retrieved 2020-07-17.
  2. ^ Vanhelsuwé, Laurence (January 1, 1999). "The battle of the container frameworks: which should you use?". JavaWorld. Retrieved 2020-07-17.
  3. ^ Lea, Doug. "Overview of package util.concurrent Release 1.3.4". Retrieved 2011-01-01.
  • Goetz, Brian; Joshua Bloch; Joseph Bowbeer; Doug Lea; David Holmes; Tim Peierls (2006). Java Concurrency in Practice. Addison Wesley. ISBN 0-321-34960-1. OL 25208908M.
  • Lea, Doug (1999). Concurrent Programming in Java: Design Principles and Patterns. Addison Wesley. ISBN 0-201-31009-0. OL 55044M.

외부 링크