Họ đang đo gì
Bạn có tò mò về thứ mình dùng hằng ngày không. Đây cũng là câu dẫn tự nhiên sang ConcurrentHashMap.
Trả lời ngắn~30 giây
Bên trong là một mảng Node[]. Khoá được băm rồi trộn thêm (h ^ (h >>> 16)) để phân tán bit cao, sau đó lấy hash & (n-1) ra chỉ số bucket. Va chạm nối thành danh sách; từ Java 8, một bucket vượt 8 phần tử (và bảng đã đủ 64 ô) sẽ chuyển thành cây đỏ-đen, nên trường hợp xấu nhất từ O(n) xuống O(log n). Bảng tăng gấp đôi khi số phần tử vượt capacity × 0.75.
Giải thích sâu
Phép trộn bit tồn tại vì chỉ số bucket chỉ dùng các bit THẤP của hash. Nếu hashCode() của bạn chỉ khác nhau ở bit cao — chuyện rất hay xảy ra với hash dựa trên địa chỉ hoặc ID tăng dần nhân với số lớn — thì mọi khoá rơi vào cùng một bucket. Dịch phải 16 bit rồi XOR là cách rẻ nhất để bit cao có ảnh hưởng tới chỉ số.
Việc chuyển sang cây có lý do bảo mật chứ không chỉ hiệu năng: trước Java 8, kẻ tấn công có thể gửi hàng nghìn khoá cố tình cùng bucket để biến mọi lần tra cứu thành O(n) — một dạng tấn công từ chối dịch vụ bằng va chạm hash, từng ảnh hưởng nhiều framework web. Cây đỏ-đen chặn kịch bản đó, với điều kiện khoá Comparable.
Về việc resize: nó không rẻ, vì mọi phần tử phải được đặt lại chỗ. Nếu bạn biết trước số phần tử, khởi tạo với capacity phù hợp (new HashMap<>(expected / 0.75f + 1)) tránh được vài lần resize. Đây là một trong ít chỗ tinh chỉnh nhỏ mà đo được lợi ích thật khi map lớn.
Câu hỏi tiếp theo họ sẽ hỏi
?HashMap dùng trong môi trường đa luồng thì sao?
Hỏng theo cách khó đoán: mất dữ liệu, và ở Java 7 còn có thể tạo vòng lặp vô hạn trong danh sách liên kết khi resize đồng thời, làm CPU lên 100% mãi mãi. Java 8 đổi cách resize nên không còn vòng lặp đó, nhưng vẫn mất dữ liệu. Dùng ConcurrentHashMap.
?ConcurrentHashMap khoá thế nào?
Từ Java 8 nó bỏ segment: đọc hoàn toàn không khoá (trường Node.val là volatile), ghi vào bucket rỗng dùng CAS, còn ghi vào bucket đã có thì synchronized trên node đầu của đúng bucket đó. Nghĩa là mức tranh chấp tỉ lệ với số bucket chứ không phải số segment cố định.
Trả lời thế này là mất điểm
- Nói
HashMapluôn O(1). Nó là O(1) trung bình có phân bổ; xấu nhất là O(log n) từ Java 8, và O(n) trước đó. - Mô tả cơ chế của Java 7 (segment lock) như hiện trạng của
ConcurrentHashMap.