Lệnh gọi đệ quy ConcurrentHashMap.computeIfAbsent () không bao giờ kết thúc. Lỗi hoặc "tính năng"?


76

Cách đây một thời gian, tôi đã viết blog về một cách hàm Java 8 để tính toán đệ quy số fibonacci , với ConcurrentHashMapbộ nhớ cache và computeIfAbsent()phương pháp mới, hữu ích :

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    static Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        System.out.println(
            "f(" + 8 + ") = " + fibonacci(8));
    }

    static int fibonacci(int i) {
        if (i == 0)
            return i;

        if (i == 1)
            return 1;

        return cache.computeIfAbsent(i, (key) -> {
            System.out.println(
                "Slow calculation of " + key);

            return fibonacci(i - 2) + fibonacci(i - 1);
        });
    }
}

Tôi chọn ConcurrentHashMapvì tôi đang nghĩ đến việc làm cho ví dụ này tinh vi hơn nữa bằng cách giới thiệu song song (cuối cùng thì tôi không làm như vậy).

Bây giờ, hãy tăng số lượng từ 8lên 25và quan sát điều gì sẽ xảy ra:

        System.out.println(
            "f(" + 25 + ") = " + fibonacci(25));

Chương trình không bao giờ tạm dừng. Bên trong phương thức, có một vòng lặp chạy mãi mãi:

for (Node<K,V>[] tab = table;;) {
    // ...
}

Tôi đang sử dụng:

C:\Users\Lukas>java -version
java version "1.8.0_40-ea"
Java(TM) SE Runtime Environment (build 1.8.0_40-ea-b23)
Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, mixed mode)

Matthias, một độc giả của bài đăng trên blog đó cũng xác nhận vấn đề (anh ấy thực sự đã tìm thấy nó) .

Thật là kỳ lạ. Tôi đã mong đợi bất kỳ điều nào trong số hai điều sau:

  • Nó hoạt động
  • Nó ném một ConcurrentModificationException

Nhưng chỉ cần không bao giờ dừng lại? Điều đó có vẻ nguy hiểm. Nó là một lỗi? Hay tôi đã hiểu nhầm hợp đồng nào đó?

Câu trả lời:


58

Điều này đã được sửa trong JDK-8062841 .

Trong đề xuất năm 2011 , tôi đã xác định vấn đề này trong quá trình xem xét mã. JavaDoc đã được cập nhật và một bản sửa lỗi tạm thời đã được thêm vào. Nó đã bị xóa trong một lần viết lại tiếp theo do các vấn đề về hiệu suất.

Trong cuộc thảo luận năm 2014 , chúng tôi đã khám phá những cách để phát hiện và thất bại tốt hơn. Lưu ý rằng một số cuộc thảo luận đã được chuyển sang email riêng tư ngoại tuyến để xem xét các thay đổi cấp thấp. Mặc dù không phải mọi trường hợp đều có thể được bảo hiểm, nhưng các trường hợp thông thường sẽ không sống động. Các bản sửa lỗi này nằm trong kho lưu trữ của Doug nhưng chưa được đưa vào bản phát hành JDK.


5
Rất thú vị, cảm ơn vì đã chia sẻ! Báo cáo của tôi cũng được chấp nhận là JDK-8074374
Lukas Eder

1
@Ben xin lỗi vì offtop, nhưng tôi thực sự ngạc nhiên về mức độ xếp hạng SO thấp của bạn mặc dù có đóng góp đáng kể trong cơ sở kiến ​​thức liên quan đến thứ phức tạp như đồng thời không khóa (và gần không khóa).
Alex Salauyou

@SashaSalauyou Cảm ơn. Tôi thường đưa ra câu trả lời nhanh trong các bình luận thay vì dành thời gian cho một câu trả lời dài hơn, đầy đủ. Tuy nhiên, điểm bình luận không làm tăng danh tiếng. Tuy nhiên, tôi có lẽ ít quan tâm hơn đến việc theo đuổi đại diện.
Ben Manes

@Ben không hoàn toàn cố định: boom
Scott

@ScottMcKinney bắt thật hay! Có vẻ như khẳng định tương tự cũng có thể được áp dụng transfervà đó là một trường hợp bị bỏ sót. Bạn có thể gửi email concurrency-interest@cs.oswego.eduđể thu hút sự chú ý của Doug?
Ben Manes

56

Đây tất nhiên là một "tính năng" . Các ConcurrentHashMap.computeIfAbsent()Javadoc đọc:

Nếu khóa được chỉ định chưa được liên kết với một giá trị, hãy cố gắng tính toán giá trị của nó bằng cách sử dụng hàm ánh xạ đã cho và nhập nó vào bản đồ này trừ khi rỗng. Toàn bộ lệnh gọi phương thức được thực hiện nguyên tử, vì vậy hàm được áp dụng nhiều nhất một lần cho mỗi khóa. Một số hoạt động cố gắng cập nhật trên bản đồ này bởi các chuỗi khác có thể bị chặn khi đang tính toán, vì vậy việc tính toán phải ngắn gọn và đơn giản và không được cố gắng cập nhật bất kỳ ánh xạ nào khác của bản đồ này .

Từ "không được phép" là một hợp đồng rõ ràng, mà thuật toán của tôi đã vi phạm, mặc dù không phải vì những lý do tương tự.

Điều thú vị là không có ConcurrentModificationException. Thay vào đó, chương trình không bao giờ bị tạm dừng - theo ý kiến ​​của tôi vẫn là một lỗi khá nguy hiểm (tức là vòng lặp vô hạn. Hoặc: bất cứ điều gì có thể xảy ra sai sót ).

Ghi chú:

Các HashMap.computeIfAbsent()hoặc Map.computeIfAbsent()Javadoc không cấm tính toán đệ quy như vậy, đó là tất nhiên vô lý như loại bộ nhớ cache là Map<Integer, Integer>, không phải ConcurrentHashMap<Integer, Integer>. Sẽ rất nguy hiểm cho các kiểu phụ xác định lại một cách quyết liệt các hợp đồng siêu kiểu ( Setso với SortedSetlà lời chào). Do đó, nó cũng bị cấm trong các loại siêu, thực hiện đệ quy như vậy.


3
Tốt tìm thấy. Tôi muốn đề xuất một báo cáo lỗi / RFE chống lại JDK.
Axel

4
Xong, xem có được chấp nhận không ... Mình sẽ cập nhật kèm theo link nếu được.
Lukas Eder

3
Đối với tôi, có vẻ như kiểu sửa đổi đệ quy của các ánh xạ khác không được phép thực hiện ConcurrentHashMapvì phần quan trọng khác của hợp đồng của nó: " Toàn bộ lệnh gọi phương thức được thực hiện theo nguyên tử, vì vậy hàm được áp dụng nhiều nhất một lần cho mỗi khóa. " có khả năng là chương trình của bạn, bằng cách vi phạm hợp đồng "không sửa đổi đệ quy", đang cố gắng lấy một khóa mà nó đã giữ và đang bị khóa với chính nó, không chạy trong một vòng lặp vô hạn.
Murgatroid99

3
Từ JavaDoc:IllegalStateException - if the computation detectably attempts a recursive update to this map that would otherwise never complete
Ben Manes

2
@LukasEder ngay cả với HashMapnó không ổn. bug.openjdk.java.net/browse/JDK-8172951
Piotr Findeisen

4

Điều này rất giống với lỗi. Bởi vì, nếu bạn tạo cache với dung lượng 32 thì chương trình của bạn sẽ hoạt động đến 49. Và điều thú vị là, tham số sizeCtl = 32 + (32 >>> 1) + 1) = 49! Có thể là lý do trong việc thay đổi kích thước?


1
Tôi không có thời gian để tìm hiểu mã nguồn, nhưng tôi nghĩ rằng bạn đã đúng. Chúng tôi liên tục đánh vào vấn đề này trong sản xuất. Ngay sau khi chúng tôi tăng CHMcông suất ban đầu lên một con số rất cao, điều này đã biến mất. Cho đến khi chúng tôi có thể cấu trúc lại mã của mình, đây là những gì chúng tôi sẽ làm ...
Eugene
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.