Java Internals & Concurrency/Module 04 — Tổng kết & cheat sheet Collections Internals
75/75
Bài 75 / 75~6 phútCollections InternalsMiễn phí lượt xem

Module 04 — Tổng kết & cheat sheet Collections Internals

Một trang để bookmark: cheat sheet grow strategy, glossary nội thất ArrayList/HashMap, pitfall lớn nhất, và self-assessment ba outcome của module.

Module này bóc nội tạng JDK của hai cấu trúc dữ liệu bạn dùng mỗi ngày — ArrayListHashMap — và trả lời những con số cụ thể mà khoá Thuật toán không chạm tới: vì sao grow 1.5x, vì sao treeify ở ngưỡng 8. Trang này gom cả module thành một chỗ tra cứu nhanh.

🧵 Đã đi qua những gì

Sợi chỉ xuyên suốt là cùng một câu chuyện resize, khác chiến lược. Bài 01 — ArrayList.grow() đọc thẳng OpenJDK 21: ArrayList grow bằng oldCapacity + (oldCapacity >> 1) — 1.5x, không phải 2x của C++ std::vector — vì 1.5 nhỏ hơn golden ratio φ nên allocator tái sử dụng được block cũ, waste RAM trung bình 17% thay vì 25%, và tính bằng bit shift nguyên. Pre-size loại bỏ toàn bộ grow event. Bài 02 — HashMap internals trace một put đi qua bucket array: index bằng bit AND (n-1) & hash (nhanh hơn modulo, đòi capacity power-of-2), collision thì walk chain so hash trước equals() sau, resize nhân đôi khi size vượt capacity × 0.75 và re-distribute bằng bit (oldHash & oldCap) không recompute hash — O(1) chỉ đúng amortized. Bài 02b — treeify mổ đường phòng thủ khi hash không tử tế: chain đạt TREEIFY_THRESHOLD = 8 và table từ 64 bucket trở lên thì bucket chuyển thành red-black tree, hạ worst case từ O(n) xuống O(log n) — ngưỡng 8 chọn theo Poisson (chạm gần như chắc chắn là hash bị lạm dụng), nhưng treeify chỉ giảm thiệt hại DoS chứ không thay được validate input. Mini-challenge ghép cả ba vào một sự cố ingest service: P99 spike do resize storm của cả ArrayList lẫn HashMap, và CPU 100% do hash-collision DoS — cùng lúc, cùng root cause là cách khởi tạo collection.

flowchart TD
    A["Collection dong trong JDK"] --> B["ArrayList: mang lien tiep<br/>grow 1.5x, pre-size"]
    A --> C["HashMap: bucket + chaining<br/>resize 2x @ load factor 0.75"]
    C --> D["Treeify @ 8 entry<br/>phong thu collision DoS"]
    B --> E["Mini-challenge:<br/>resize storm + DoS trong 1 service"]
    C --> E
    D --> E

🗺️ Cheat sheet

Cấu trúcGrow strategyBí quyết pre-sizePitfall lớn nhất
ArrayListMảng liên tiếp, lazy cap 10, grow 1.5x (>> 1)new ArrayList<>(n) hoặc Stream.toList()Collect DB query không hint capacity → nhiều grow (bài 01)
HashMapBucket power-of-2, resize 2x khi size vượt cap × 0.75HashMap.newHashMap(n) (Java 19+), chia cho load factorTruyền thẳng n làm capacity → vẫn resize; mutable key → get() trả null (bài 02)
Bucket treeifiedChain đạt 8 (table ≥ 64) → red-black tree; co còn 6 entry → về listHash tốt thì không bao giờ chạmCoi treeify là đủ chống DoS — nó chỉ là lưới cuối (bài 02b)

📖 Glossary module

Thuật ngữMột câuNguồn
Grow factorHệ số nhân capacity mỗi lần collection đầy — ArrayList 1.5x, Vector 2x.Bài 01
Golden ratio boundr nhỏ hơn φ ≈ 1.618 thì tổng block đã free đủ cho block mới → allocator tái dùng được.Bài 01
Amortized O(1)Chi phí trung bình mỗi thao tác qua chuỗi dài là hằng số, dù một thao tác lẻ có thể O(n).Bài 01, 02
Separate chainingXử lý collision bằng cách mỗi bucket giữ một danh sách entry cùng index.Bài 02
Load factorTỷ lệ size / capacity mà vượt qua thì HashMap resize — mặc định 0.75.Bài 02
Power-of-2 capacityHashMap ép capacity là luỹ thừa 2 để tính bucket index bằng bit AND thay modulo.Bài 02
TreeifyChuyển bucket quá 8 entry thành red-black tree, hạ lookup worst case xuống O(log n).Bài 02b
Hysteresis (8/6)Khoảng đệm giữa treeify (8) và untreeify (6) tránh dựng/phá cây liên tục khi size dao động.Bài 02b
Hash-collision DoSTấn công nhồi key cùng hashCode để mọi thao tác thành O(n), đốt CPU.Bài 02b

⚠️ Pitfall tổng hợp

1. Không pre-size collection biết trước kích thước (bài 01, 02).

// SAI -- default cap, resize storm khi biet truoc N
List<Row> rows = new ArrayList<>();
Map<String, Row> index = new HashMap<>();

// DUNG -- pre-size, 0 grow
List<Row> rows = new ArrayList<>(expectedCount);
Map<String, Row> index = HashMap.newHashMap(expectedCount);

2. Truyền thẳng số phần tử làm initial capacity của HashMap (bài 02). new HashMap<>(2000) vẫn resize một lần: constructor làm tròn 2000 lên power-of-2 là 2048, threshold = 2048 × 0.75 = 1536 → resize ở entry 1.537. Dùng HashMap.newHashMap(2000) hoặc new HashMap<>((int)(2000 / 0.75 + 1)).

3. Mutable key trong HashMap (bài 02). Mutate field dùng trong hashCode() sau khi put → key "lạc" bucket, get() trả null, entry unreachable (memory leak). Dùng final field hoặc record.

4. Coi treeify là đủ chống collision DoS (bài 02b). Treeify hạ worst case xuống O(log n) nhưng không chặn tấn công. Phòng thủ thật là giới hạn input + rate limiting đứng trước.

✅ Self-assessment

Bạn đã đạt module này nếu tự tin tick đủ ba ô — mỗi ô khớp một learning outcome:

  • Explain được vì sao ArrayList.grow() dùng 1.5x thay vì 2x (memory utilization, block reuse) và hệ quả pre-sizing trong production.
  • Trace được HashMap internals: bucket array, separate chaining, resize re-distribution, và khi nào claim O(1) thực sự đứng vững.
  • Diagnose được hash-collision DoS và cơ chế treeify/untreeify Java 8 phòng thủ suy biến O(n).

🚀 Học tiếp gì

Module này là chặng đầu của khoá Java Internals nhìn nội thất collection. Hai hướng đi tiếp tự nhiên:

📚 Tài liệu mở rộng

Bài đầu tiên của module kế: Stream I/O cổ điển

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên