2 poin oleh GN⁺ 2025-03-01 | 1 komentar | Bagikan ke WhatsApp
  • Pada aplikasi Java 17, seluruh 32 core tersaturasi, membuat penggunaan CPU melonjak hingga 3.200%; pengurutan thread dump berdasarkan CPU time menunjukkan beberapa thread tertahan di TreeMap.put()
  • Kode loop yang awalnya dicurigai memang tidak efisien, tetapi kompleksitas O(N lg(M)) dan validasi ukuran input saja tidak cukup menjelaskan waktu eksekusi pada level gangguan layanan
  • Penyebab sebenarnya adalah beberapa thread memodifikasi TreeMap bersama tanpa proteksi, sehingga muncul cycle di dalam red-black tree dan operasi pencarian/penyisipan masuk ke loop tak berujung
  • Fenomena yang sama muncul pada kode reproduksi sederhana, ExecutorService, dan layanan gRPC; thread pool tidak menampilkan NPE ke standard output, sehingga akar masalah makin sulit ditemukan
  • Perbaikannya tidak cukup hanya dengan beralih ke Collections.synchronizedMap atau ConcurrentHashMap; perlu juga penanganan exception di executor, alert CPU/NPE, analisis statis, dan pengujian multithread

Gejala gangguan dan petunjuk pertama

  • Mesin rusak parah sampai hampir sulit diakses lewat ssh, dan penggunaan CPU naik hingga 3.200%
    • Seluruh 32 core pada host sedang digunakan
    • Ini kontras dengan bug sebelumnya yang hanya menggunakan 1 core pada 100%
  • Thread dump dari runtime Java 17 menyertakan CPU time, dan pengurutan berdasarkan CPU time memperlihatkan thread-thread yang mirip
    • Stack mengarah ke java.util.TreeMap.put()
    • Lokasi kode aplikasi adalah BusinessLogic.someFunction(BusinessLogic.java:29)

Kode yang awalnya dicurigai dan hipotesis yang dieliminasi

  • Kode di titik masalah melakukan iterasi atas unrelatedObjects, tetapi di dalam body loop hanya menggunakan relatedObject
for (SomeOtherType unrelatedObject : unrelatedObjects) {
    treeMap.put(relatedObject.a(), relatedObject.b()); // line 29
}
  • Kode ini bisa disederhanakan menjadi satu kali put seperti berikut
treeMap.put(relatedObject.a(), relatedObject.b());
  • Karena unrelatedObjects digunakan di bagian akhir fungsi, parameter itu sendiri tidak bisa dihapus
  • Ada kemungkinan penggunaan unrelatedObject hilang selama proses refactoring
  • Pada unit test, masalah tidak dapat direproduksi meski treeMap dan unrelatedObjects masing-masing dibesarkan hingga 1.000.000 entri
    • Jika ukuran unrelatedObjects adalah N dan ukuran treeMap adalah M, kompleksitasnya O(N lg(M))
    • Untuk melihat waktu eksekusi sekitar 1 menit, diperkirakan perlu ukuran 100 juta hingga 1 miliar entri
    • Ini juga tidak cocok dengan asumsi bahwa di aplikasi nyata kedua struktur data tersebut tidak melebihi 1.000 entri

Loop tak berujung yang dibuat TreeMap tanpa proteksi

  • Definisi field TreeMap adalah sebagai berikut
private final Map<K,V> treeMap = new TreeMap<>();
  • Beberapa thread mengakses TreeMap ini, tetapi tidak ada sinkronisasi atau mekanisme proteksi
  • TreeMap di Java diimplementasikan sebagai red-black tree, dan jika koneksi node internal rusak akibat modifikasi serentak, cycle dapat terbentuk
  • Saat mencari atau memasukkan nilai yang belum ada, traversal dapat mengikuti cycle tersebut dan masuk ke loop tak berujung

Eksperimen reproduksi sederhana

  • Ditulis eksperimen di mana beberapa thread memperbarui TreeMap bersama secara acak
for (int i = 0; i < numThreads; i++) {
    threads.add(new Thread(() -> {
        Random random = new Random();
        for(int j = 0; j < numUpdates; j++) {
            try {
                treeMap.put(random.nextInt(1000), random.nextInt(1000));
            } catch (NullPointerException e) {
                // let it keep going so we can reproduce the issue.
            }
        }
    }));
}
  • Proyek lengkapnya ada di SimpleRepro.java
  • Awalnya try/catch tampak seperti bagian kunci
    • Tanpa try/catch, sebagian thread mati karena NullPointerException dan program berhenti
    • Setelah menambahkan try/catch dan menjalankannya beberapa kali, terlihat penggunaan CPU 500%
  • Race condition tidak hanya menyebabkan kerusakan data atau deadlock, tetapi juga dapat merusak struktur data menjadi bentuk yang memungkinkan loop tak berujung, sehingga berujung pada gangguan performa

Memastikan cycle internal TreeMap

  • Ditulis eksperimen yang memakai reflection untuk mengakses field root, left, right, key, dan color pada TreeMap, lalu mencetak node dan warnanya
  • Jika saat traversal bertemu lagi dengan TreeMap.Entry yang sudah dikunjungi, itu dianggap sebagai cycle
private void print(
    Object treeMapEntry, String tabs, IdentityHashMap<Object, Object> visited
) throws Exception {
    if (treeMapEntry != null && !visited.containsKey(treeMapEntry)) {
        visited.put(treeMapEntry, treeMapEntry);
        print(treeMapEntryLeft.get(treeMapEntry), tabs + " ", visited);
        System.out.println(tabs + treeMapEntryKey.get(treeMapEntry) + ":"
            + (treeMapEntryColor.getBoolean(treeMapEntry) ? "BLACK" : "RED"));
        print(treeMapEntryRight.get(treeMapEntry), tabs + " ", visited);
    } else if (treeMapEntry != null && visited.containsKey(treeMapEntry)) {
        System.out.println(tabs + treeMapEntryKey.get(treeMapEntry) + ":"
            + (treeMapEntryColor.getBoolean(treeMapEntry) ? "BLACK" : "RED")
            + " CYCLE"
        );
    }
}
  • Untuk membuka pembatasan akses modul Java, argumen JVM berikut diperlukan saat menjalankan
--add-opens java.base/java.util=ALL-UNNAMED

Kasus terkait yang sudah ada dan perbedaannya

Reproduksi yang lebih realistis dengan ExecutorService

  • Kode yang sekadar mengabaikan NPE mungkin terlihat tidak realistis, tetapi pada ExecutorService, exception bisa tidak mudah terlihat
final ExecutorService pool = Executors.newFixedThreadPool(numThreads);
final TreeMap<Integer,Integer> treeMap = new TreeMap<>();
Random random = new Random();

for (int i = 0; i < numThreads*numUpdatesPerThread; i++) {
    pool.submit(() -> {
        treeMap.put(random.nextInt(10000), random.nextInt(10000));
    });
}
  • Kode lengkapnya ada di ExecutorUncaughtRepro.java
  • Saat dijalankan, program berhenti, dan thread dump menunjukkan thread yang tertahan di TreeMap.put()
  • Tidak ada apa pun di standard output
    • Thread pool menelan NPE sehingga sinyal masalah tidak terlihat
    • Situasi nyata juga seperti ini
  • Jika mengelola thread pool sendiri, langkah berikut diperlukan
    • Mendaftarkan uncaught exception handler melalui thread factory
    • Memproses Future yang dikembalikan dan memeriksa NPE yang dibungkus dalam ExecutionException lewat future.get()

Masalah yang sama pada layanan gRPC

  • Pada layanan berbasis thread pool seperti layanan gRPC, TreeMap tanpa proteksi juga dapat menimbulkan masalah yang sama
@Override
public void addReceipt(
    ReceiptProcessorServiceOuterClass.AddReceiptRequest req,
    StreamObserver<ReceiptProcessorServiceOuterClass.AddReceiptResponse> responseObserver
) {
    int timestamp = req.getTimestamp();
    int totalPrice = req.getTotalPrice();
    receipts.put(timestamp, totalPrice);
    ReceiptProcessorServiceOuterClass.AddReceiptResponse response =
        ReceiptProcessorServiceOuterClass.AddReceiptResponse.newBuilder().build();
    responseObserver.onNext(response);
    responseObserver.onCompleted();
}

Dugaan penyebab dan eksperimen lintas bahasa

  • Kemungkinan penyebabnya diduga berupa dua thread yang secara independen memutar tree ke arah berlawanan, atau penulisan dari rotasi yang tumpang tindih saling ter-interleave hingga membentuk cycle
  • Namun tidak ada pembuktian tentang interleaving thread mana yang membentuk cycle
  • Awalnya NPE diduga diperlukan, tetapi eksperimen berikutnya mematahkan asumsi ini
  • Upaya reproduksi masalah yang sama dilakukan di beberapa bahasa
    • Java menjadi basis seluruh kasus, sehingga dapat direproduksi
    • std::map pada C++ menggunakan red-black tree, dan sesekali thread berhenti dengan penggunaan CPU tinggi, bukan segfault
    • Go juga, berbeda dari perkiraan, dapat direproduksi; kodenya ada di eksperimen Go
    • Ruby tidak berhasil direproduksi meski bahasanya dapat menangkap NPE, dan ada kemungkinan GIL mencegah interleaving yang memicu masalah
  • Eksperimen C++, di luar dugaan, mereproduksi loop tak berujung bahkan tanpa try/catch atau exception null pointer
    • Kadang berakhir dengan segmentation fault
    • Sangat jarang, proses berhenti lebih dari 10 menit dan top menunjukkan SimpleRepro menggunakan 170,8% CPU
    • Karena dereference null pointer di C++ adalah segfault, harus ada interleaving yang tidak melewati null
  • Setelah melihat hasil ini, eksperimen Java dijalankan ulang 12 kali tanpa menangkap NPE, dan loop tak berujung di TreeMap.put() tetap berhasil direproduksi tanpa catch NPE

Perbaikan mudah dan pertahanan di level struktur data

  • Perbaikan paling mudah adalah melindungi TreeMap bersama
    • Membungkusnya dengan Collections.synchronizedMap
    • Menggantinya dengan ConcurrentHashMap dan hanya melakukan pengurutan saat diperlukan
  • Pertahanan yang lebih kontroversial adalah melacak node yang sudah dikunjungi saat traversal red-black tree
    • Jika bertemu kembali dengan node yang sudah dikunjungi, lempar ConcurrentModificationException
    • Ini tidak mencegah kerusakan data itu sendiri
    • Tetapi dapat mencegah penggunaan CPU 100% akibat loop tak berujung
    • Memori tambahan dibatasi oleh tinggi tree, yaitu O(lg(n))
    • Tinggi red-black tree dijamin O(lg(n))
    • Kemungkinan kecil standard library akan mengadopsi cara ini
  • Contoh perbaikan mencatat node yang sudah dikunjungi dengan IdentityHashMap di getEntry dan put
IdentityHashMap<Entry<?,?>, Boolean> visited = new IdentityHashMap<>();

while (p != null) {
    visited.put(p, true);
    int cmp = k.compareTo(p.key);
    if (cmp < 0)
        p = p.left;
    else if (cmp > 0)
        p = p.right;
    else
        return p;

    if (visited.containsKey(p)) {
        throw new ConcurrentModificationException("TreeMap corrupted. Loop detected");
    }
}

Membangun beberapa lapis pertahanan

  • Kesalahan bisa terjadi, jadi satu lapis pertahanan saja tidak cukup

  • Dalam kasus ini, beberapa kesalahan terjadi bersamaan, tetapi sebagian pemantauan masih tersisa sehingga masalah dapat ditemukan

  • Alert NPE

    • Tidak ada alert untuk terjadinya NPE itu sendiri
    • Hanya ada alert tingkat error, dan NPE ini hanya terjadi sekali per API handler worker thread sehingga tidak melewati ambang tingkat error
    • Karena cara executor menanganinya, log NPE juga tidak tersisa
  • Alert anomali penggunaan CPU

    • Penggunaan CPU dipantau dan alert berbasis ambang sederhana digunakan
    • Saat penggunaan CPU melewati ambang, itu dinilai sebagai perilaku abnormal dan alert muncul; lewat jalur inilah masalah ditemukan
  • Penanganan exception executor

    • Jika memasukkan pekerjaan ke executor, uncaught exception handler harus selalu disetel
    thread.setUncaughtExceptionHandler(
        (dyingThread, throwable) -> {
            logger.error("uncaught exception!", throwable);
        }
    );
    
  • Code review dan analisis statis

    • Pada code review, kombinasi thread dan TreeMap bisa saja ditemukan, atau mungkin muncul saran untuk tidak memakai TreeMap jika pengurutan tidak diperlukan, tetapi ini tidak terjadi dalam kasus ini
    • Alat analisis statis seperti SpotBugs, JLint, dan Chord dapat menemukan masalah concurrency semacam ini pada saat build
    • Sebagai materi terkait analisis statis, disebutkan How Good is Static Analysis at Finding Concurrency Bugs?
  • Pengujian

    • Tidak ada pengujian multithread untuk jalur kode tersebut
    • Jika kode dapat diakses secara bersamaan, pengujiannya juga perlu mencakup situasi multithread

Kesimpulan

  • Modifikasi serentak tanpa proteksi tidak hanya menyebabkan kerusakan data, tetapi juga dapat muncul sebagai loop tak berujung dan penggunaan CPU tinggi
  • Jika struktur data dengan pointer internal seperti TreeMap dimodifikasi oleh beberapa thread secara bersamaan, struktur red-black tree dapat membentuk cycle
  • Memilih struktur data tersinkronisasi, penanganan exception, alert, analisis statis, dan pengujian multithread secara bersamaan dapat membantu menemukan atau mencegah masalah lebih cepat

1 komentar

 
GN⁺ 2025-03-01
Pendapat di Hacker News
  • Sudah diketahui luas bahwa koleksi inti Java secara desain tidak thread-safe, jadi ini seharusnya terdeteksi
    OP perlu menelusuri sisa kodenya juga untuk memastikan apakah ada tempat lain yang memungkinkan beberapa thread memanipulasi koleksi secara bersamaan
    Membungkusnya dengan Collections.synchronizedMap atau menggantinya dengan ConcurrentHashMap akan membuat operasi map individual menjadi thread-safe, tetapi apakah rangkaian beberapa operasi juga aman adalah persoalan terpisah
    Objek yang memiliki TreeMap itu sendiri juga patut dicurigai apakah thread-safe, dan modifikasi seperti melacak node yang sudah dikunjungi hanya membuat koleksinya tetap tidak aman sambil gagal dengan cara yang lebih subtil
    Intinya bukan efek samping TreeMap, melainkan pelanggaran kontrak koleksi; menggantinya dengan HashMap pun tetap kode yang salah
    Dalam kode yang ditangani banyak thread, pendekatan yang paling pasti adalah membuat semua objek yang mungkin menjadi immutable, dan membatasi objek yang tidak bisa dibuat immutable hanya pada area kecil, independen, dan dikendalikan ketat

    • Bagian ini sangat penting: meskipun ada dynamic array yang thread-safe, kombinasi dua operasi—memeriksa panjang dengan size() lalu mengakses element(10)tidak atomik
      Jika thread lain menghapus elemen di antara dua pemanggilan itu, akses di luar batas bisa terjadi
      Solusinya adalah memakai metode atomik yang menangani keduanya sekaligus, misalnya element_or_null(), atau sejak awal memakai array biasa dan melindungi keseluruhan dua operasi tersebut dengan mutex
    • Data race terjadi pada shared mutable state yang tidak disinkronkan
      Karena ketiga syarat itu semuanya diperlukan, solusinya juga ada tiga: menambahkan sinkronisasi seperti lock, tidak membagikan akses mutable seperti pada model single owner yang memakai channel, atau membuat data menjadi immutable seperti insight dari bahasa fungsional murni
      Sejauh yang saya tahu, Google juga cukup banyak berinvestasi pada model ini di Guava
      Rust memungkinkan kita memilih mana dari tiga hal ini yang akan dilepas, dan secara statis mencegah ketiga syarat itu berlaku bersamaan
    • Pikiran pertama saya juga “tunggu, struktur data itu thread-safe tidak?”, dan pada akhirnya ini terlihat seperti pemilihan struktur data yang tidak sesuai tujuan
      Jika dipaksakan dengan mutex, biasanya akan muncul masalah lain dan bottleneck
      Jika ingin memakai objek immutable, perlu memakai struktur data immutable yang memanfaatkan structural sharing agar lonjakan penggunaan memori bisa dihindari atau dikurangi
      Dengan struktur data fungsional murni, satu sisi masalahnya hilang, tetapi jika thread-thread saling bergantung pada versi perantara yang dibuat satu sama lain, akan muncul masalah lain yang merepotkan dan mungkin perlu struktur data lain
      Jika tetap memaksakan struktur data mutable yang sudah dipakai, upaya akses bisa diserialisasi sebelum mencapai struktur data, lalu akses dibungkus sebagai transaksi sehingga hanya transaksi lengkap yang dijalankan; lama-lama rasanya seperti hampir mengimplementasikan database
    • Menurut saya, model threading yang mencoba menjalankan eksekusi independen di lingkungan shared memory itu sendiri pada dasarnya keliru
      Upaya untuk membuat hal semacam itu bekerja seharusnya dicurahkan pada model proses yang lebih baik
  • Contoh penyederhanaan kode di artikel asli tidak akurat
    Kode aslinya hanya menjalankan treeMap.put ketika unrelatedObjects tidak kosong, dan perbedaan ini bisa saja merupakan bug, bisa juga bukan
    Selain itu, perlu dipastikan apakah a dan b mengembalikan nilai yang sama setiap kali, dan apakah treeMap benar-benar berperilaku hanya seperti map
    Misalnya, jika itu map yang mencatat update ke log, perlu dipertimbangkan apakah mengubahnya agar hanya mencatat log sekali saja memang tidak masalah

    • Poin yang tepat. Seharusnya diubah menjadi if yang memeriksa apakah tidak kosong
  • Loop tak berhingga juga bisa terjadi jika implementasi Comparator atau Comparable tidak menyediakan total order yang konsisten: https://stackoverflow.com/questions/62994606/concurrentskips...
    Ini tidak terkait dengan concurrency
    Terjadi atau tidaknya bisa bergantung pada data konkret yang diproses dan urutan pemrosesannya, sehingga dapat terlihat normal dalam waktu lama lalu meledak di lingkungan produksi

    • Saya penasaran apakah ada yang benar-benar pernah melihatnya sendiri. Sepertinya bisa jadi bahan tulisan blog yang bagus
      Secara pribadi saya belum pernah menemui comparator buggy yang tidak memiliki total order
  • Saya dulu mengira race condition hanya menyebabkan kerusakan data atau deadlock, tidak terpikir bahwa ia juga bisa menimbulkan masalah performa
    Namun kalau race condition bisa merusak data hingga membuat infinite loop, itu masuk akal
    Karena itu saya sering berpikir bahwa error, perilaku aneh, dan warning dalam proyek pada prinsipnya harus diperbaiki. Sebab hal-hal itu bisa menimbulkan masalah yang tampaknya tidak berkaitan
    Namun jarang sekali prinsip seperti ini diterima oleh pihak yang menentukan apa yang harus dikerjakan

    • Itu aturan praktis yang bagus, tetapi perlu kepraktisan
      Dalam beberapa proyek, biaya untuk menghilangkan semua error, perilaku aneh, dan warning bisa jauh lebih besar daripada biaya menanggung masalah yang sesekali muncul dan tampaknya tidak berkaitan
      Hampir mustahil memprediksi apakah error tertentu akan terlibat dalam masalah di masa depan dan apakah biaya memperbaikinya lebih awal akan lebih murah, jadi pada akhirnya ini lebih dekat ke keterampilan daripada sains
      “Tidak memperbaiki apa pun” itu buruk, dan “memperbaiki semuanya” biasanya tidak realistis, jadi dibutuhkan kerangka pengambilan keputusan, intuisi dari pengalaman, serta kepercayaan para pemangku kepentingan
    • Di tempat kerja, semua warning diperlakukan sebagai error, dan pull request tidak bisa di-merge kecuali semua pipeline CI yang berjalan otomatis lulus
      Ini membutuhkan disiplin, tetapi setelah sudah dibuat seperti itu, mempertahankannya jauh lebih mudah
    • Penting juga untuk tidak menangkap exception lalu mengabaikannya
      Kecuali kita tahu secara spesifik bahwa aman untuk melanjutkan eksekusi program, “menangkap lalu hanya mencatat ke log” juga ide yang buruk
      Lebih baik membiarkan exception dipropagasikan sampai ke tempat yang bisa menanganinya secara berguna, misalnya mengembalikan 500 atau menampilkan dialog error
    • Pada akhirnya kita harus memilih pertempuran
      Ada masalah di proyek Rails yang saya rawat: log penuh dengan “unsupported parameters”. Saya sudah memeriksa controller dengan cermat dan mengizinkannya, tetapi pesan itu tetap muncul
      Mungkin tidak berbahaya, tetapi membuat log sangat berisik
      Beberapa orang mencoba menyelesaikannya, tetapi selalu ada prioritas yang lebih tinggi, dan sulit membenarkan menghabiskan banyak waktu untuk sesuatu yang tidak berdampak pada fitur
      Ini seperti wasir yang menjengkelkan. Pilihannya antara operasi lalu menderita berat selama beberapa minggu, atau menahannya saja; biasanya jinak, tetapi ada kemungkinan memburuk dan menjadi masalah besar
      Rasanya fenomena seperti ini boleh disebut wasir digital
    • Bahkan tanpa kerusakan, race condition bisa membuat pekerjaan yang sama dilakukan berkali-kali dan hanya menyisakan satu hasil, sehingga menimbulkan masalah performa besar
      Jika sebuah warning dinilai tidak relevan, setidaknya sebaiknya dijelaskan dengan komentar, dan kalau memungkinkan tambahkan pragma yang mematikan warning dalam cakupan sesempit mungkin
      Saya lebih suka menghilangkan perilaku aneh. Saya pernah melihat kode yang ditandai “tidak tahu kenapa ini bekerja” kemudian tidak lagi bekerja, dan seandainya dirapikan lebih awal, kami akan punya waktu untuk memperbaikinya dengan hati-hati, bukannya harus menulis ulang dengan tergesa-gesa
  • Bagian “nyaris berhasil masuk lewat ssh” mengingatkan saya pada masa pascasarjana
    Lab kami adalah kelompok kecil sekitar 8 orang yang meneliti pemrosesan paralel dan terdistribusi, berbagi sebuah mesin yang sepertinya Sun UltraSparc 170, dengan hard disk 1GB dan RAM sekitar 128 atau 256MB
    Perlu juga diingat bahwa mesin Sun hampir tidak pernah di-reboot
    Seorang mahasiswa baru tampaknya membagi file teks besar menjadi 32 atau 64 bagian berdasarkan nomor baris, lalu tanpa membuat file terpisah menjalankan N salinan perl secara paralel agar masing-masing memproses rentang barisnya sendiri
    Untuk ukuran saat itu, N interpreter perl memakan banyak RAM, dan ketika swapping terjadi, masing-masing proses mencoba membaca beberapa baris lagi sambil melakukan seek secara gila-gilaan ke bagian berbeda dari file yang sama
    Selain itu, kemungkinan besar proses ke-J harus membaca J/N dari file untuk sampai ke bagiannya sendiri
    Dari konsol, bahkan prompt login pun tidak muncul, tetapi untungnya sudah ada sesi yang sedang login, dan su menampilkan prompt kata sandi setelah 20–30 menit
    Setelah 5–10 menit lagi saya mendapatkan sesi root, memeriksa penyebabnya dengan top, lalu mencoba menghubungi pengguna dan membunuh proses bermasalah, sehingga sistem kembali normal
    Idenya sendiri benar, tetapi itu kasus yang sama sekali tidak memahami batas sistem; karena hard disk dan RAM yang rendah, bottleneck I/O sangat parah, sehingga pemrosesan linear sederhana kemungkinan jauh lebih baik

  • Baik di Java maupun bahasa lain, melakukan operasi bersamaan pada objek yang tidak thread-safe akan menghasilkan bug-bug paling menarik di dunia

    • Karena itu, saat memakai objek yang tidak thread-safe dari beberapa thread, kita harus mengelola akses atomik sendiri atau memakai versi yang thread-safe
      Error multithreading adalah yang paling buruk untuk di-debug
      Dalam kasus ini, seharusnya sangat mudah dikenali pada tahap desain, dan lampu peringatan semestinya menyala begitu hendak memakai container biasa di lingkungan multithread
    • Saya cukup sering mengalami bug concurrency, tetapi belum pernah sengaja mereproduksi inkonsistensi yang muncul ketika modifier volatile dihapus dari field mutable Java
      Mungkin JVM yang dipakai saat itu dibuat terlalu baik
    • Bahkan ketika saya yakin bisa menanggung konsekuensi dari berbagi sesuatu yang tidak disinkronkan, hasil nyatanya selalu di luar dugaan
    • Ini agak ekstrem, tetapi karena alasan seperti ini saya lebih suka debugging race di C/C++
      Bahasa itu memang menetapkan semantik gila yang pada praktiknya hampir tidak bermakna ketika race terjadi, tetapi dalam kenyataannya kerusakan memori atau masalah yang berisik sering muncul
      Karena sebagian besar race bersifat ilegal, kita bisa membuat alat seperti thread checker tanpa harus mengubah source code untuk menandai maknanya
      Sebaliknya, Java tidak memiliki undefined behavior, tetapi mudah terjadi dua field melenceng secara halus, dan hal semacam ini jauh lebih sulit dilacak
    • Sebagian, mungkin sebagian besar, operasi di Java Collections melakukan pemeriksaan integritas untuk memperingatkan masalah seperti ini
      Misalnya map melempar ConcurrentModificationException
  • Saya juga pernah melihat fenomena yang sama di C# yang sedang berjalan di production
    Gejalanya sama, dan saat melihat process dump ada dictionary yang rusak
    Saya kira semuanya memakai ConcurrentDictionary, tetapi ternyata satu yang berasal dari library menjadi masalah
    Saat itu kami memakai .NET Framework, dan seingat saya .NET Core memiliki kode untuk mendeteksi modifikasi bersamaan
    Saya tidak tahu implementasinya, tetapi penghitung versi saja seharusnya cukup
    Aneh kalau terpaku seolah-olah NPE adalah bahan utama. Pada pola kejadian aslinya, tampaknya itu tidak ada, dan hanya karena tidak ada exception bukan berarti bug seperti ini tidak ada di C
    Intinya adalah invarian kelas. Secara umum, selama mutator berjalan, invarian tidak berlaku dan baru dipulihkan di akhir
    Jika mutator lain dijalankan sebelum invarian dipulihkan, struktur data akan rusak. Kalau tidak dimulai dari keadaan yang valid, kecil kemungkinan akhirnya menjadi keadaan yang valid

    • Pada akhirnya itu disebabkan oleh logika yang keliru
      Karena mengalami nasib sial berupa tidak bisa mereproduksinya dengan NPE yang tidak tertangkap, saya keliru menyimpulkan bahwa NPE yang tidak tertangkap adalah syarat yang diperlukan
  • Saya pernah melihat hal yang sama pada java.util.HashMap yang kurang sinkronisasi
    Itu sekitar tahun 2009, tetapi setahu saya masih mungkin terjadi sampai sekarang
    Sepingat saya HashMap memakai chaining untuk menyelesaikan collision, dan sepertinya ada siklus yang terbentuk di dalam chain
    Namun alih-alih menggali untuk memverifikasinya, kami fokus menghapus seluruh kode yang bermasalah
    Pengetahuan tentang concurrency sering ditanyakan dalam wawancara, dan kalau menganggap data race sebagai masalah sepele, sulit memberi kesan yang baik; kasus ini menunjukkan alasannya

    • Saya tidak tahu hal seperti ini bisa terjadi juga di HashMap
      Apakah ini berkaitan dengan linked list yang dipakai untuk menangani collision?
  • Saya pikir bisa saja menaruh counter yang bertambah untuk mencari siklus, lalu melempar exception jika nilainya lebih besar daripada kedalaman tree atau ukuran collection
    Berbeda dari pendekatan hash set yang diusulkan penulis, cara ini nyaris tidak punya overhead memori atau CPU dan tampaknya lebih mungkin diterima
    Namun selama memakai C# lebih dari 10 tahun, saya belum pernah gagal mempertimbangkan akses bersamaan ke struktur data dalam situasi concurrency

    • Bukan ide yang buruk, tetapi menambahkan precondition yang berguna dalam situasi yang memiliki data race umumnya cukup sulit
      Ada banyak cara lain tree bisa rusak selain yang ini
    • Ide bagus. Saya pernah melakukannya sebelumnya pada binary search dan struktur tree
      Jika memungkinkan, hindari infinite loop, dan kasus yang tidak bisa dihindari cukup jarang
      Ini bukan perbaikan, tetapi strategi mitigasi yang bagus
      Infinite loop adalah salah satu bug paling menyebalkan. Mudah ditemukan di debugger, tetapi hasilnya bisa sangat tidak ramah seperti situasi OP yang “ssh saja nyaris tidak bisa”
      Infinite loop di kode library terasa lebih menjengkelkan lagi
    • Itu jauh lebih baik. Cukup memakai memori konstan
      Ada jaminan bahwa jumlah node tidak lebih dari tinggi tree
  • Exception di dalam thread benar-benar fatal
    Ada kisah pelacakan bug mengerikan yang tokoh utamanya adalah C++, select(), dan thread yang melempar-lempar exception: https://news.ycombinator.com/item?id=42532979

    • Saya ingat pernah membaca tulisan itu, tetapi tidak memahaminya karena kurang punya pengetahuan di bidang tersebut
      Saya harus membacanya lagi