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 น้อยกว่า 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 ที่วัดได้ลงไปในบทเรียน