jdk1.6 이상에서 HashMaps가 multi = threading에 문제를 일으킨다는 점을 감안할 때 코드를 어떻게 수정해야합니까?


83

최근에 stackoverflow에서 질문을 제기 한 다음 답을 찾았습니다. 초기 질문은 뮤텍스 또는 가비지 수집 이외의 메커니즘이 다중 스레드 Java 프로그램을 느리게 할 수 있다는 것이 었습니다.

나는 HashMap이 JDK1.6과 JDK1.7 사이에서 수정되었다는 것을 공포에 질렀습니다. 이제 HashMap을 만드는 모든 스레드가 동기화되도록하는 코드 블록이 있습니다.

JDK1.7.0_10의 코드 줄은 다음과 같습니다.

 /**A randomizing value associated with this instance that is applied to hash code of  keys to make hash collisions harder to find.     */
transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);

결국 전화

 protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
 }    

다른 JDK를 살펴보면 JDK1.5.0_22 또는 JDK1.6.0_26에 존재하지 않습니다.

내 코드에 미치는 영향은 엄청납니다. 64 스레드에서 실행할 때 1 스레드에서 실행할 때보 다 성능이 떨어집니다. JStack은 대부분의 스레드가 Random에서 해당 루프에서 회전하는 데 대부분의 시간을 소비하고 있음을 보여줍니다.

그래서 몇 가지 옵션이있는 것 같습니다.

  • HashMap을 사용하지 않고 비슷한 것을 사용하도록 코드를 다시 작성하십시오.
  • 어떻게 든 rt.jar을 엉망으로 만들고 그 안의 해시 맵을 교체하십시오.
  • 어떻게 든 클래스 경로가 혼란 스럽기 때문에 각 스레드는 자체 버전의 HashMap을 얻습니다.

이러한 경로를 시작하기 전에 (모두 시간이 많이 걸리고 잠재적으로 큰 영향을 미치는 것처럼 보임) 분명한 트릭을 놓쳤는 지 궁금했습니다. 스택 오버플로 사람들 중 누구든지 더 나은 경로를 제안하거나 새로운 아이디어를 식별 할 수 있습니까?

도와 주셔서 감사합니다


2
그렇게 많은 해시 맵을 생성하려면 무엇이 필요합니까? 무엇을하려고합니까?
fge

3
2 개의 코멘트 : 1. ConcurrentHashMap은 그것을 사용하지 않는 것 같습니다-대안이 될 수 있습니까? 2.이 코드는 맵 생성시에만 호출됩니다. 이는 높은 경합 하에서 수백만 개의 해시 맵을 생성하고 있음을 의미합니다. 실제로 실제 프로덕션 부하를 반영합니까?
assylias

1
실제로 ConcurrentHashMap은 (oracle jdk 1.7_10에서) 그 방법을 사용하지만 분명히 openJDK 7은 그렇지 않습니다 .
assylias

1
@assylias 여기 에서 최신 버전을 확인해야 합니다 . 이것은 그러한 코드 라인을 자랑합니다.
Marko Topolnik

3
@StaveEscura는 AtomicLong잘 작동하기 위해 낮은 쓰기 경합에 베팅합니다. 쓰기 경합이 높으므로 정기적 인 배타적 잠금이 필요합니다. 동기화 된 HashMap팩토리를 작성하면 이 스레드에서 수행하는 모든 작업 이 맵 인스턴스화가 아니라면 개선 된 것을 볼 수 있습니다 .
Marko Topolnik 2012

답변:


56

저는 7u6, CR # 7118743 : Alternative Hashing for String with Hash-based Maps‌에 등장한 패치의 원저자입니다.

hashSeed의 초기화가 병목 현상이라는 사실을 바로 인정하겠습니다.하지만 Hash Map 인스턴스 당 한 번만 발생하기 때문에 문제가 될 것으로 예상 한 문제는 아닙니다. 이 코드가 병목 현상이 되려면 초당 수백 또는 수천 개의 해시 맵을 만들어야합니다. 이것은 확실히 전형적인 것이 아닙니다. 거기에 정말 응용 프로그램이이 일을 할 수있는 타당한 이유? 이 해시 맵은 얼마나 오래 유지됩니까?

어쨌든 우리는 Random이 아닌 ThreadLocalRandom으로의 전환과 cambecc가 제안한 지연 초기화의 변형을 조사 할 것입니다.

3 편집

병목 현상에 대한 수정 사항이 JDK7 업데이트 mercurial repo로 푸시되었습니다.

http://hg.openjdk.java.net/jdk7u/jdk7u-dev/jdk/rev/b03bbdef3a88

이 수정 사항은 곧 출시 될 7u40 릴리스의 일부이며 IcedTea 2.4 릴리스에서 이미 사용 가능합니다.

7u40의 최종 테스트 빌드는 여기에서 사용할 수 있습니다.

https://jdk7.java.net/download.html

피드백은 여전히 ​​환영합니다. 에 보내기 http://mail.openjdk.java.net/mailman/listinfo/core-libs-dev 확인이 오픈 JDK 개발자들에 의해 볼 도착합니다.


1
조사해 주셔서 감사합니다. 네, 정말 많은지도를 만들어야 할 필요가 있습니다. 응용 프로그램은 실제로 매우 간단하지만 10 만 명의 사람들이 1 초에 그것을 칠 수 있으며 이는 수백만 개의지도를 매우 빠르게 만들 수 있음을 의미합니다. 물론 맵을 사용하지 않도록 다시 작성할 수 있지만 개발 비용이 매우 많이 듭니다. 지금은 랜덤 필드를 해킹 반사를 사용하는 계획은 좋아 보인다
지팡이 Escura

2
Mike, 단기 수정을위한 제안 : ThreadLocalRandom (스레드 로컬 저장소를 엉망으로 만드는 응용 프로그램에 고유 한 문제가 있음)을 제외하면 (시간, 위험 및 테스트 측면에서) 훨씬 쉽고 저렴하지 않을 것입니다. Hashing.Holder.SEED_MAKER를 (예를 들어) <num cores> 임의 인스턴스의 배열로 스트라이프하고 호출 스레드의 ID를 사용하여 %-인덱싱 하시겠습니까? 이는 눈에 띄는 부작용없이 스레드 당 경합을 즉시 완화 (제거하지는 않음)합니다.
Holger Hoffstätte 2013 년

10
요청 속도가 높고 JSON을 사용하는 @mduigou 웹 응용 프로그램은 대부분의 JSON 라이브러리가 HashMaps 또는 LinkedHashMaps를 사용하여 JSON 개체를 역 직렬화하기 때문에 초당 많은 수의 HashMap을 생성합니다. JSON을 사용하는 웹 응용 프로그램은 널리 퍼져 있으며 HashMaps 생성은 응용 프로그램 (그러나 라이브러리 응용 프로그램 사용)에 의해 제어되지 않을 수 있으므로 HashMaps를 생성 할 때 병목 현상이 발생하지 않는 타당한 이유가 있다고 말하고 싶습니다.
sbordet 2013 년

3
@mduigou 아마도 간단한 완화는 CAS를 호출하기 전에 oldSeed가 동일한 지 확인하는 것입니다. 이 최적화 (테스트 테스트 및 설정 또는 TTAS라고 함)는 중복 된 것처럼 보일 수 있지만 CAS가 실패 할 것임을 이미 알고있는 경우 시도하지 않기 때문에 경합시 중요한 성능 영향을 미칠 수 있습니다. 실패한 CAS는 캐시 라인의 MESI 상태를 Invalid로 설정하는 불행한 부작용이 있습니다. 모든 당사자가 메모리에서 값을 다시 검색해야합니다. 물론 Holger의 시드 스트라이핑은 훌륭한 장기 수정이지만 TTAS 최적화를 사용해야합니다.
Jed Wesley-Smith

5
"수백 또는 수천"대신 "수십만"을 의미합니까? -큰 차이
Michael Neale 2013 년

30

이것은 해결할 수있는 "버그"처럼 보입니다. 새로운 "대체 해싱"기능을 비활성화하는 속성이 있습니다.

jdk.map.althashing.threshold = -1

그러나 대체 해싱을 비활성화하는 것은 무작위 해시 시드 생성을 끄지 않기 때문에 충분하지 않습니다 (실제로 그래야 함). 따라서 대체 해싱을 끄더라도 해시 맵 인스턴스화 중에 스레드 경합이 계속 발생합니다.

이 문제를 해결하는 특히 불쾌한 방법 중 하나는 Random해시 시드 생성 에 사용 된 인스턴스를 자신의 동기화되지 않은 버전 으로 강제로 대체하는 것입니다 .

// Create an instance of "Random" having no thread synchronization.
Random alwaysOne = new Random() {
    @Override
    protected int next(int bits) {
        return 1;
    }
};

// Get a handle to the static final field sun.misc.Hashing.Holder.SEED_MAKER
Class<?> clazz = Class.forName("sun.misc.Hashing$Holder");
Field field = clazz.getDeclaredField("SEED_MAKER");
field.setAccessible(true);

// Convince Java the field is not final.
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// Set our custom instance of Random into the field.
field.set(null, alwaysOne);

이 작업을 수행하는 것이 (아마도) 안전한 이유는 무엇입니까? 대체 해싱이 비활성화 되었기 때문에 임의의 해시 시드가 무시됩니다. 따라서의 인스턴스 Random가 실제로 무작위가 아닌 것은 중요하지 않습니다 . 이와 같이 불쾌한 해킹은 항상 그렇듯이주의해서 사용하십시오.

( 정적 최종 필드를 설정하는 코드에 대해 https://stackoverflow.com/a/3301720/1899721 에 감사드립니다 ).

--- 편집하다 ---

FWIW에서 다음과 같이 변경 HashMap하면 alt 해싱이 비활성화 된 경우 스레드 경합이 제거됩니다.

-   transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
+   transient final int hashSeed;

...

         useAltHashing = sun.misc.VM.isBooted() &&
                 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
+        hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0;
         init();

ConcurrentHashMap, 등에 유사한 접근 방식을 사용할 수 있습니다 .


1
감사합니다. 이것은 실제로 해킹이지만 일시적으로 문제를 해결합니다. 위에서 확인한 목록에있는 어떤 것보다 확실히 더 나은 솔루션입니다. 장기적으로는 어쨌든 더 빠른 HashMap으로 무언가를해야 할 것입니다. 이것은 이전 ResourceBundle 캐시에 대한 해결책이 지울 수 없다는 것을 상기시킵니다. 코드는 거의 동일합니다!
Stave Escura 2012

1
참고로이 대체 해싱 기능은 여기에 설명되어 있습니다. 검토 요청 CR # 7118743 : 해시 기반 맵을 사용하는 문자열에 대한 대체 해싱 . murmur3 해시 함수의 구현입니다.
cambecc

3

빅 데이터 애플리케이션에서 레코드 당 임시 HashMap을 생성하는 앱이 많이 있습니다. 예를 들어이 파서 및 직렬 변환기입니다. 동기화되지 않은 컬렉션 클래스에 동기화를 배치하는 것은 정말 어려운 일입니다. 제 생각에는 이것은 용납 할 수 없으며 최대한 빨리 수정해야합니다. 7u6, CR # 7118743에 분명히 도입 된 변경 사항은 동기화 또는 원자 적 작업없이 되돌 리거나 수정해야합니다.

어떻게 든 이것은 JDK 1.1 / 1.2에서 StringBuffer와 Vector 및 HashTable을 동기화하는 엄청난 실수를 상기시킵니다. 사람들은 그 실수에 대해 수년 동안 값진 지불을했습니다. 그 경험을 반복 할 필요가 없습니다.


2

사용 패턴이 합리적이라고 가정하면 자체 버전의 Hashmap을 사용하고 싶을 것입니다.

이 코드는 해시 충돌을 유발하기 훨씬 더 어렵게 만들어 공격자가 성능 문제 ( 세부 사항 ) 를 생성하지 못하도록합니다. 이 문제가 이미 다른 방식으로 처리되었다고 가정하면 동기화가 전혀 필요하지 않다고 생각합니다. 그러나 동기화를 사용하는지 여부와 관계없이 JDK가 제공하는 것에 그다지 의존하지 않도록 자신의 Hashmap 버전을 사용하고 싶을 것입니다.

따라서 일반적으로 비슷한 것을 작성하고 그것을 가리 키거나 JDK에서 클래스를 재정의합니다. 후자를 수행하려면 -Xbootclasspath/p:매개 변수로 부트 스트랩 클래스 경로를 재정의 할 수 있습니다 . 그러나 그렇게하면 "Java 2 Runtime Environment 바이너리 코드 라이센스에 위배됩니다"( 소스 ).


아하. 나는 그것이 최적화의 요점이라는 것을 깨닫지 못했습니다. 매우 영리한. 공격자에 대한 저의 위협 모델은 해시 맵을 이런 식으로 엉망으로 만들지 않지만 미래를 위해 이것을 기억할 것입니다. 결국 HashMap을 교체하는 것에 대한 귀하의 요점에 동의합니다. 나는 아마도 팩토리 객체 나 IOC 컨테이너를 그것을 만드는 모든 클래스에 스레딩해야 할 것입니다. 저는 Cambecc이 제공 한 대답이 저를 홀에서 빠져 나갈 수있을 것이라고 생각합니다. 저는 장기적인 솔루션을 위해 작업하는 동안
Stave Escura
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.