ข้าม​ไป​ยัง​เนื้อหา

virtual thread · วัด​ด้วย​จำนวน ไม่ใช่​ด้วย​เวลา

ค้น​เรื่อง virtual thread แล้ว​คุณ​จะ​เจอ​กราฟ​เวลา​เป็น​ส่วน​ใหญ่

คอร์ส​นี้​ไม่​ใช้​หน่วย​เวลา​สัก​ตัว และ​บท​นี้​เป็น​บท​ที่​ข้อ​นั้น​ยั่ว​ใจ​ที่สุด · เพราะ​ฉะนั้น​เรา​จะ​วัด​ด้วย​อย่าง​อื่น

ไม่ใช่​เพราะ​มัน​โกหก แต่​เพราะ​มัน​ไม่​ข้าม​เครื่อง

ตัวเลข​เวลา​ขึ้น​กับ​จำนวน​แกน​ประมวล​ผล · ขึ้น​กับ​ว่า​มี​อะไร​รัน​อยู่​ข้างๆ · ขึ้น​กับ​สถานะ​ของ JIT ตอน​วัด · และ​ขึ้น​กับ​ปลาย​ทาง​ที่​รอ

ผล​คือ​คุณ​อ่าน​กราฟ​ของ​คน​อื่น​แล้ว​เอา​มา​ตัดสิน​ใจ​ไม่​ได้ · และ​รัน​ซ้ำ​บน​เครื่อง​ตัว​เอง​ก็ได้​เลข​ไม่​เท่า​เดิม

สิ่ง​ที่​ข้าม​เครื่อง​ได้​คือ​ความ​สัมพันธ์ เช่น “ของ​นี้​น้อย​กว่า​ของ​นั้น​มาก” · ไม่ใช่ “ของ​นี้​เท่ากับ​สิบ​แปด”

จำนวน​ที่​จบ กับ​จำนวน carrier ที่​รับ​งาน​ไป​รัน

int n = 10_000;
Set<String> carriers = ConcurrentHashMap.newKeySet();
Set<String> virtuals = ConcurrentHashMap.newKeySet();
CountDownLatch done = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
final int id = i;
Thread.ofVirtual().start(() -> {
virtuals.add("v" + id);
String carrier = Thread.currentThread().toString();
int at = carrier.lastIndexOf('@');
carriers.add(at >= 0 ? carrier.substring(at) : carrier);
done.countDown();
});
}
done.await();

ผล​ที่​ได้

นับ​อะไรได้
สร้าง10000
จบ​ครบ10000
carrier ที่​เห็นเท่ากับ​จำนวน​แกน​ประมวล​ผล​ของ​เครื่อง​ที่​วัด

Thread.ofVirtualThread.ofVirtualทาง​เข้า​ที่​สร้าง virtual thread · คู่​กับ `Thread.ofPlatform` ที่​สร้าง​แบบ​เดิม · ตัว​ที่​ได้​ตอบ `isVirtual()` ว่า​จริง ซึ่ง​เป็น​วิธี​ตรวจ​ที่​ไม่​ต้อง​เดา สร้าง​หนึ่ง​หมื่น​ตัว​ได้​โดย​ไม่มี​อะไร​บ่น · ลอง Thread.ofPlatform ด้วย​จำนวน​เดียวกัน​บน​เครื่อง​เดียวกัน แล้ว​คุณ​จะ​ได้​คำ​ตอบ​ว่า​ทำไม​ข้อ​นี้​ถึง​สำคัญ

เลข carrier ผูก​กับ​เครื่อง · คอร์ส​นี้​จึง​ไม่​พิมพ์​มัน​ลง​ไป

บน​เครื่อง​ที่​ผม​วัด จำนวน carrier ออก​มา​เท่ากับ​จำนวน​แกน​ประมวล​ผล​พอดี

แต่​เลข​นั้น​เขียน​ลง​บท​เรียน​ไม่​ได้ เพราะ​เครื่อง​ของ​คุณ​มี​แกน​ไม่​เท่า​ผม · ถ้า​ผม​พิมพ์​มัน​ลง​ไป มัน​จะ​เป็น​ตัวเลข​ที่​ผิด​สำหรับ​ผู้​อ่าน​เกือบ​ทุก​คน

สิ่ง​ที่​เขียน​ได้​คือ​ความ​สัมพันธ์ · carrier น้อย​กว่า thread ที่​สร้าง​มาก และ​ผูก​กับ​จำนวน​แกน ไม่​ได้​ผูก​กับ​จำนวน thread

รัน​เอง​แล้ว​ดู​เลข​ของ​เครื่อง​คุณ · นั่น​คือ​เลข​ที่​มี​ความ​หมาย​กับ​คุณ

flowchart TB
  V["virtual thread หนึ่งหมื่นตัว"]
  C["carrier thread จำนวนเท่ากับแกนประมวลผล"]
  OS["thread ของระบบปฏิบัติการ"]
  V -->|"ตัวจัดคิวของ JVM สลับให้"| C --> OS
  N["ข้อที่ผูกกับเครื่อง · จำนวน carrier"]
  M["ข้อที่ข้ามเครื่อง · carrier น้อยกว่า virtual thread มาก"]

คำ​บรรยาย​ภาพ: virtual thread จำนวน​มาก​วิ่ง​ผ่าน carrier จำนวน​น้อย​ที่​ผูก​กับ​จำนวน​แกน

carrier threadcarrier threadthread ของ​ระบบ​ปฏิบัติ​การ​ที่​รับ virtual thread ไป​รัน​จริง · จำนวน​ของ​มัน​ผูก​กับ​จำนวน​แกน​ประมวล​ผล ไม่​ได้​ผูก​กับ​จำนวน virtual thread ที่​เรา​สร้าง คือ​อะไร และ​ทำไม​ต้อง​รู้จัก

หัวข้อ​ที่​มีชื่อ​ว่า “carrier thread คือ​อะไร และ​ทำไม​ต้อง​รู้จัก”

เพราะ​มัน​คือ​ที่​ที่​ข้อ​จำกัด​เก่า​ยัง​อยู่

virtual threadvirtual threadthread ที่​ตัว​จัด​คิว​ของ JVM ดูแล​เอง สร้าง​พร้อม​กัน​จำนวน​มาก​ได้ · ไม่​ต้อง​เปิด​ธง​อะไร​ใน JDK 25 และ​เขียน​แบบ​เดียว​กับ thread เดิม​ทุก​บรรทัด ไม่ใช่ thread ของ​ระบบ​ปฏิบัติ​การ · มัน​เป็น object ที่ JVM สลับ​เข้า​ออก จาก thread จริง​จำนวน​น้อย​ที่​เรียก​ว่า carrier

ตราบ​ใด​ที่ virtual thread รอ​อะไร​อยู่ มัน​คืน carrier ให้​ตัว​อื่น​ใช้ · นั่น​คือ​ทั้งหมด​ที่​มัน​ทำ และ​เป็น​เหตุผล​ที่​สร้าง​จำนวน​มาก​ได้

แต่​ถ้า​มัน​ไม่​คืน carrier เช่น​ไป​ติด​อยู่​ใน​บล็อก​ที่​ยึด monitor ไว้ จำนวน carrier ที่​มี​อยู่​น้อย​ก็​กลาย​เป็น​เพดาน​ทันที

เว็บ​นี้​มี​บทความ​ที่​วัด​ข้อ​นั้น​คร่อม​สาม​รุ่น​ของ JDK อยู่​แล้ว · virtual thread ที่​ยัง pin ข้าม JEP 491 · บท​นี้​ไม่​เล่า​ซ้ำ

ข้อ​นี้​สำคัญ​กว่า​ที่​หน้าตา​บอก เพราะ​มัน​คือ​เหตุผล​ที่​ย้าย​ได้​จริง

  • เขียน​เหมือน​เดิม​ทุก​บรรทัด ไม่มี async ไม่มี await ไม่มี​ชนิด​ใหม่​ให้​เรียน​รู้
  • ตัว​จับ exception ทำงาน​เหมือน​เดิม stack trace ยัง​อ่าน​ได้​ตาม​ปกติ
  • ThreadLocal ยัง​ใช้ได้ แต่​คิด​ใหม่​ว่า​ควร​ใช้​ไหม เมื่อ​มี thread เป็น​หมื่น​ตัว · ScopedValueScopedValueค่าที่​ส่ง​ต่อ​ลง​ไป​ใน​ขอบเขต​ที่​กำหนด โดย​ไม่​ต้อง​ส่ง​ผ่าน argument ทุก​ชั้น · เข้า​รุ่น​ถาวร​แล้ว​ใน JDK 25 ต่าง​จาก​ที่​หลาย​คน​ยัง​จำ​ว่า​เป็น preview คือ​ทาง​ที่​ตั้งใจ​ให้​แทน และ​มัน​เป็น final แล้ว​ใน 25
  • ไม่​ต้อง​เปิด​ธง​อะไร เป็น​ของ​ถาวร​ตั้งแต่ JDK 21

สิ่ง​ที่​เปลี่ยน​คือ การ​ใช้ pool กลาย​เป็น​เรื่อง​ไม่​จำเป็น · pool มี​ไว้​เพราะ thread แพง · เมื่อ​มัน​ไม่​แพง​แล้ว pool กลาย​เป็น​เพดาน​ที่​คุณ​ตั้ง​เอง

สาม​อย่าง​ที่​ยัง​ไม่มี และ​แต่ละ​อย่าง​มี​เหตุผล​ของ​มัน

  • ไม่​วัด​เวลา​เลย ทั้ง​บท · ถ้า​อยาก​รู้​ว่า​เร็ว​ขึ้น​ไหม​กับ​งาน​ของ​คุณ ต้อง​วัด​กับ​งาน​ของ​คุณ​เอง · ตัวเลข​ของ​คน​อื่น​ตอบ​ข้อ​นี้​ไม่​ได้
  • ไม่​แตะ pinning มี​บทความ​ที่​มี​ด่าน​ของ​ตัว​เอง​อยู่​แล้ว
  • ไม่​ได้​ใช้ StructuredTaskScope ซึ่ง​เป็น​ของ​คู่​กัน​โดย​ธรรมชาติ · แต่​มัน​ยัง preview · บท​ที่ 7 ว่า​ด้วย​เรื่อง​นั้น​โดยตรง

บท​นี้​เลือก​หน่วย​ใหม่ แล้ว​ได้​ข้อ​อ้าง​ที่​ตรวจ​ซ้ำ​ได้

virtual thread หนึ่ง​หมื่น​ตัว​จบ​ครบ​ทุก​ตัว · และ​จำนวน carrier ผูก​กับ​จำนวน​แกน​ประมวล​ผล ไม่​ได้​ผูก​กับ​จำนวน thread ที่​สร้าง

หน่วย​ที่​ใช้​คือ​จำนวน ซึ่ง​รัน​บน​เครื่อง​ไหน​ก็ได้​ความ​สัมพันธ์​เดียวกัน · ต่าง​จาก​ตัวเลข​เวลา​ที่​เปลี่ยน​ทุก​ครั้ง​ที่​รัน

บท​หน้า​เป็น​บท​ที่​สอง​ที่​คอร์ส​นี้​มี​อยู่​เพื่อ​มัน — ราคา​ของ preview · และ​เรา​จะ​วัด​มัน​ด้วย​ข้อความ​ที่ JVM ตอบ


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

จำนวน​ทั้ง​สอง​มา​จาก​การ​รัน​จริง:

  • วัด​เมื่อ 2026-09-01 · openjdk 25.0.4.1 2026-08-18
  • รัน​ซ้ำ​เอง​ได้​ด้วย npm run verify:java-modern ซึ่ง​นับ carrier จาก​ชื่อ​ของ thread ที่​กำลัง​รัน​อยู่ · ด่าน​ของ probe บังคับ​แค่​ว่า carrier ต้อง​น้อย​กว่า thread ที่​สร้าง ไม่​ได้​บังคับ​ตัวเลข​ตายตัว เพราะ​ตัวเลข​นั้น​ผูก​กับ​เครื่อง
  • JEP 444 — Virtual Threads (เข้า​ถึง 2026-09-01)
  • JEP 506 — Scoped Values (เข้า​ถึง 2026-09-01) — สถานะ final ใน 25 และ​เหตุ​ที่​มัน​ตั้งใจ​มา​แทน ThreadLocal ใน​บริบท​นี้

เช็กความเข้าใจ — บทที่ 6

ข้อ 1 / 3

ทำไมบทนี้ถึงไม่พิมพ์จำนวน carrier ที่วัดได้ลงไปในบทเรียน