นากแม่น้ำต้นฉบับกำลังคัดบัตรงานซ้ำจากสายพานหลายเส้นลงตะกร้ากลางเพียงใบเดียว

Java ConcurrentHashMap.newKeySet: ทำ concurrent set โดยไม่ใส่ Boolean หลอก

พี่โชว์Engineering

ถ้าหลาย thread ต้องตอบเพียงว่า “เคยเห็น id นี้หรือยัง” การใช้ ConcurrentHashMap<String, Boolean> ทำให้ value เป็นของหลอกครับ ConcurrentHashMap.newKeySet() คืน concurrent set ที่สื่อ contract ตรงกว่า และ add บอกได้ทันทีว่าเราเป็นคนเพิ่มรายแรกหรือไม่

WHY: ให้ชนิดข้อมูลบอกเจตนา

เหมือนจุดแจกบัตรคิวที่สนใจเพียงเลขบัตร ไม่ได้ต้องจด true หลังทุกหมายเลข Set ลดโอกาสที่คนจะตีความ value ผิด และทำให้โค้ด dedup อ่านเป็นประโยค if (seen.add(id)) { ... }

HOW: รับเหตุการณ์ซ้ำจากหลาย worker

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
  public static void main(String[] args) throws Exception {
    Set<String> seen = ConcurrentHashMap.newKeySet();
    ExecutorService pool = Executors.newFixedThreadPool(4);
    try {
      for (int i = 0; i < 100; i++) {
        int id = i % 10;
        pool.submit(() -> seen.add("job-" + id));
      }
    } finally {
      pool.shutdown();
      assert pool.awaitTermination(2, TimeUnit.SECONDS);
    }

    assert seen.size() == 10 : seen;
    assert !seen.add("job-3") : "duplicate must return false";
    assert seen.add("job-10") : "new key must return true";
  }
}

Set นี้ไม่รับ null ตาม contract ของ ConcurrentHashMap และ iterator เป็น weakly consistent: มองเห็นสถานะบางช่วงระหว่างการเปลี่ยนแปลงได้โดยไม่โยน ConcurrentModificationException จึงอย่าใช้การวนรายการเป็น snapshot ทางบัญชี

ขอบเขตที่ต้องเลือก map แทน

ถ้าต้องเก็บ timestamp, retry count หรือ owner ต่อ key ให้ใช้ ConcurrentHashMap<K,V> จริง และอัปเดตด้วย compute/merge ตาม semantics งาน newKeySet เหมาะกับ membership เท่านั้น นอกจากนี้ dedup ในหน่วยความจำไม่ durable; process restart แล้วลืมทั้งหมด ถ้างานห้ามทำซ้ำข้าม restart ต้องมีฐานข้อมูลหรือ idempotency key ที่ชั้นปลายทาง

Recap

  • ใช้ newKeySet เมื่อต้องการ membership concurrent อย่างเดียว
  • ใช้ค่าคืนจาก add เพื่อรู้ว่าเป็นผู้เพิ่มรายแรก
  • อย่าถือ iterator เป็น snapshot แบบหยุดเวลา
  • เพิ่ม durable idempotency เมื่อ restart แล้วห้ามทำซ้ำ