สวัสดี Virtual Threads นานๆ ที Java จะมีของให้ตื่นเต้น!

jo@sabotender
KBTG Life
Published in
6 min readSep 13, 2022

ช่วงนี้ผมวนเวียนอยู่กับเรื่อง Concurrency เยอะ เลยถือโอกาสหยิบฟีเจอร์ใหม่กิ๊งอันนึงของ Java มาเล่าสู่กันฟัง นั่นคือ Virtual Threads ที่เปิดตัวเป็น Preview Features มากับ Java 19 ซึ่งขณะที่ผมเขียนบทความอยู่นี้ JDK19 ได้ออกเป็น Early Access (EA) Version มาให้ลองชิมกันแล้ว ส่วนตัว General Availability (GA) Version นั้นมีแผนจะออกวันที่ 20 กันยายน 2022 ที่จะถึงนี้

Virtual Threads ใน JDK19 เป็น Preview Feature ลองได้ เรียนได้ แต่ไม่แนะนำให้ใช้บน Production นะครับ

ย้อนกลับไปหลายสิบปีที่แล้ว Java มีอะไรให้ตื่นเต้นบ่อยหน่อย มี Generics ใน Java 5 มี Streams และ Lambda ใน Java 8 หลังจากนั้นมายาวๆ ผมก็รู้สึกเฉยๆ กับ Language Features ของ Java รุ่นใหม่เอามากๆ ที่รู้สึกว้าวจริงก็จะฉีกไปที่ Native Java บน GraalVM เลย แต่ล่าสุดผมมาสะดุดกับ Virtual Threads ที่มาโผล่ใน Java 19 นี่แล

ที่น่าตื่นเต้นไม่ใช่เพราะ Virtual Threads เป็นของใหม่นำเทรนด์ ใครที่เขียน Go หรือ Kotlin จะรู้ว่า Concurrency Programming ของสองภาษานี้มีความสามารถเหนือล้ำในแบบที่ Java ยังไม่มี เราก็รออยู่ว่าเมื่อไหร่ Java จะมีอะไรแบบนี้มาสักที ผมว่าทีมงานที่พัฒนาภาษา Java เขาก็มองเห็นอยู่นะครับ จึงมีการจัดตั้งโปรเจคชื่อ Project Loom ขึ้นมาเพื่อโฟกัสกับการปรับปรุงเรื่อง Concurrency โดยเฉพาะ ถ้าผมจำไม่ผิด ผมได้ยินชื่อโปรเจคนี้ตั้งแต่ 3–4 ปีที่แล้ว แต่กว่าจะได้ยลโฉมผลงานอันเป็นรูปธรรมหน่อยก็รอเอานานหลายปีอยู่ทีเดียว ความตื่นเต้นอยู่ที่ว่าเจ้าสิ่งนี้จะนำพา Java ให้มีประสิทธิภาพขึ้นได้มากขนาดไหน เพราะนอกจาก Developers จะเขียนโปรแกรมและรันโปรแกรมได้มีประสิทธิภาพขึ้นแล้ว ภายในตัว JVM เองก็มีส่วนที่สำคัญที่ทำงานในลักษณะของ Multi-Threading อยู่ด้วย เช่น Garbage Collector และ Just-In-Time Compiler เป็นต้น

ลองเล็งดูว่าถ้าออกเป็น Preview Feature มาที่ Java 19 กว่าจะ Final ผมว่าก็ไม่น่าเร็วกว่า Java 21 แน่ (ประมาณปลายปี ค.ศ. 2023) และถ้า Final ที่ Java 21 ได้จริงจะดีมาก เพราะ Java 21 น่าจะเป็น Long-Term Support (LTS) Version พอดีอีกด้วย

ที่ผมไปดูๆ มา Project Loom จะมีงานหลักอยู่ 3 งานด้วยกัน คือ

  • Virtual Threads
  • Delimited Continuations
  • Tail-Call Elimination

ส่วนบทความนี้ก็ตามชื่อเลยครับ เป็นเรื่องของ Virtual Threads

จากอดีตสู่ปัจจุบัน

สมัย Java เป็นเบบี๋เพิ่งเกิด เหมือนจะมีความคิดที่จะทำให้ Java Threads หลายๆ อันทำงานขี่อยู่บน OS Thread อันเดียว (มีคำเรียก Java Threads ในแนวคิดนี้ว่า Green Threads และเราเรียก OS Thread ได้อีกอย่างว่า Platform Thread) ซึ่งแนวทางนี้ได้ถูกยกเลิกไปแถวๆ ยุค Java 1.2–1.3 หลังจากนั้นมากลายเป็นว่า Java Thread อันนึงก็จะขี่อยู่บน OS Thread อันนึงแบบ 1 ต่อ 1 ซึ่งบางคนจะเรียก Java Thread ว่าเป็น Wrapper ของ OS Thread กันเลยทีเดียว

นั่นแปลว่าเวลาเราเขียนโปรแกรมเรียกเมธอด Thread.start() ก็จะได้ OS Thread ขึ้นมาหนึ่งอันนั่นเอง

ผมหาข้อมูลไม่ได้ว่าทำไมในอดีตแนวคิดของ Green Threads ถึงได้ถูกยกเลิกไป แต่ในปัจจุบันชัดเจนว่า Java Threads นั้น Heavyweight เพราะเวลาจะทำอะไรทีจะต้องไปทำกับ OS Thread ด้วย การ Block Java Threads ก็คือการ Block Platform Threads ซึ่งมีขั้นตอนยุ่งยากและใช้เวลามาก อีกอย่างหนึ่งคือการสร้างและจัดการ Platform Threads เป็นภาระของ OS ที่ไม่สามารถสร้างจำนวนมากๆ ได้ ความสามารถด้าน Scalability ก็น้อยตามไปด้วย ไม่สอดคล้องกับสเปคคอมของเราที่โคตะระแรงเอาเสียเลย ทางออกหนึ่งในปัจจุบันก็คือการเขียนโปรแกรมแบบ Reactive ซึ่งผมว่า Reactive Programming อ่านเข้าใจยากนะครับ เทสก็ยาก แต่ก็นะครับ มันจำเป็นเพราะเราอยากได้ประสิทธิภาพมาก่อน

ใครที่มีประสบการณ์ทำ Web-based Application ด้วย Java ใช้งานบน Web Servers มา คงจะนึกออกว่าเมื่อไหร่ที่อยากจะ Scale ต้องวิ่งหาสิ่งที่เรียกว่า Thread Pool นั่นเพราะ Web Server ใช้หนึ่ง Threads ต่อหนึ่ง Connection ที่เข้ามา เวลาที่เราต้องการรองรับ Concurrent Requests เยอะๆ ก็จะทำได้ยาก เพราะจำนวน Threads ที่สร้างได้พร้อมๆ กันมันน้อย ต้องอัดสเปคเครื่องเพิ่ม อัดเข้าไป อัดเข้าไป เวลาจะสร้าง Thread ใหม่ทีก็ช้า ต้องทำเป็น Pool ไว้เอากลับมาใช้ใหม่ ทั้งหมดทั้งมวลก็เพราะ Java Threads ไปฟีเจอริ่งตัวต่อตัวกับ OS Threads โดยตรง

ถ้าไปดูใน Java Language Specification จริงๆ แล้วไม่ได้มีการกำหนดตายตัวว่าระหว่าง Java Threads กับ OS Threads จะต้องเป็น 1 ต่อ 1 เท่านั้น

ใช่ครับ Project Loom คือการกลับมาทำสิ่งที่คล้ายๆ กับ Green Threads ในอดีต

Hello, Virtual Threads

ตอนแรกๆ เราอาจจะเคยได้ยินคำว่า Fiber มาก่อน สุดท้ายก็กลายมาเป็นชื่อ Virtual Thread นี่แหละครับ โดยหัวใจของ Virtual Threads คือการถูกบริหารจัดการโดย Java Runtime เองเกือบทั้งหมด (ไม่ใช่ส่งต่อไปที่ OS เหมือนเดิมละ) ดังนั้นทุกอย่างจะเบาสบายทันที แต่อย่างที่หลายคนพอเดาได้ ยังไง Virtual Threads สุดท้ายก็ยังต้องอาศัย OS Threads หรือ Platform Threads อยู่ดี แต่จะไม่ใช่แบบ 1–1 อีกต่อไป (และเราไม่ต้องไปสนใจแล้วครับว่ามันจะเป็นเท่าไหร่) ดังนั้นปัญหา Scalability หรือการเพิ่มจำนวน Threads ที่เดิมทำได้น้อยจะถูกปลดล็อกทันที!

ผมว่าความยากของทีม Project Loom คือการสร้างของใหม่ให้สอดคล้องกับของเก่า และต้องรักษาโค้ดเก่าของพวกเราไม่ให้ Break อีกด้วย ผมไปดู Source Codes ที่เกี่ยวกับ Virtual Thread เห็นว่ามีการใช้ Sealed Class ใช้ Modularization ใช้ฟีเจอร์ของ Java ในยุคใหม่ๆ หลายอย่าง จึงพอเดาได้ว่าทำไมงาน Project Loom ถึงใช้เวลาหลายปี นอกจากความซับซ้อนในงานออกแบบแล้ว ยังต้องรอฟีเจอร์อีกหลายตัวให้ไฟนอลก่อนถึงจะมีประสิทธิภาพ

ในเชิงของ Developer’s Experience ถ้าโค้ดปัจจุบันของเรามีการใช้งาน Threads อยู่ โค้ดนั้นก็จะยังทำงานได้ในแบบของ Java Threads เดิมที่เป็น 1–1 Wrapper over OS Thread เด๊ะๆ แต่ถ้าเราอยากเปลี่ยนมาใช้ Virtual Threads ก็จะต้องแก้โค้ดกันนิดหน่อย คือเราต้องเปลี่ยนมาใช้วิธีใหม่ในการสร้าง Virtual Threads ครับ แล้วหลังจากนั้น Programming Model แทบจะเหมือนเดิมเลย ไม่ว่าจะเป็นการใช้ Thread-local Variables, Synchronized Blocks, Interruption ฯลฯ โดยโค้ดส่วนอื่นจะไม่รับรู้ถึงความแตกต่างเลยว่ารันอยู่บน Virtual Threads หรือ Traditional Java Threads

มาลองดูโค้ดกันครับ ด้านล่างผมลองสร้าง Threads ทั้งสองแบบมาเปรียบเทียบกัน โดยวิธีการสร้างจะใช้ Factory Methods ใหม่ของ Java 19 นะครับ ถ้าใครลองรันในเครื่องตัวเองอย่าลืมรันแบบ Enable Preview ด้วยนะครับ

ThreadCreation.java

จากโปรแกรมด้านบน ผมสร้าง Threads 2 แบบ แล้วสั่งพิมพ์ String Representation มาดูเปรียบเทียบกัน รันในเครื่องผมเองได้ผลแบบรูปด้านล่าง บรรทัดแรกเป็น Traditional Java Thread แบบเดิม ส่วนบรรทัดที่สองเป็น Virtual Thread ครับ

ผลการรัน ThreadCreation.java บนเครื่องผมเอง

สังเกตบรรทัดที่สองที่เป็นของ Virtual Thread กันครับ จะเห็นว่าเราเขียนโปรแกรมใช้งานผ่านคลาส java.lang.Thread เหมือนเดิม แต่คลาสจริงๆ ของมันจะเป็น java.lang.VirtualThread ซึ่งเป็น Internal Class ภายใน Java ครับ แล้ว String ที่พิมพ์ออกมาจะมี 2 ส่วนด้วยกัน คือส่วนของ Virtual Thread และส่วนที่สะท้อนไปถึง Platform Thread ผ่านการทำงานของ ForkJoinPool

ผมจะลองแก้โปรแกรมให้สร้างหลายๆ Threads ดู ใช้การก๊อปปี้โค้ดรัวๆ เท่านั้นเองครับ รันแล้วได้ผลแบบด้านล่าง

ลองแก้ไขให้สร้าง Threads หลายๆ อัน

ผมสร้าง Virtual Threads ขึ้นมา 5 อันแล้วส่งไปพิมพ์ จะเห็นว่า Virtual Thread เบอร์ #29 ไปทำงานอยู่บน Platform Thread ชื่อ worker-1 และ Virtual Threads อีก 4 อันที่เหลือไปทำงานอยู่บน worker-2 ซึ่งทั้ง worker-1 และ worker-2 ถูกบริหารจัดการโดย ForkJoinPool-1 ทั้งคู่ เป็นไปตามคอนเซ็ปต์ที่ Virtual Threads ไม่ได้อยู่บน Platform Thread แบบ 1–1 อีกต่อไป

ศัพท์เทคนิค

เวลาผมอ่านบทความเกี่ยวกับ Virtual Threads จะเจอศัพท์ใหม่ที่ไม่ได้เจอในโลก Traditional Java Threads แบบเดิมๆ

Carrier Threads

ใช้เรียก Platform Threads ที่รัน Virtual Threads ครับ โดยคำว่า “Carrier” คงจะสื่อถึง Platform Thread ที่สามารถบรรทุกหลายๆ Virtual Threads ได้ ไม่ใช่ความสัมพันธ์ 1–1 แบบดั้งเดิม

Mounting และ Unmounting

Mounting คือกระบวนการที่ Java Runtime ทำการส่ง Virtual Threads ให้ไปทำงานบน Platform Thread และการ Unmounting คือกระบวนการย้อนกลับของการ Mounting ครับ

การ Unmounting คือการถอดการทำงานของ Virtual Threads ออกจาก Platform Thread ตัวอย่างง่ายๆ ของการ Unmounting คือเวลาที่ Virtual Thread ต้องรอ Blocking I/O เป็นต้น Java Runtime ก็จะทำการ Unmounting Virtual Thread นั้นออก และทำการ Mounting Virtual Thread อันใหม่ (ถ้ามี) ลงไปทำงานแทน จะเห็นว่าแบบนี้ Platform Threads ข้างใต้จะไม่เกิดการ Block เลย เป็นการใช้งาน Platform Threads ได้อย่างคุ้มค่ากว่าเดิม ซึ่งพอ Blocking I/O ทำงานเสร็จ Java Runtime ก็จะทำการ Mounting Virtual Thread อันนั้นใหม่อีกครั้ง โดยครั้งนี้ Virtual Thread อาจจะไปทำงานอยู่บน Platform Thread อันอื่นที่ไม่ใช่อันเดิมตอนแรกก็ได้

ลองเขียนโปรแกรมทดสอบดู

จากโค้ดด้านบน ผมสร้าง Virtual Threads ขึ้นมา 10 อัน โดยวิธีง่ายๆ ที่จะทำการเกิดการ Unmounting และ Mounting ใหม่คือการสั่ง Thread.sleep นั่นเองครับ ทีนี้ผมก็เก็บค่า String Representation ก่อนและหลัง Sleep ไว้เพื่อที่จะมาดูว่ามีการย้าย Platform Threads จริงหรือไม่ ไปรันกันโลด

ผลการรัน VirtualThreadMounting.java

จะเห็นว่าหลังจากการ Sleep มีการ Mounting ไปที่ Platform Thread ใหม่ซึ่งอาจจะไม่ใช่ตัวเดิมจริงด้วย ผมแอบไปไฮไลท์สีไว้ให้จะได้ไม่ตาลายครับ

กระบวนการ Mounting และ Unmounting ทำให้การ Block Virtual Threads นั้นใช้ทรัพยากรน้อยกว่าการ Block Platform Threads มาก และยังช่วยลดโอกาสที่จะเกิดการ Block Platform Threads ให้น้อยที่สุด จึงเป็นที่มาของประสิทธิภาพที่เพิ่มขึ้นอย่างมาก

Pinning

เราเห็นแล้วว่า Virtual Threads นั้นไม่ได้ทำงานตายตัวอยู่บน Platform Threads อันใดอันหนึ่ง มันถูกย้ายไปมาได้ด้วยการบริหารจัดการของ Java Runtime แต่ก็มีกรณีที่ Virtual Threads ไม่สามารถย้ายข้าม Platform Threads ได้ด้วยครับ คือมันจำเป็นที่จะต้องผูกติดอยู่กับ Platform Thread อันเดิมตลอด ซึ่งเราเรียกว่า Pinning

การที่ Virtual Threads โดน Pinned มีผลให้มันกลับไปเหมือนกับ Java Threads ยุคเก่าที่รักเดียวใจเดียวกับ OS Threads ซึ่งแน่นอนว่าประสิทธิภาพก็ลดลงไปด้วย แล้วถามว่าสถานการณ์ไหนที่ Java Runtime ต้องตัดสินใจทำ Pinning บ้าง?

เคสแรกคือกรณีที่โปรแกรมภาษา Java ต้องไปเรียกภาษาอื่นที่ Low Level มากๆ เช่นภาษา C ที่สามารถเข้าถึง Memory Addresses ได้ (นึกถึงวันวานกับการใช้งาน &variable ได้เลยครับ) การที่ Java Runtime จะย้าย Virtual Threads ที่ทำงาน Low Level ลักษณะนี้มีความเสี่ยงในที่จะทำให้ค่าของข้อมูลใน Addresses เหล่านี้ไม่ถูกต้อง ทีมงานจึงต้องทำ Pinning แบบไม่มีทางเลือก กรณีนี้ผมคิดเองว่าน่าจะเป็นข้อจำกัดที่ Java มาทำ Virtual Threads ทีหลังด้วย ถ้าออกแบบไว้ตั้งแต่แรกน่าจะทำออกมาได้ดีกว่านี้ ที่ไปอ่านบล็อกของ Oracle มาเห็นบอกว่าทีมงานมีการแก้ไขนำโค้ดภาษา C ใน Java 19 หลายจุดออกไป และเขียนใหม่ให้เป็นภาษา Java แทน

เคสที่สองคือ Virtual Threads ที่ทำงานแล้วเจอ Synchronized Block ก็จะโดน Pinned เช่นกันครับ ด้วยสาเหตุที่ Synchronized Block มีการทำงานที่ต้องใช้ Address บน Stack คล้ายกับเคสของภาษา C เคสนี้เราลองเขียนโปรแกรมเทสเองได้ง่ายๆ

PinningWithSynchronizedBlock.java

ผลการรันบนเครื่องผมครับ

ผลการรัน PinningWithSynchronizedBlock.java บนเครื่องผมเอง

จากโปรแกรมด้านบนที่ผมสร้าง Virtual Threads ขึ้นมา 1,000 อัน แต่ละ Thread จะทำการเพิ่ม Counter ทีละหนึ่งสามครั้งใน Synchronized Block แล้วผมก็พยายามดักก่อนเข้าและหลังเข้า Synchronized Block เพื่อดูว่าชื่อ Platform Threads มีการเปลี่ยนหรือไม่ จากการทดสอบเราจะเห็นได้เลยว่าไม่มีการเปลี่ยนของชื่อ Virtual Threads (ซึ่งหมายถึงแต่ละ Virtual Threads ถูก Pinned ไว้ทั้งหมด) จนจบการทำงานที่ทุก Threads ทำการเพิ่ม Counter จนครบได้ค่าสุดท้ายเป็น 3,000 ในเวลา 26 มิลลิวินาที

ภายใต้การทำงานของ Virtual Threads

ทีมงานเขาบอกว่า Virtual Threads ถูกพัฒนาอยู่บนสองสิ่งนี้

ForkJoinPool

ForkJoinPool เป็นหัวใจของ Fork/Join Framework ที่ออกมาตั้งแต่ Java 7 แล้ว โอ้วววววววววววว ผมไม่ได้ตั้งใจจะมาประชาสัมพันธ์เรื่องฟอร์คจ๋อยในบทความนี้ เอาสั้นๆ ว่ามันคือ ExecutorService ที่ Java เขาตั้งใจออกแบบใหม่เพื่อให้มันเจ๋ง ด้วยความสามารถที่เรียกว่า Work-Stealing ใครสนใจก็ไปศึกษากันต่อได้ครับ ซึ่ง Java ได้นำ ForkJoinPool มาใช้เป็น Scheduler ในการบริหารจัดการ Virtual Threads เชื่อมต่อไปยัง Platform Threads นั่นเอง

Continuation

สำหรับใครที่เคยเขียน Kotlin ให้นึกถึง Coroutine หรือ Suspend Functions ใน Kotlin เลยครับ เท่าที่ผมเห็นตอนนี้ Continuation ใน Java อธิบายง่ายๆ คือสิ่งที่มาช่วยให้เรารันงานแบบทำๆ หยุดๆ แล้วกลับมาทำต่อได้ครับ ลองดูโค้ดด้านล่างจะช่วยให้เข้าใจมากขึ้น (หรือเปล่า) ถ้าใครอยากรันต้องใส่ Options เพิ่มนะครับ เพราะตัว Continuation ในโค้ดด้านล่างยังเป็น Internal Package ของ JDK อยู่ครับ

ContinuationYield.java

รันแล้วได้ผลแบบนี้เงยยย

ผลการรัน ContinuationYield.java บนเครื่องผม

ผมว่า APIs ในการใช้งานดูยากๆ อยู่เหมือนกันครับ แต่ช่างมันก่อน ลองไล่ดูจะพบว่าตอนเราสั่ง run() ในบรรทัดที่ 20 มันจะไปเริ่มทำงานในฟังก์ชันที่เราใส่เข้าไปตอนสร้าง Continuation Object ทำไปเรื่อยๆ พอเจอเมธอด yield(ContinuationScope) ก็จะหยุดแล้วส่ง Control กลับมาที่เมธอด main แล้วพอเจอ run() อีกครั้งหนึ่งที่บรรทัดที่ 22 ก็จะกลับไปทำงานที่ค้างไว้ต่อประมาณนี้ครับ

นอกจาก Continuation จะมีบทบาทอยู่เบื้องหลัง Virtual Threads แล้ว เมื่อไหร่ที่เปิดใช้งานแบบ Public (ย้อนไปดูภารกิจข้อที่ 2 ของ Project Loom ด้านบนได้ครับ) เราจะสามารถเขียน Java ในรูปแบบใหม่ๆ ได้อีกเยอะ

ศึกษาเพื่อใช้งาน

แม้ว่าทีมงาน Java เขาทำการบ้านมาอย่างดีเพื่อให้การมาของ Virtual Threads มีผลกระทบกับ Programming Model เดิมน้อยที่สุด แต่หากเราอยากได้พลังแห่ง Virtual Threads ที่แท้จริง ผมแนะนำให้ศึกษาและทำความเข้าใจพฤติกรรมการทำงานของ Virtual Threads ให้ลึกมากขึ้น ซึ่งแน่นอนว่ามันจะมีผลต่อมุมมองในการเขียนโค้ดของเรา ตอนนี้ผมพยายามหาข้อมูลมาสรุปไว้ในบทความนี้เท่าที่ทำได้ แต่อย่าลืมว่าพอ Virtual Threads ออกเป็น Final จริงๆ แล้ว เราน่าจะมีรายละเอียดที่มากกว่านี้ และบางอย่างอาจจะเปลี่ยนแปลงไปก็ได้

ไม่ควรสร้าง Threads ด้วยการ Extends java.lang.Thread ละ

ก่อนหน้านี้ในตำรามักจะสอนว่าวิธีหนึ่งในการสร้าง Java Thread คือให้ไป Extend java.lang.Thread แล้วทำการ Override run() method พอ Virtual Threads ออกมา ผมว่าควรจะต้องเปลี่ยนให้เหมาะสมแล้ว เพราะการใช้วิธี Extends จะทำให้เราได้ Traditional Java Thread แบบเก่าที่เป็น 1–1 กับ OS Thread ครับ วิธีที่ยืดหยุ่นและใหม่กว่าคือการไปใช้ Static Factory Methods หรือ Thread Builders แบบที่ใช้ในโค้ดตัวอย่างแทน

ไม่ต้อง Pool Virtual Threads แล้ว

อย่างที่กล่าวไปแล้วด้านบนครับ Virtual Threads เบาสบาย ไม่ต้องไปพยายามบริหารจัดการด้วยการสร้าง Thread Pool ใดๆ เหมือนกับ Traditional Java Threads ก่อนหน้านี้

Virtual Threads ไม่ได้ทำให้โค้ดของคุณเร็วขึ้น

จริงๆ แล้ว Virtual Threads ช่วยให้เราใช้ Resources อย่างมีประสิทธิภาพมากขึ้น พอ Throughput ของเราเพิ่มขึ้น Scale ได้ดีขึ้น ก็เลยเหมือนระบบงานของเราเร็วขึ้นนั่นเอง 555

ลองมาทดสอบจำนวน Virtual Threads ที่สร้างได้ในเครื่องผมดูหน่อย

VirtualThreadPerformance.java

ผลลัพธ์จากการรัน 1,000,000 Virtual Threads ในเครื่องผมเป็นดังรูปด้านล่างครับ เครื่องผมมี 4 Cores ใช้ Platform Threads แค่ 4 อันรองรับ Virtual Threads ระดับ 1,000,000 อันได้อย่างชิลมากๆ ใช้เวลาไปหนึ่งวินาทีกว่าๆ โดยในแต่ละ Thread ทำงานแค่ตัดชื่อของตัวเองด้วย Regular Expression มาใส่เก็บไว้ใน HashSet เพื่อเอาไปแสดงเป็นสถิติในตอนท้ายครับ

ผลการรัน VirtualThreadPerformance.java บนเครื่องผมเอง

ถามว่าเพิ่มเป็นร้อยล้านได้ไหม ผมลองเปรี้ยวดูแล้วปรากฏว่า Default Heap Size ไม่พอครับ ต้องไปขยาย Heap Size ตอนรันด้วย เพราะสุดท้ายการสร้าง Objects ใดๆ ก็ต้องใช้ Heap Memory อยู่ดีครับ แต่ถ้าเป็น Traditional Java Thread แบบเดิม น่าจะตายตั้งแต่หลักพันหรือหลักหมื่นแล้ว

หลีกเลี่ยงการโดน Pinning ได้ถ้าจำเป็น

จากตัวอย่างโค้ดที่ผ่านมา เราเห็นแล้วว่า Virtual Threads ที่รันโค้ดที่มี Synchronized Block จะโดน Pinned แต่ถ้าเราไม่ได้ต้องการ Performance อย่างยิ่งยวด ถามว่าต้องแก้โค้ดไหม? ตอบได้เลยว่าไม่จำเป็นครับ แต่ถ้าการ Block นั้นเกิดขึ้นบ่อยมาก และเกิดขึ้นทีนึงเป็นระยะเวลานานๆ การโดน Pinned ก็ทำให้สิ้นเปลืองทรัพยากรเหมือนเรากลับไปอยู่โลก Java Threads แบบเดิมๆ หากเราต้องการที่จะใช้ Virtual Threads ให้ได้ประโยชน์จริงๆ ต้องทำอย่างไร?

ทีมงาน Java เขาแนะนำว่าให้เราไปใช้ Locks แทน Synchronized Block ครับ งั้นมาลองดูกัน

PinningWithReentrantLock.java

ผมนำไฟล์ PinningWithSynchronizedBlock.java มาแก้ไขโดยเปลี่ยนการใช้ Synchronized Block เป็นการใช้ ReentrantLock รวมไปถึงเพิ่มจำนวน Virtual Threads ให้เยอะขึ้นหน่อยเป็น 4,000 อัน จะเห็นว่าผลลัพธ์ที่ได้ เราจะสามารถตรวจเจอการเปลี่ยนของ Platform Threads ภายใต้ Virtual Threads ได้แล้ว แต่จำนวนการเปลี่ยนจะไม่แน่นอน บางครั้งน้อย บางครั้งเยอะ บางทีอาจจะไม่เปลี่ยนเลย แล้วแต่ใจ Java Runtime เขาเลยครับ รวมไปถึงถ้าให้จำนวน Virtual Threads เท่ากัน ดูเหมือนการใช้ ReentrantLock จะทำงานได้เร็วกว่าการใช้ Synchronized Block ด้วย

ผลการรันโปรแกรม PinningWithReentrantLock.java บนเครื่องผมเอง

ผมลองเปลี่ยนไปใช้ StampedLock ดู ก็ยังได้ผลเหมือนกับ ReentrantLock ครับ

นอกจากนี้มีอะไรอื่น ๆ อีกไหม

นอกจากตัว Virtual Threads เอง อีกหัวข้อหนึ่งที่น่าสนใจมากๆ คือ Structured Concurrency ที่เป็นการนำองค์ความรู้ในเรื่อง Virtual Threads มาใช้ในกับงาน Asynchronous Programming รวมกับเรื่อง Continuation เราจะได้เห็นการเขียนโปรแกรม Java ในรูปแบบใหม่ที่ทันสมัยและง่ายขึ้นกว่าเดิม ไว้มีโอกาสจะมาเล่าสู่กันฟังต่อไปครับ

ขอขอบคุณตัวอย่างโค้ดจากบล็อกของ Oracle และ OpenJDK Project Loom มา ณ ที่นี้ด้วย

Happy Coding!

สำหรับชาวเทคคนไหนที่สนใจเรื่องราวดีๆแบบนี้ หรืออยากเรียนรู้เกี่ยวกับ Product ใหม่ๆ ของ KBTG สามารถติดตามรายละเอียดกันได้ที่เว็บไซต์ www.kbtg.tech

--

--

jo@sabotender
KBTG Life

principal DEVelopment eXcellence engineer — DEVX@KBTG / Full-time Daddy / Console Gamer & Gunpla Collector