Do HashMaps trong jdk1.6 trở lên gây ra sự cố với multi = threading, tôi nên sửa mã của mình như thế nào


83

Gần đây tôi đã đưa ra một câu hỏi trong stackoverflow, sau đó tìm thấy câu trả lời. Câu hỏi ban đầu là Cơ chế nào ngoài mutexs hoặc thu gom rác có thể làm chậm chương trình java đa luồng của tôi?

Tôi kinh hoàng phát hiện ra rằng HashMap đã được sửa đổi giữa JDK1.6 và JDK1.7. Bây giờ nó có một khối mã khiến tất cả các luồng tạo HashMaps đồng bộ hóa.

Dòng mã trong JDK1.7.0_10 là

 /**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);

Kết thúc cuộc gọi

 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));
 }    

Tìm kiếm trong các JDK khác, tôi thấy điều này không có trong JDK1.5.0_22 hoặc JDK1.6.0_26.

Tác động đến mã của tôi là rất lớn. Nó làm cho nó để khi tôi chạy trên 64 luồng, tôi nhận được hiệu suất kém hơn so với khi tôi chạy trên 1 luồng. Một JStack cho thấy rằng hầu hết các chủ đề đang dành phần lớn thời gian để quay trong vòng lặp đó ở chế độ Ngẫu nhiên.

Vì vậy, tôi dường như có một số lựa chọn:

  • Viết lại mã của tôi để tôi không sử dụng HashMap mà sử dụng thứ gì đó tương tự
  • Bằng cách nào đó, hãy làm lộn xộn với rt.jar và thay thế bản đồ băm bên trong nó
  • Lộn xộn với đường dẫn lớp bằng cách nào đó, vì vậy mỗi luồng có phiên bản HashMap của riêng mình

Trước khi bắt đầu bất kỳ con đường nào trong số này (tất cả đều trông rất tốn thời gian và có khả năng ảnh hưởng cao), tôi tự hỏi liệu mình có bỏ lỡ một mẹo rõ ràng nào không. Mọi người trong số các bạn có thể đề xuất con đường nào tốt hơn không, hoặc có thể xác định một ý tưởng mới.

Cảm ơn đã giúp đỡ


2
Điều gì đòi hỏi bạn phải tạo nhiều bản đồ băm như vậy? Bạn đang cố làm gì vậy?
fge 23/12/12

3
2 nhận xét: 1. ConcurrentHashMap dường như không sử dụng nó - nó có thể là một sự thay thế? 2. Đoạn mã này chỉ được gọi khi tạo bản đồ. Điều đó ngụ ý rằng bạn đang tạo ra hàng triệu bản đồ băm với sự tranh cãi cao - Điều đó có thực sự phản ánh tải sản xuất thực tế không?
assylias 23/12/12

1
Trên thực tế ConcurrentHashMap cũng sử dụng phương pháp đó (trong oracle jdk 1.7_10) - nhưng rõ ràng openJDK 7 thì không .
assylias 23/12/12

1
@assylias Bạn nên kiểm tra phiên bản mới nhất tại đây . Cái này có một dòng mã như vậy.
Marko Topolnik 23/12/12

3
@StaveEscura AtomicLongđặt cược vào khả năng ghi thấp để hoạt động tốt. Bạn có khả năng tranh chấp cao, vì vậy bạn cần thường xuyên khóa độc quyền. Viết một HashMapnhà máy được đồng bộ hóa và bạn có thể sẽ thấy một sự cải tiến, trừ khi tất cả những gì bạn từng làm trong các chuỗi này là tạo bản đồ.
Marko Topolnik 23/12/12

Câu trả lời:


56

Tôi là tác giả ban đầu của bản vá xuất hiện trong 7u6, CR # 7118743: Thay thế băm cho chuỗi với Bản đồ dựa trên băm‌.

Tôi sẽ thừa nhận ngay trước rằng quá trình khởi tạo hashSeed là một nút thắt cổ chai nhưng nó không phải là một vấn đề mà chúng tôi mong đợi vì nó chỉ xảy ra một lần cho mỗi phiên bản Hash Map. Để mã này trở thành nút cổ chai, bạn sẽ phải tạo hàng trăm hoặc hàng nghìn bản đồ băm mỗi giây. Điều này chắc chắn không phải là điển hình. Có thực sự là một lý do chính đáng cho các ứng dụng của bạn để được làm điều này? Các bản đồ băm này tồn tại trong bao lâu?

Bất kể, chúng tôi có thể sẽ điều tra việc chuyển sang ThreadLocalRandom thay vì Ngẫu nhiên và có thể là một số biến thể của khởi tạo lười biếng như được đề xuất bởi cambecc.

CHỈNH SỬA 3

Một bản sửa lỗi cho nút cổ chai đã được đẩy lên repo thương mại của bản cập nhật JDK7:

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

Bản sửa lỗi sẽ là một phần của bản phát hành 7u40 sắp tới và đã có sẵn trong các bản phát hành IcedTea 2.4.

Các bản dựng thử nghiệm gần cuối cùng của 7u40 có sẵn tại đây:

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

Phản hồi vẫn được hoan nghênh. Gửi nó đến http://mail.openjdk.java.net/mailman/listinfo/core-libs-dev để chắc chắn rằng nó sẽ được các nhà phát triển openJDK nhìn thấy.


1
Cảm ơn đã xem xét này. Vâng, thực sự cần thiết để tạo ra nhiều bản đồ: ứng dụng này thực sự khá đơn giản, nhưng 100.000 người có thể sử dụng nó trong một giây, và điều đó có nghĩa là hàng triệu bản đồ có thể được tạo rất nhanh chóng. Tất nhiên tôi có thể viết lại nó để không sử dụng bản đồ, nhưng đó là chi phí phát triển rất cao. Còn bây giờ kế hoạch của việc sử dụng phản ánh hack lĩnh vực ngẫu nhiên có vẻ tốt
Stave Escura

2
Mike, một gợi ý cho một giải pháp ngắn hạn: ngoài ThreadLocalRandom (sẽ có vấn đề riêng với các ứng dụng gây rối với lưu trữ cục bộ luồng) sẽ không dễ dàng và rẻ hơn nhiều (về thời gian, rủi ro và thử nghiệm) sọc Hashing.Holder.SEED_MAKER thành một mảng (giả sử) <num core> Phiên bản ngẫu nhiên và sử dụng id của luồng gọi đến% -index vào đó? Điều này sẽ ngay lập tức làm giảm (mặc dù không loại bỏ) tranh chấp trên mỗi chủ đề mà không có bất kỳ tác dụng phụ đáng chú ý nào.
Holger Hoffstätte

10
@mduigou Các ứng dụng web có tỷ lệ yêu cầu cao và sử dụng JSON sẽ tạo ra một số lượng lớn các HashMap mỗi giây, vì hầu hết nếu không phải tất cả các thư viện JSON đều sử dụng HashMaps hoặc LinkedHashMaps để giải mã hóa các đối tượng JSON. Các ứng dụng web sử dụng JSON phổ biến và việc tạo HashMap có thể không được kiểm soát bởi ứng dụng (mà do ứng dụng thư viện sử dụng), vì vậy tôi muốn nói rằng có những lý do hợp lệ để không bị tắc nghẽn khi tạo HashMap.
sbordet

3
@mduigou có lẽ một cách giải quyết đơn giản là kiểm tra xem Hạt giống cũ có giống nhau không trước khi gọi CAS lên đó. Việc tối ưu hóa này (được gọi là kiểm tra-kiểm tra và tập hợp hoặc TTAS) có vẻ thừa, nhưng có thể có tác động hiệu suất quan trọng đang được tranh cãi vì CAS không được thử nếu nó đã biết nó sẽ thất bại. CAS không thành công có tác dụng phụ đáng tiếc là đặt trạng thái MESI của dòng bộ nhớ cache thành Không hợp lệ - yêu cầu tất cả các bên truy xuất lại giá trị từ bộ nhớ. Tất nhiên, việc tách hạt giống của Holger là một giải pháp lâu dài tuyệt vời, nhưng ngay cả khi đó, việc tối ưu hóa TTAS cũng nên được sử dụng.
Jed Wesley-Smith

5
Bạn có nghĩa là "hàng trăm nghìn" thay vì "hàng trăm hoặc hàng nghìn"? - Sự khác biệt LỚN
Michael Neale

30

Điều này giống như một "lỗi" mà bạn có thể khắc phục. Có một thuộc tính vô hiệu hóa tính năng "băm thay thế" mới:

jdk.map.althashing.threshold = -1

Tuy nhiên, vô hiệu hóa hàm băm thay thế là không đủ vì nó không tắt việc tạo hạt băm ngẫu nhiên (mặc dù thực sự nên làm như vậy). Vì vậy, ngay cả khi bạn tắt chức năng băm thay thế, bạn vẫn có sự tranh cãi về chủ đề trong quá trình khởi tạo bản đồ băm.

Một cách đặc biệt khó chịu để giải quyết vấn đề này là thay thế mạnh mẽ phiên bản Randomđược sử dụng để tạo hạt giống băm bằng phiên bản không đồng bộ của riêng bạn:

// 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);

Tại sao nó (có thể) an toàn để làm điều này? Bởi vì băm thay thế đã bị vô hiệu hóa, khiến cho các hạt băm ngẫu nhiên bị bỏ qua. Vì vậy, không có vấn đề gì rằng trường hợp của chúng ta Randomtrên thực tế không phải là ngẫu nhiên. Như mọi khi với những vụ hack khó chịu như thế này, hãy thận trọng khi sử dụng.

(Cảm ơn https://stackoverflow.com/a/3301720/1899721 về mã đặt các trường cuối cùng tĩnh).

--- Biên tập ---

FWIW, thay đổi sau đây HashMapsẽ loại bỏ tranh chấp chuỗi khi chức năng băm thay thế bị tắt:

-   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();

Một cách tiếp cận tương tự có thể được sử dụng cho ConcurrentHashMap, v.v.


1
Cảm ơn bạn. Đây thực sự là một hack, nhưng nó giải quyết vấn đề tạm thời. Nó chắc chắn là một giải pháp tốt hơn bất kỳ giải pháp nào trong danh sách mà tôi đã xác định ở trên. Về lâu dài, tôi sẽ phải làm gì đó với HashMap nhanh hơn. Điều này nhắc tôi về giải pháp cho bộ nhớ cache ResourceBundle cũ không thể xóa được. Mã gần như giống hệt nhau!
Stave Escura 23/12/12

1
FYI, tính năng băm thay thế này được mô tả ở đây: Yêu cầu đánh giá CR # 7118743: Băm thay thế cho chuỗi với Bản đồ dựa trên băm . Nó là một triển khai của hàm băm murmur3.
cambecc 24/12/12

3

Có rất nhiều ứng dụng tạo HashMap tạm thời trên mỗi bản ghi trong các ứng dụng dữ liệu lớn. Ví dụ: trình phân tích cú pháp và trình tuần tự này. Đưa bất kỳ đồng bộ hóa nào vào các lớp bộ sưu tập không đồng bộ là một việc làm thực sự. Theo tôi, điều này là không thể chấp nhận được và cần phải được sửa càng sớm càng tốt. Thay đổi dường như đã được giới thiệu trong 7u6, CR # 7118743 sẽ được hoàn nguyên hoặc sửa chữa mà không yêu cầu bất kỳ hoạt động đồng bộ hóa hoặc nguyên tử nào.

Bằng cách nào đó, điều này nhắc nhở tôi về sai lầm lớn khi làm cho StringBuffer và Vector và HashTable được đồng bộ hóa trong JDK 1.1 / 1.2. Mọi người đã phải trả giá đắt trong nhiều năm cho sai lầm đó. Không cần phải lặp lại trải nghiệm đó.


2

Giả sử kiểu sử dụng của bạn là hợp lý, bạn sẽ muốn sử dụng phiên bản Hashmap của riêng mình.

Đoạn mã đó ở đó để làm cho va chạm băm khó gây ra hơn rất nhiều, ngăn những kẻ tấn công tạo ra các vấn đề về hiệu suất ( chi tiết ) - giả sử vấn đề này đã được xử lý theo một cách nào đó, tôi không nghĩ rằng bạn cần đồng bộ hóa chút nào. Tuy nhiên, không liên quan đến việc bạn có sử dụng đồng bộ hóa hay không, có vẻ như bạn sẽ muốn sử dụng phiên bản Hashmap của riêng mình để bạn không làm mất quá nhiều điều mà JDK sẽ cung cấp.

Vì vậy, bạn chỉ cần viết một cái gì đó tương tự và trỏ đến đó, hoặc ghi đè một lớp trong JDK. Để làm điều sau, bạn có thể ghi đè đường dẫn bootstrap bằng -Xbootclasspath/p:tham số. Tuy nhiên, làm như vậy sẽ "trái với giấy phép mã nhị phân Java 2 Runtime Environment" ( nguồn ).


Aha. Tôi đã không nhận ra đó là điểm của việc tối ưu hóa. Rất thông minh. Mô hình mối đe dọa của tôi dành cho những kẻ tấn công không khiến chúng rối tung với các bản đồ băm theo cách này, nhưng tôi sẽ ghi nhớ điều này cho tương lai. Tôi đồng ý với quan điểm của bạn về việc thay thế HashMap cuối cùng. Tôi có thể sẽ phải luồng một đối tượng factory hoặc có thể là một vùng chứa IOC vào mọi lớp tạo ra chúng. Tôi nghĩ câu trả lời được đưa ra bởi Cambecc sẽ làm cho tôi ra khỏi lỗ, trong khi tôi làm việc trên một giải pháp dài hạn
Stave Escura
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.