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

최근에 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을 얻습니다.

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

도와 주셔서 감사합니다



답변

저는 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 개발자들에 의해 볼 도착합니다.


답변

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

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, 등에 유사한 접근 방식을 사용할 수 있습니다 .


답변

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

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


답변

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

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

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


답변