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

ค้นหาใน CooLWJ

选择搜索范围,输入关键词后按回车。

ภาษา

มองเทคโนโลยีจากภาพรวม-ประวัติวิวัฒนาการมัลติเธรดของ Java

คำนำ

วันที่ 22 เดือน 09 ปี 2022 JDK19 เปิดตัวแล้ว จุดเด่นที่สุดของรุ่นนี้คือการรองรับเธรดเสมือน ทำให้ตระกูลเธรดน้ำหนักเบามีสมาชิกสำคัญเพิ่มขึ้นอีกหนึ่งราย เธรดเสมือนช่วยให้ JVM หลุดพ้นจากข้อจำกัดของการใหัระบบปฏิบัติการจัดตารางเธรด โดย JVM จะจัดตารางเธรดเอง ที่จริงแล้ว ในยุคแรก Sun เคยนำการจัดตารางเธรดโดย JVM มาใช้ในเครื่องเสมือนบนระบบปฏิบัติการ Solaris แต่เมื่อพิจารณาถึงความซับซ้อนและความสะดวกในการบำรุงรักษา สุดท้ายจึงกลับไปใช้รูปแบบที่ให้ระบบปฏิบัติการจัดตารางเธรด

“长安归来锦衣客,昨日城南起新宅。” ผู้กลับจากฉางอานในอาภรณ์หรู เมื่อวานเพิ่งสร้างเรือนใหม่ทางใต้ของเมือง เมื่อย้อนคิดถึงเส้นทางที่ผ่านมา แนวคิดเรื่องมัลติเธรดชวนให้ตาลาย และคำอธิบายที่เกี่ยวข้องบนอินเทอร์เน็ตก็มีมากมายนับไม่ถ้วน แต่กลับรู้สึกเสมอว่ายังขาดมุมมองภาพรวม ด้วยเหตุนี้ ผู้เขียนจึงเรียบเรียงประวัติวิวัฒนาการของมัลติเธรดใน Java อย่างเป็นระบบ หวังว่าจะช่วยให้คุณเข้าใจความรู้เรื่องมัลติเธรดได้ดีขึ้น

สิ่งที่บทความนี้จะไม่กล่าวถึง:

1 ไม่อธิบายหลักการทำงานโดยละเอียดของประเด็นทางเทคนิคบางอย่าง ไม่แยกวิเคราะห์ซอร์สโค้ด และไม่วาดแผนภาพ หากพบแนวคิดหรือเทคโนโลยีที่สนใจจากบทความนี้ สามารถค้นหาต่อเองได้ 2 ไม่กล่าวถึงไลบรารีและเฟรมเวิร์กที่รองรับการทำงานพร้อมกัน เช่น Quasar、Akka、Guava เป็นต้น

บทความนี้กล่าวถึงอะไร

1 อธิบายประวัติวิวัฒนาการของมัลติเธรดใน JDK 2 อธิบายหน้าที่ หลักการทำงาน และภูมิหลังของประเด็นทางเทคนิคบางอย่างในวิวัฒนาการนั้น รวมถึงปัญหาที่แก้ไขได้ 3 กล่าวถึงมุมมองของผู้เขียนต่อประเด็นทางเทคนิคบางอย่าง ยินดีให้ผู้ที่มีมุมมองต่างกันมาพูดคุยในส่วนความคิดเห็น

หมุดหมายสำคัญ

ตามธรรมเนียมเดิม เริ่มด้วยตารางสรุปก่อน ตารางนี้รวบรวมแนวคิดหลักเกี่ยวกับเธรดใน JDK แต่ละยุคไว้ ในที่นี้ขอเปรียบเทียบ ซึ่งอาจไม่เหมาะนัก โดยเทียบวิวัฒนาการของมัลติเธรดกับรถยนต์ ได้แก่ ยุคเกียร์ธรรมดา(JDK1.4 และต่ำกว่า) ยุคเกียร์อัตโนมัติ(JDK5-JDK18) และยุคขับเคลื่อนอัตโนมัติ(JDK19 และหลังจากนั้น) การเปรียบเทียบนี้มีไว้เพียงเพื่อบอกผู้อ่านว่า ตั้งแต่ JDK5 เป็นต้นมา เราควบคุมมัลติเธรดได้สะดวกขึ้น และตั้งแต่ JDK19 เป็นต้นมา ก็ไปไกลกว่าความสะดวกเพียงอย่างเดียว โดยทำให้ประสิทธิภาพของบริการที่เน้นงาน IO ก้าวกระโดดในเชิงคุณภาพ

ยุคสมัยเวอร์ชันเวลาเผยแพร่แนวคิดหลัก
เกียร์ธรรมดาJDK1.01996-01-23Thread และ Runnable
เกียร์ธรรมดาJDK1.21998-12-04ThreadLocal、Collections
เกียร์อัตโนมัติJDK1.5/5.02004-09-30กำหนดโมเดลหน่วยความจำของ Java ให้ชัดเจน และเพิ่มแพ็กเกจสำหรับการทำงานพร้อมกัน
เกียร์อัตโนมัติJDK1.6/6.02006-12-11การปรับปรุง synchronized ให้มีประสิทธิภาพ
เกียร์อัตโนมัติJDK1.7/7.02011-07-28เฟรมเวิร์ก Fork/Join
เกียร์อัตโนมัติJDK1.8/8.02014-03-18CompletableFuture、Stream
เกียร์อัตโนมัติJDK1.9/9.02014-09-08ปรับปรุงกลไกจัดการการแย่งล็อก
เกียร์อัตโนมัติJDK102018-03-21การประสานงานเฉพาะเธรด
เกียร์อัตโนมัติJDK152020-09-15ปิดใช้งานและเลิกใช้การล็อกแบบเอนเอียง
การขับขี่อัตโนมัติJDK192022-09-22เธรดเสมือน

2f98c71c0436418d8d260d75cb921c81~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF_q75.webp

ยุคเกียร์ธรรมดา

ผู้เขียนเรียกยุค JDK1.4 และต่ำกว่าว่ายุคมัลติเธรด “เกียร์ธรรมดา” หรือยุคมัลติเธรดดั้งเดิม การจัดการเธรดยังค่อนข้างเป็นแบบพื้นฐาน ไม่มีพูลเธรดให้ใช้ นักพัฒนาต้องเขียนเครื่องมือเองเพื่อหลีกเลี่ยงการสิ้นเปลืองทรัพยากรจากการสร้างเธรดบ่อย ๆ และล็อกทรัพยากรที่ใช้ร่วมกันด้วยตนเอง ยุคนี้เองที่บ่มเพาะเฟรมเวิร์กมัลติเธรดชั้นเยี่ยมมากมาย โดยเฟรมเวิร์กที่โด่งดังที่สุดถูกนำมาใช้ใน JDK5.0

JDK 1.0 Thread และ Runnable

JDK1.0 ที่ออกในเดือน 1 ปี 1996 ได้วางแบบจำลองเธรดพื้นฐานที่สุดของ Java ตั้งแต่เริ่มต้น และแบบจำลองเธรดนี้ก็ไม่ได้เปลี่ยนแปลงในสาระสำคัญตลอดการปรับปรุงแก้ไขในเวลาต่อมา จึงกล่าวได้ว่าเป็นการออกแบบที่ดีและสืบทอดต่อมาได้ การจัดตารางแบบแย่งใช้ทรัพยากรและแบบร่วมมือเป็นวิธีจัดตารางโพรเซส/เธรดที่พบบ่อยสองแบบ ระบบปฏิบัติการเหมาะอย่างยิ่งกับการใช้วิธีแย่งใช้ทรัพยากรเพื่อจัดตารางโพรเซส โดยแบ่งช่วงเวลาให้โพรเซสต่าง ๆ และสามารถริบทรัพยากรของโพรเซสที่ไม่ตอบสนองเป็นเวลานาน หรือแม้แต่บังคับให้หยุดได้ การใช้วิธีร่วมมือต้องอาศัยโพรเซสคืนทรัพยากรด้วยตนเองอย่างสมัครใจ ภายใต้วิธีจัดตารางเช่นนี้,เธรดที่ทำงานนานมากเพียงเธรดเดียวอาจทำให้เธรดอื่นทั้งหมดที่ต้องการ CPU ”อดตาย” ได้ เครื่องเสมือน Java hotspot ใช้วิธีจัดตารางแบบแย่งใช้ทรัพยากร ดังนั้นภาษา Java จึงใช้การจัดตารางเธรดแบบนี้มาตั้งแต่เริ่มต้น ใน JDK 1.0 วิธีสร้างเธรดหลักคือการสืบทอดคลาส Thread หรือใช้อินเทอร์เฟซ Runnable เริ่มเธรดผ่านเมธอด start ของอินสแตนซ์ออบเจ็กต์ วางโค้ดที่ต้องประมวลผลแบบขนานไว้ในเมธอด run ส่วนการประสานงานและสื่อสารระหว่างเธรดใช้เมธอดตรงไปตรงมาอย่าง stop/resume/suspend。

แผนภาพสถานะเธรด Java และการเปลี่ยนสถานะ

จะอธิบายแนวคิด stop/resume/suspend อย่างไร?ก็คือเธรดหลักสามารถเรียกเมธอดหยุด พัก และทำงานต่อของเธรดย่อยได้โดยตรง หากตอนเด็กคุณเคยใช้เครื่องเล่นเพลงพกพา บนนั้นมีปุ่มสามปุ่ม คือหยุด พัก และเล่นต่อ ลองจินตนาการว่าคุณกำลังฟังเครื่องเล่นเพลงพกพา3เครื่องพร้อมกัน เครื่องเล่นทั้งสามก็คือเธรดย่อยสามเธรด ส่วนคุณคือเธรดหลัก คุณสามารถควบคุมการเริ่มและหยุดของอุปกรณ์ทั้งสามได้ตามใจ。

กลไกชุดนี้มีปัญหาร้ายแรงอยู่อย่างหนึ่ง คือเกิดเดดล็อกได้ง่าย เพราะเมื่อเธรด A ล็อกทรัพยากรบางอย่างไว้และยังไม่ปล่อย ก็ถูกเธรดหลักพักการทำงานไว้(เมธอด suspend จะไม่ปล่อยล็อก) หากตอนนี้เธรด B ต้องการครอบครองทรัพยากรนี้ ก็ต้องรอให้เธรด A กลับมาทำงานต่อ(resume)แล้วปล่อยทรัพยากร มิฉะนั้นจะไม่มีวันได้ทรัพยากรนั้น และเกิดเดดล็อก。

JDK 1.2

กลไก stop/resume/suspend แบบหักดิบถูกห้ามใช้ในเวอร์ชันนี้ แล้วหันมาใช้วิธีให้หลายเธรดประสานงานกัน เช่น wait/notify/sleep สิ่งที่น่ากล่าวถึงคือ ในเวอร์ชันนี้ได้ออกแบบออบเจ็กต์อะตอมมิก AtomicityXXX ไว้แล้ว โดยหลักเพื่อแก้ปัญหา i++ ที่ไม่เป็นอะตอมมิก การเพิ่ม ThreadLocal และ Collections ทำให้มีวิธีใช้งานหลายเธรดมากขึ้น ด้วยเทคโนโลยีสองอย่างนี้ ผู้เขียนจึงเรียกช่วงนี้ว่ายุคเทอร์โบของ Java

ThreadLocal

ThreadLocal เป็นแนวทางที่ใช้วิธีไม่ล็อก เพื่อให้หลายเธรดใช้ออบเจ็กต์ที่ไม่ปลอดภัยต่อเธรดร่วมกันได้ มันไม่สามารถแก้ปัญหาอย่าง “การเพิ่มหรือลดยอดบัญชีธนาคารหรือสินค้าคงคลัง” แต่ถนัดในการทำให้คลาสที่มีคุณสมบัติเป็น “เครื่องมือ” เรียกใช้เมธอด “เครื่องมือ” ได้อย่างปลอดภัยด้วยการทำสำเนา ตัวอย่างทั่วไปคือ SimpleDateFormat และการเชื่อมต่อฐานข้อมูล เป็นต้น สิ่งที่น่ากล่าวถึงคือการออกแบบของมันชาญฉลาดมาก ลองจินตนาการว่าหากให้คุณออกแบบ แนวคิดง่าย ๆ โดยทั่วไปคือ:ดูแล Map ส่วนกลางที่ปลอดภัยต่อเธรดไว้ใน ThreadLocal โดยมี key เป็นเธรด และ value เป็นออบเจ็กต์ที่ใช้ร่วมกัน การออกแบบเช่นนี้มีข้อเสียคือปัญหาหน่วยความจำรั่ว เพราะ Map นี้จะขยายตัวไม่สิ้นสุดเมื่อมีเธรดเข้ามามากขึ้น หากจะแก้ปัญหาหน่วยความจำรั่ว ก็ต้องล้าง Map นี้เมื่อเธรดสิ้นสุด ซึ่งต้องเพิ่มความสามารถของ GC อีก เห็นได้ชัดว่าไม่คุ้มกับสิ่งที่ลงทุนไป ดังนั้น ThreadLocal จึงถูกออกแบบให้ ThreadLocal ไม่ใช่ผู้ถือ Map แต่ให้ Thread เป็นผู้ถือเอง โดย key เป็นตัวแปร ThreadLocal และ value เป็นค่า แต่ละ Thread จะเก็บ ThreadLoacl ที่ใช้งานไว้ในนั้น(แน่นอนว่าการออกแบบนี้ยังมีปัญหาอื่นที่ตามมา ซึ่งจะไม่กล่าวถึงที่นี่ ผู้ที่สนใจสามารถค้นหาเพิ่มเติมได้เอง)。

Collections

คลาสยูทิลิตี Collections ถูกออกแบบขึ้นในเวอร์ชันนี้ โดยห่อหุ้มคอลเลกชันที่ปลอดภัยต่อเธรดบางชนิด เช่น SynchronizedList ในยุคที่มีคอลเลกชันที่ปลอดภัยต่อเธรดเพียง Hashtable, Vector, Stack เป็นต้น การปรากฏตัวของมันก็มีความสำคัญต่อยุคสมัยเช่นกัน แนวคิดพื้นฐานของเครื่องมือ Collections คือฉันช่วยห่อหุ้มคอลเลกชันที่ไม่ปลอดภัยต่อเธรดให้กลายเป็นแบบปลอดภัยต่อเธรด ทำให้คุณไม่ต้องใช้เวลามากในการอัปเกรดและปรับปรุงโค้ดเดิม เพียงใช้เมธอดที่ฉันมีให้กำหนดค่าเริ่มต้นของคอลเลกชันตอนสร้างก็พอ ค่อนข้างคล้ายเทคโนโลยีเทอร์โบชาร์จของรถยนต์ ที่เพิ่มกำลังและแรงบิดของเครื่องยนต์โดยไม่เปลี่ยนความจุกระบอกสูบ ยุคเทอร์โบชาร์จของ Java มาถึงแล้ว ^\_^

ยุคเกียร์อัตโนมัติ

JDK 5.0

นำเข้าแพ็กเกจสำหรับการทำงานพร้อมกัน

Doug Lea มีชื่อภาษาจีนว่า 道格·利(เต้าเก๋อ·ลี่) เป็นอาจารย์มหาวิทยาลัยในสหรัฐอเมริกาและเป็นบุคคลระดับปรมาจารย์ J.U.C ก็มาจากฝีมือของเขา ก่อน JDK1.5 เราควบคุมการเข้าถึงโค้ดที่ต้องซิงโครไนซ์พร้อมกันได้ด้วย synchronized เท่านั้น ในเวลานั้นประสิทธิภาพของ synchronized ยังไม่ได้รับการปรับปรุงดีนัก จึงทำงานได้ไม่ดี ส่วนการควบคุมเธรดก็ใช้ได้เพียงเมธอด wait และ notify ของ Object ในช่วงนั้น Doug Lea ได้ยื่นข้อเสนอ JSR-166 ต่อ JCP ก่อนยื่น JSR-166 Doug Lea ใช้โค้ดที่มีความสามารถคล้ายแพ็กเกจ J.U.C มานานกว่าสามปีแล้ว โค้ดเหล่านี้ก็คือต้นแบบของ J.U.C。

J.U.C มีเครื่องมือหลักหลายประเภท เช่น ออบเจ็กต์อะตอมมิก ล็อกและชุดยูทิลิตี เธรดพูล และคอนเทนเนอร์ที่ปลอดภัยสำหรับเธรด นักพัฒนาสามารถเลือกใช้ความสามารถใดก็ได้อย่างยืดหยุ่นเพื่อสร้างผลิตภัณฑ์ของตน จะใช้ ReentrantLock สร้างเฟรมเวิร์กระดับล่าง หรือจะใช้เครื่องมือหรือคอนเทนเนอร์สำเร็จรูปเขียนโค้ดธุรกิจโดยตรงก็ได้ เมื่อมองจากมุมประวัติศาสตร์ J.U.C ในปี 2004 เรียกได้อย่างไม่มีข้อโต้แย้งว่าเป็น “ผลิตภัณฑ์เทคโนโลยีล้ำสมัย” และมีคุณูปการอย่างยิ่งต่อการแพร่หลายของ Java ยุคเกียร์อัตโนมัติของ Java มาถึงแล้ว เช่นเดียวกับรถเกียร์อัตโนมัติที่ทำให้การขับรถง่ายขึ้น J.U.C ช่วยลดอุปสรรคในการใช้มัลติเธรดของโปรแกรมเมอร์อย่างมาก นี่คือผลิตภัณฑ์ที่เปิดยุคสมัยใหม่

แน่นอนว่า J.U.C ก็มีข้อบกพร่องบางประการเช่นกัน:

ใช้ทรัพยากร CPU สูง:หากการวนรอด้วย CAS ไม่สำเร็จเป็นเวลานาน จะสร้างภาระให้ CPU อย่างมาก。

แนวทางแก้ไข:บางส่วนใน JUC จำกัดจำนวนรอบการหมุนรอ CAS เช่น SynchronousQueue ของ BlockingQueue。

ปัญหา ABA:หากค่าเดิมเป็น A เปลี่ยนเป็น B แล้วเปลี่ยนกลับเป็น A อีกครั้ง เมื่อ CAS ตรวจสอบจะพบว่าค่าไม่เปลี่ยนแปลง แต่ในความเป็นจริงค่าได้เปลี่ยนไปแล้ว นี่คือปัญหา ABA โดยส่วนใหญ่ปัญหา ABA ไม่ส่งผลต่อความถูกต้องของการทำงานพร้อมกันในโปรแกรม

วิธีแก้ปัญหา:เพิ่มหมายเลขเวอร์ชันให้ตัวแปรทุกตัว และเพิ่มขึ้น 1 ทุกครั้งที่มีการเปลี่ยนแปลง กล่าวคือ A —> B —> A จะกลายเป็น 1A —> 2B —> 3A Java มี AtomicStampedReference ไว้แก้ปัญหานี้ AtomicStampedReference ทำเครื่องหมายเวอร์ชัน(stamp)ให้กับอ็อบเจ็กต์ด้วยการห่อหุ้มทูเพิล [E,Integer] จึงหลีกเลี่ยงปัญหา ABA ได้。

รับประกันได้เพียงการดำเนินการแบบอะตอมกับตัวแปรที่ใช้ร่วมกันหนึ่งตัว:กลไก CAS รับประกันได้เพียงการดำเนินการแบบอะตอมมิกกับตัวแปรหนึ่งตัว แต่ไม่อาจรับประกันความเป็นอะตอมมิกของโค้ดทั้งบล็อกได้

วิธีแก้ไข:ตัวอย่างเช่น หากต้องรับประกันว่าตัวแปร 3 ตัวจะอัปเดตพร้อมกันแบบอะตอม ก็จำเป็นต้องใช้ Synchronized นอกจากนี้ยังอาจพิจารณาใช้ AtomicReference ห่อหุ้มตัวแปรหลายตัว เพื่อจัดการกรณีที่มีตัวแปรที่ใช้ร่วมกันหลายตัวด้วยวิธีนี้。

ทำความเข้าใจโมเดลหน่วยความจำ Java ให้ชัดเจน

JDK รุ่นนี้กำหนดโมเดลหน่วยความจำของ Java ให้ชัดเจนขึ้นใหม่ ก่อนหน้านี้ โมเดลหน่วยความจำที่พบทั่วไปได้แก่โมเดลความสอดคล้องแบบลำดับและโมเดล happens-before สำหรับโมเดลความสอดคล้องแบบลำดับ ลำดับการทำงานของโปรแกรมจะตรงกับลำดับที่ปรากฏในโค้ดทุกประการ ซึ่งรับประกันได้ยากสำหรับ CPU หลายคอร์สมัยใหม่ที่มีการปรับลำดับการประมวลผลคำสั่งให้เหมาะสม นอกจากนี้ การรับประกันความสอดคล้องแบบลำดับยังจำกัดการปรับโค้ดให้เหมาะสมขณะทำงานของ JVM อย่างมาก。

แต่ความสัมพันธ์เกิดก่อน(Happens-before)ที่ข้อกำหนด JSR 133 ในเวอร์ชันนี้ระบุไว้ ทำให้ลำดับการประมวลผลคำสั่งยืดหยุ่นขึ้น:

ภายในเธรดเดียวกัน ตามลำดับการทำงานของโค้ด(ก็คือลำดับตามความหมายของโค้ด)การดำเนินการก่อนหน้าจะเกิดก่อนการดำเนินการถัดไป การปลดล็อกออบเจ็กต์ monitor จะเกิดก่อนการล็อกออบเจ็กต์ monitor เดียวกันในภายหลัง การเขียนฟิลด์ volatile จะเกิดก่อนการอ่านฟิลด์นี้ในภายหลัง การ start เธรด(เรียกเมธอด start() ของออบเจ็กต์เธรด)จะเกิดก่อนการดำเนินการอื่นใดของเธรดนั้น การดำเนินการทั้งหมดในเธรดหนึ่งจะเกิดก่อนเธรดอื่นใดเรียกเมธอด join() บนเธรดนี้ หากการดำเนินการ A เกิดก่อน B และ B เกิดก่อน C การดำเนินการ A ก็เกิดก่อน C

ส่วนการจัดสรรหน่วยความจำ การแยกหน่วยความจำทำงานของแต่ละเธรดออกจากหน่วยความจำหลัก ยิ่งทำให้ JVM มีพื้นที่มากในการปรับการประมวลผลคำสั่งภายในเธรดให้เหมาะสม ตัวแปรในหน่วยความจำหลักสามารถคัดลอกไปยังหน่วยความจำทำงานของเธรดเพื่อประมวลผลแยกกันได้ และเมื่อประมวลผลเสร็จ ผลลัพธ์อาจถูกเขียนกลับไปยังหน่วยความจำหลักในเวลาใดเวลาหนึ่ง: แต่จะรับประกันความสอดคล้องของข้อมูลระหว่างเธรดต่าง ๆ ได้อย่างไร? วิธีที่ JLS(Java Language Specification)กำหนดไว้คือ โดยค่าเริ่มต้นจะไม่รับประกันความสอดคล้องของข้อมูลในทุกขณะ แต่สามารถทำให้ข้อมูลสอดคล้องกันได้ด้วยการใช้คีย์เวิร์ด synchronized、volatile และ final ซึ่งได้รับการเสริมความหมายเชิงพฤติกรรม

การปรับปรุง synchronized ใน JDK 6.0

คีย์เวิร์ด synchronized ในฐานะ“บุตรคนโตของสาธารณรัฐ” ถูก ReentrantLock แย่งความโดดเด่นไปในเวอร์ชัน 5.0 เวอร์ชันนี้จึงต้องเอาคืนให้ได้สักครั้ง ดังนั้น JDK 6.0 จึงปรับปรุงการล็อกหลายอย่าง เช่น การหมุนรอล็อก การตัดล็อกที่ไม่จำเป็น การรวมล็อก ล็อกแบบเบา และล็อกแบบเอนเอียง การปรับปรุงครั้งนี้เป็นการแสดงแนวคิด“การจัดการอย่างละเอียด”ให้เห็นเป็นรูปธรรม ก่อนปรับปรุง ออบเจ็กต์ที่ล็อกด้วย synchronized มีเพียงสองสถานะ:ไม่มีล็อก กับมีล็อก(ล็อกแบบหนัก)หลังปรับปรุง ล็อกมีทั้งหมด 4 สถานะ เรียงจากระดับต่ำไปสูงคือ:ไม่มีล็อก、ล็อกแบบเอนเอียง、ล็อกแบบเบา、ล็อกแบบหนัก สถานะเหล่านี้จะค่อย ๆ ยกระดับตามการแย่งใช้ แต่ไม่สามารถลดระดับได้ จุดประสงค์คือเพิ่มประสิทธิภาพการได้มาซึ่งล็อกและการปล่อยล็อก(ผู้เขียนคิดว่าที่จริงคงซับซ้อนเกินไป จนนักพัฒนา JVM เห็นแล้วถอดใจ)。

การปรับปรุงครั้งนี้ทำให้ synchronized ได้เชิดหน้าชูตา นับแต่นั้นก็ไม่ยอมให้ใครบอกว่าประสิทธิภาพของมันด้อยกว่า ReentrantLock อีก แต่เรื่องสนุกยังมีต่อ เพราะ biased locking ถูกเลิกแนะนำให้ใช้ใน JDK 15(─.─||) ผู้เขียนเห็นว่า synchronized เสียเปรียบตรงที่มันเป็นเพียงคีย์เวิร์ด โดย JVM รับผิดชอบการทำงานเบื้องหลัง ส่วนแอปพลิเคชันจะล็อกแบบไหนจึงเหมาะที่สุดนั้น JVM ต้องเป็นผู้ “เดา” ขณะที่ ReentrantLock ต่างออกไป มันมอบเรื่องนี้ให้โปรแกรมเมอร์จัดการโดยตรง ถ้าคุณต้องการความเป็นธรรมก็ใช้ล็อกแบบเป็นธรรม ถ้าต้องการแบบไม่เป็นธรรมก็ใช้ล็อกแบบไม่เป็นธรรม ในแง่การออกแบบถือว่าเป็นการประหยัดแรงอย่างหนึ่ง แต่ก็ให้ความยืดหยุ่นด้วยเช่นกัน

เฟรมเวิร์ก Fork/Join ของ JDK 7.0

Fork/Join ก็ถือกำเนิดขึ้นในฐานะผลิตภัณฑ์ที่ค่อนข้างก้าวหน้า จุดแข็งหลักของมันคือรองรับการแยกงานแบบเรียกซ้ำ พร้อมทั้งรวมผลลัพธ์ของแต่ละงาน แต่ก็เป็นเทคโนโลยีที่ทั้งคุ้นเคยและแปลกหน้า คุ้นเคยเพราะถูกใช้ในหลายที่ เช่น CompletableFuture และ Stream ที่จะกล่าวถึงต่อไปใน JDK8.0;แปลกหน้าเพราะดูเหมือนเราแทบไม่ได้ใช้มันระหว่างการพัฒนาระบบธุรกิจ

บางคนถึงกับมองว่ามันแทบไม่มีประโยชน์ ผู้เขียนเห็นว่า หากคุณเป็นนักพัฒนาที่ทำงานตามความต้องการทางธุรกิจ มันก็แทบไม่มีประโยชน์ เพราะแทบไม่ได้ใช้เลย สำหรับสถานการณ์ที่มีข้อมูลจำนวนมากก็มีชุดเครื่องมือคลังข้อมูลอยู่แล้ว ส่วนสถานการณ์อื่น ๆ ใช้เธรดพูลแทนได้;แต่หากคุณเป็นนักพัฒนาที่เขียนเฟรมเวิร์กมิดเดิลแวร์ มันก็มีประโยชน์ และอาจได้ใช้ บนอินเทอร์เน็ตภาษาจีนมีคนตั้งข้อสงสัยกับเทคโนโลยีนี้น้อยมาก แต่ในต่างประเทศมีคนกำลังอภิปรายกันแล้ว ผู้ที่สนใจสามารถกดเข้าไปอ่านได้โดยตรง เฟรมเวิร์ก Fork-Join ใน Java มีข้อบกพร่องหรือไม่?

JDK 8.0

การออกเวอร์ชันนี้ถือเป็นหมุดหมายสำคัญของ Java ถึงขนาดที่ในบรรดาโปรแกรม Java ที่กำลังทำงานทั่วโลกในปัจจุบัน มากกว่าครึ่งใช้เวอร์ชันนี้ แต่การอัปเดตด้านมัลติเธรดไม่ได้พลิกโฉมเท่ากับ JDK5.0 นอกจากเพิ่มออบเจ็กต์อะตอมมิกบางส่วนแล้ว การอัปเดตที่โดดเด่นที่สุดของเวอร์ชันนี้ก็คือสองรายการต่อไปนี้。

CompletableFuture

บนอินเทอร์เน็ตมีบทความแนะนำ CompletableFuture มากมาย ส่วนใหญ่อธิบายหลักการและวิธีใช้งาน แต่ผู้เขียนยังคงไม่เข้าใจอยู่ข้อหนึ่ง:ในเมื่อมีเครื่องมือเธรดพูลมากมายอยู่แล้ว ทำไมจึงยังมี CompletableFuture เกิดขึ้น มันแก้ปัญหาอะไร?จุดแข็งหลักของมันคืออะไรกันแน่?เชื่อว่าหากคุณเคยคิดเรื่องนี้ ก็คงตั้งคำถามเดียวกัน ไม่เป็นไร ผู้เขียนหาคำตอบให้คุณแล้ว。

ข้อสรุป:จุดแข็งหลักในการแข่งขันของ CompletableFuture คือการจัดลำดับและประสานงาน。CompletableFuture สืบทอดคุณสมบัติของอินเทอร์เฟซ Future และมีความสามารถต่าง ๆ เช่น การเรียกใช้งานพร้อมกัน ซึ่งล้วนมีสิ่งอื่นทดแทนได้ แต่ความสามารถในการจัดลำดับและประสานงานของมันไม่มีสิ่งใดทดแทนได้ API หลักของมันมีทั้งการสร้างสายงานและการรวมผลลัพธ์ของงาน ซึ่งล้วนออกแบบมาเพื่อการจัดลำดับและประสานงาน ดังนั้น เหตุผลหลักที่ JDK นำเฟรมเวิร์กนี้เข้ามาในเวอร์ชันนี้ คือเพื่อแก้ปัญหาความต้องการจัดลำดับและประสานงานที่สร้างความยุ่งยากมากขึ้นเรื่อย ๆ ในการพัฒนาระบบธุรกิจ。

ขอเสริมอีกสักประโยคว่า ภายใน CompletableFuture ใช้เฟรมเวิร์ก Fork/Join ในการทำงาน

Stream

หนังสือ《Clean Architecture》กล่าวถึงกระบวนทัศน์การเขียนโปรแกรมสามแบบ ได้แก่ การเขียนโปรแกรมแบบมีโครงสร้าง(การเขียนโปรแกรมเชิงกระบวนการ)、การเขียนโปรแกรมเชิงวัตถุ และการเขียนโปรแกรมเชิงฟังก์ชัน Stream เป็นการแสดงออกอย่างหนึ่งของการเขียนโปรแกรมเชิงฟังก์ชันในภาษา Java ผู้เขียนเห็นว่าเส้นทางที่โปรแกรมเมอร์ระดับต้นต้องผ่านเพื่อก้าวสู่ระดับกลางคือการทำความเข้าใจ Stream ให้ได้ เมื่อสัมผัส Stream ครั้งแรกย่อมรู้สึกไม่คุ้นเคยอย่างมาก แต่เมื่อคุ้นเคยแล้ว คุณจะได้เปิดมุมมองใหม่ต่อวิธีเขียนโปรแกรม นักพัฒนามักสับสนแนวคิดสามอย่าง ได้แก่ การเขียนโปรแกรมเชิงฟังก์ชัน、Stream และนิพจน์ Lambda โดยคิดเสมอว่าทั้งสามคือเรื่องเดียวกัน ต่อไปนี้คือความเข้าใจของผู้เขียน:

•การเขียนโปรแกรมเชิงฟังก์ชันเป็นแนวคิดในการเขียนโปรแกรม ซึ่งมีการนำไปใช้ในภาษาโปรแกรมต่าง ๆ

•Stream เป็นคุณสมบัติใหม่ของ JDK8.0 หรืออาจมองว่าเป็นแนวคิดที่สร้างขึ้นใหม่ โดยมีเป้าหมายเพื่อรองรับแนวคิดการเขียนโปรแกรมเชิงฟังก์ชัน การใช้ Stream ทำให้เขียนโปรแกรมเชิงฟังก์ชันกับคลาสคอลเลกชันได้

•นิพจน์ Lambda(lambda expression)คือฟังก์ชันนิรนาม ซึ่งช่วยให้แสดงการเขียนโปรแกรมเชิงฟังก์ชันได้กระชับและมีประสิทธิภาพยิ่งขึ้น

พูดมาตั้งมากมาย แล้ว Stream เกี่ยวข้องกับการทำงานหลายเธรดอย่างไร?เมธอดที่เกี่ยวกับการทำงานแบบขนานใน Stream ใช้เฟรมเวิร์ก Fork/Join เป็นกลไกเบื้องหลัง ใน《Effective Java》มีคำแนะนำที่เกี่ยวข้องว่า “ใช้การทำงานแบบขนานของ Stream อย่างระมัดระวัง” เหตุผลคือการทำงานแบบขนานทั้งหมดรันอยู่ในพูล Fork/Join ส่วนกลางเดียวกัน หาก pipeline หนึ่งทำงานผิดปกติ อาจส่งผลเสียต่อประสิทธิภาพของส่วนอื่นที่ไม่เกี่ยวข้อง

JDK 9.0

ปรับปรุงกลไกจัดการการแย่งล็อก

การแย่งล็อกจำกัดประสิทธิภาพของแอปพลิเคชัน Java แบบหลายเธรดจำนวนมาก กลไกจัดการการแย่งล็อกแบบใหม่ช่วยปรับปรุงประสิทธิภาพของมอนิเตอร์ออบเจ็กต์ Java และผ่านการยืนยันจากการทดสอบเบนช์มาร์กหลายประเภท(เช่น Volano)ซึ่งสามารถประเมินขีดจำกัดอัตราการประมวลผลของ JVM ได้ ในทางปฏิบัติ กลไกใหม่ทำผลงานได้ยอดเยี่ยมในการทดสอบเบนช์มาร์กที่แตกต่างกัน 22 ประเภท หากนำกลไกใหม่นี้มาใช้ใน Java 9 ประสิทธิภาพของแอปพลิเคชันจะเพิ่มขึ้นอย่างมาก อธิบายง่าย ๆ คือเมื่อหลายเธรดแย่งล็อกกัน ก่อนปรับปรุง:เธรดที่มาทีหลังทั้งหมดจะรอล็อกตามขั้นตอนมาตรฐานเดียวกัน หลังปรับปรุง:เมื่อ JVM ตรวจพบสถานการณ์บางอย่างที่ปรับให้เหมาะสมได้ ก็จะให้เธรดที่มาทีหลังเข้าแย่งล็อกโดยตรงแบบ “ช่องทาง VIP”

ดูคำอธิบายโดยละเอียดได้ที่: อธิบายล็อกที่มีการแย่งใช้งาน – แนวทางด้านประสิทธิภาพ

สตรีมแบบรีแอกทีฟ

สตรีมแบบรีแอกทีฟ(Reactive Streams)เป็นมาตรฐานสำหรับประมวลผลสตรีมข้อมูลแบบอะซิงโครนัสด้วยกลไกแรงดันย้อนกลับที่ไม่ปิดกั้น โดยจัดเตรียมชุดอินเทอร์เฟซ เมธอด และโพรโทคอลขั้นต่ำเพื่ออธิบายการดำเนินการและเอนทิตีที่จำเป็น

แรงดันย้อนกลับแบบไม่บล็อกคืออะไร? แรงดันย้อนกลับเป็นคำย่อของ back pressure พูดง่าย ๆ คือ ผู้ผลิตส่งข้อมูลให้ผู้บริโภค เมื่อผู้บริโภคประมวลผลไม่ไหวก็แจ้งผู้ผลิต จากนั้นผู้ผลิตจะลดอัตราการผลิต กลไกนี้ทำได้ง่ายที่สุดด้วยวิธีบล็อก นั่นคือส่งข้อมูลแรงดันกลับมาโดยตรงเมื่อส่งข้อมูลออกไป การใช้วิธีไม่บล็อกเพิ่มความซับซ้อนในการออกแบบ แต่ก็ช่วยเพิ่มประสิทธิภาพด้วย。 หมายเหตุ:รู้สึกว่าคำว่า 背压 แปลได้ไม่ดีนัก ไม่สามารถตีความจากตัวอักษรตรง ๆ ได้ ใช้คำว่า 反压 ซึ่งหมายถึงแรงดันต้านกลับ จะดีกว่าหรือเปล่า^\_^

เพื่อแก้ปัญหาที่ผู้บริโภคอาจล่มจากแรงกดดันด้านทรัพยากรอย่างมหาศาล (pressure) จึงต้องควบคุมความเร็วของกระแสข้อมูล หรือก็คือการควบคุมการไหล (flow control) เพื่อป้องกันไม่ให้กระแสข้อมูลที่รวดเร็วทำให้ปลายทางรับไม่ไหว ดังนั้นจึงต้องมีแรงดันย้อนกลับ หรือแบ็กเพรสเชอร์ (back pressure) โดยผู้ผลิตและผู้บริโภคต้องทำงานร่วมกันผ่านกลไกแบ็กเพรสเชอร์ กลไกนี้ต้องเป็นแบบอะซิงโครนัสและไม่บล็อก หากเป็นแบบซิงโครนัสและบล็อก ผู้ผลิตจะต้องรอขณะผู้บริโภคประมวลผลข้อมูล ซึ่งก่อให้เกิดปัญหาด้านประสิทธิภาพ

Reactive Streams กำหนดมาตรฐานสำหรับการทำแบ็กเพรสเชอร์แบบไม่บล็อก ผ่านการนิยามชุดเอนทิตี อินเทอร์เฟซ และวิธีการทำงานร่วมกัน ผู้พัฒนาภายนอกปฏิบัติตามมาตรฐานนี้เพื่อสร้างโซลูชันที่ใช้งานได้จริง ตัวอย่างที่พบบ่อย ได้แก่ Reactor、RxJava、Akka Streams、Ratpack เป็นต้น

JDK 10 การควบคุมเฉพาะเธรด

Safepoint และข้อจำกัด:

Safepoint เป็นกลไกหนึ่งใน Hotspot JVM ที่ทำให้แอปพลิเคชันทั้งหมดหยุดทำงาน JVM ต้องทำ Stop The World เพื่อดำเนินงานระดับล่างบางอย่าง โดยให้เธรดของแอปพลิเคชันทั้งหมดหยุดลง แต่จะสั่งหยุดโดยตรงแบบหักดิบไม่ได้ จึงส่งสัญญาณคำสั่งไปยังเธรดของแอปพลิเคชันเพื่อบอกว่าถึงเวลาหยุดแล้ว เมื่อเธรดของแอปพลิเคชันทำงานมาถึงจุด Safepoint ก็จะปฏิบัติตามคำสั่งและตอบสนอง นี่จึงเป็นที่มาของชื่อ Safepoint ส่วนคำว่า safe นั้นเพิ่มเข้ามาเพื่อเน้นว่า JVM กำลังจะดำเนินงานบางอย่างที่ต้องปลอดภัยในระดับระบบโดยรวม จึงใส่คำว่า safe ให้กับจุดนี้。

งานที่ต้องให้ทั้งระบบอยู่ในจุดปลอดภัย ได้แก่: 1、การหยุดชั่วคราวเพื่อเก็บกวาดขยะ 2、การยกเลิกการปรับแต่งโค้ดให้เหมาะสม(Code deoptimization)。 3、การล้างแคชโค้ด。 4、เมื่อมีการนิยามไฟล์คลาสใหม่(Class redefinition เช่น การอัปเดตขณะทำงาน หรือ instrumentation)。 5、การเพิกถอนล็อกแบบเอนเอียง(Biased lock revocation)。 6、การดีบักรูปแบบต่าง ๆ (เช่น: การตรวจหาเดดล็อก หรือการดัมป์ stacktrace เป็นต้น)。

อย่างไรก็ตาม การให้ทุกเธรดหยุดที่ safepoint ที่ใกล้ที่สุดนั้นต้องใช้เวลาค่อนข้างนานอยู่แล้ว อีกทั้งการสั่งให้ทุกเธรดหยุดทั้งหมดจะดูหุนหันและใช้อำนาจเกินไปหรือไม่ ด้วยเหตุนี้ Java10 จึงนำวิธีที่ไม่ต้องหยุดทุกเธรดมาใช้ นั่นคือการประสานงานเฉพาะเธรด(Thread Local Handshake)。

ตัวอย่างเช่น สถานการณ์ต่อไปนี้จัดการได้โดยไม่ต้องหยุดเธรดทั้งหมด: 1、การเพิกถอนล็อกแบบเอนเอียง เรื่องนี้เพียงหยุดเธรดเดียวก็เพิกถอนล็อกแบบเอนเอียงได้ โดยไม่ต้องหยุดทุกเธรด 2、ลดผลกระทบโดยรวมต่อความหน่วงของ VM จากการสอบถามข้อมูลเพื่อการตรวจสอบและบำรุงรักษาประเภทต่าง ๆ เช่น การดึง stack trace ของทุกเธรดบน VM ที่มีเธรด Java จำนวนมากอาจเป็นงานที่ช้า 3、สุ่มเก็บ stack trace ได้อย่างปลอดภัยยิ่งขึ้นโดยลดการพึ่งพาสัญญาณ(signals) 4、ใช้เทคนิคการประสานงานที่เรียกว่า Dekker แบบอสมมาตร เพื่อกำจัดกำแพงหน่วยความจำบางส่วนผ่านการแฮนด์เชกกับเธรด Java ตัวอย่างเช่น “โค้ดทำเครื่องหมายการ์ดแบบมีเงื่อนไข”(conditional card mark code)ที่ใช้ใน G1 และ CMS จะไม่ต้องใช้ “กำแพงหน่วยความจำ” อีกต่อไป ด้วยวิธีนี้ “กำแพงการเขียน(write barrier)” ที่ G1 สร้างขึ้นก็สามารถปรับให้มีประสิทธิภาพขึ้นได้ และยังลบสาขาเงื่อนไขที่พยายามหลีกเลี่ยง “กำแพงหน่วยความจำ” เหล่านั้นได้ด้วย。

JDK 15 ปิดใช้งานและประกาศเลิกใช้ biased locking

เหตุใดจึงเลิกใช้ biased locking?ประสิทธิภาพที่ biased locking เคยช่วยเพิ่มในอดีต เมื่อมองจากปัจจุบันก็ไม่เด่นชัดเท่าเดิมแล้ว แอปพลิเคชันที่ได้ประโยชน์จาก biased locking มักเป็นโปรแกรมที่ใช้ Java Collections API ยุคแรก(JDK 1.1)ซึ่ง API เหล่านี้(Hashtable และ Vector)จะทำ synchronization ทุกครั้งที่เข้าถึง JDK 1.2 เพิ่มคอลเลกชันที่ไม่ทำ synchronization สำหรับการใช้งานแบบเธรดเดียว(HashMap และ ArrayList)ส่วน JDK 1.5 ได้เพิ่มโครงสร้างข้อมูลแบบทำงานพร้อมกันที่มีประสิทธิภาพสูงขึ้นสำหรับการใช้งานหลายเธรด นั่นหมายความว่า หากปรับโค้ดให้ใช้คลาสรุ่นใหม่กว่า แอปพลิเคชันที่เคยได้ประโยชน์จาก biased locking เนื่องจากมี synchronization ที่ไม่จำเป็น อาจมีประสิทธิภาพเพิ่มขึ้นอย่างมาก นอกจากนี้ แอปพลิเคชันที่สร้างขึ้นโดยใช้คิวของเธรดพูลและเธรดทำงาน มักมีประสิทธิภาพดีขึ้นเมื่อปิด biased locking。

ต่อไปนี้เป็นการเขียนโดยใช้ API ของ Hashtable และ Vector: java.lang.Classloader ใช้ Vector java.util.Properties extends Hashtable java.security.Provider extends Properties java.net.URL ใช้ Hashtable java.net.URConnection ใช้ Hashtable java.util.ZipOutputStream ใช้ Vector javax.management.timer.TimerMBean มี Vector อยู่บนอินเทอร์เฟซ

ยุคแห่งการขับขี่อัตโนมัติ

เธรดเสมือนพา Java เข้าสู่ยุคขับเคลื่อนอัตโนมัติ หลายภาษามีเทคโนโลยีคล้าย “เธรดเสมือน” เช่น Go、C#、Erlang、Lua เป็นต้น โดยเรียกกันว่า “คอรูทีน” ครั้งนี้ java ไม่ได้เพิ่มคีย์เวิร์ดใด ๆ และถึงขั้นไม่ได้เพิ่มแนวคิดใหม่ด้วยซ้ำ เธรดเสมือนเข้าใจง่ายกว่า goroutine และคอรูทีนมาก แค่เห็นชื่อก็พอจะรู้แล้วว่ามันทำอะไร。

เธรดเสมือนใน JDK 19

โมเดลเธรดใน Java แบบดั้งเดิมจับคู่กับเธรดของระบบปฏิบัติการแบบ 1:1 การสร้างและสลับเธรดมีต้นทุนสูง และสร้างได้เพียงจำนวนจำกัดเนื่องจากข้อจำกัดของระบบปฏิบัติการ เมื่อมีการทำงานพร้อมกันจำนวนมาก จึงไม่อาจสร้างเธรดหนึ่งเธรดให้ทุกคำขอได้ การใช้พูลเธรดช่วยบรรเทาปัญหาได้ โดยลดต้นทุนการสร้างเธรด แต่ก็ไม่อาจเพิ่มจำนวนเธรดได้ สมมติว่ามีงานพร้อมกัน 2000 งาน แต่พูลเธรดมีเพียง 1000 เธรด ในเวลาเดียวกันจึงประมวลผลได้เพียง 1000 คำขอ ส่วนอีก 1000 คำขอยังไม่อาจประมวลผลได้ อาจปฏิเสธหรือให้รอจนกว่าจะมีเธรดว่าง

แนวทางก่อนที่จะมีเธรดเสมือนคือการใช้รูปแบบอะซิงโครนัส มีเฟรมเวิร์กจำนวนมากที่รองรับการเขียนโปรแกรมพร้อมกันแบบอะซิงโครนัสอยู่แล้ว(เช่น Reactor ของ Spring5) โดยใช้เธรดร่วมกันเพื่อให้มีความพร้อมใช้งานสูงขึ้น หลักการคือการใช้เธรดร่วมกันช่วยลดการสลับเธรด ลดการใช้ทรัพยากร และหลีกเลี่ยงการบล็อก โดยใช้เธรดเฉพาะตอนที่โปรแกรมกำลังประมวลผล เมื่อโปรแกรมต้องรอก็จะไม่ครองเธรดไว้ รูปแบบอะซิงโครนัสช่วยปรับปรุงได้หลายด้านจริง แต่ก็มีข้อเสีย เฟรมเวิร์กอะซิงโครนัสส่วนใหญ่ใช้การเขียนแบบต่อสายคำสั่ง โดยแบ่งโปรแกรมออกเป็นหลายขั้นตอน และแต่ละขั้นตอนอาจทำงานในเธรดที่ต่างกัน คุณจึงไม่สามารถใช้ API ที่คุ้นเคยเกี่ยวกับการเขียนโปรแกรมพร้อมกัน เช่น ThreadLocal ได้อีก มิฉะนั้นอาจเกิดข้อผิดพลาด รูปแบบการเขียนโปรแกรมก็เปลี่ยนไปมาก ซับซ้อนกว่ารูปแบบดั้งเดิมมาก มีต้นทุนการเรียนรู้สูง และอาจต้องปรับโมดูลเดิมจำนวนมากในโครงการให้รองรับรูปแบบอะซิงโครนัสด้วย。

หลักการทำงานของเธรดเสมือนคล้ายกับเฟรมเวิร์กอะซิงโครนัสบางตัว คือใช้เธรดร่วมกัน จึงไม่จำเป็นต้องทำพูล เวลาใช้งานคุณสามารถมองว่าเธรดเสมือนมีอยู่อย่างเหลือเฟือไม่จำกัด อยากสร้างเท่าไรก็สร้างได้ ไม่ต้องกังวลว่าจะมีปัญหา ไม่เพียงเท่านั้น เธรดเสมือนยังรองรับการดีบัก และเครื่องมือตรวจติดตามที่เกี่ยวข้องกับ Java ก็รองรับด้วย ซึ่งสำคัญมาก เธรดเสมือนจะช่วยลดการใช้หน่วยความจำของโปรแกรมคุณอย่างมาก แอปพลิเคชันทุกชนิดที่เน้น IO เช่น Web Servers สามารถเพิ่มปริมาณงาน IO ได้อย่างมากภายใต้ฮาร์ดแวร์เดียวกัน เดิมหน่วยความจำ 1G รองรับการเข้าถึงพร้อมกันได้ 1000 ครั้ง หลังใช้เธรดเสมือน ตามข้อมูลทางการระบุว่าสามารถจัดการงานพร้อมกัน 100 หมื่นงานได้อย่างสบาย ส่วนจะรองรับสถานการณ์ทางธุรกิจจริงได้หรือไม่นั้นยังต้องดูผลการทดสอบภาระงาน แต่หากเราประเมินเผื่อไว้ 10 หมื่นงานก็น่าจะทำได้อย่างสบาย โดยคุณไม่จำเป็นต้องเสียอะไรเพิ่ม อาจไม่ต้องแก้โค้ดด้วยซ้ำ เพราะเธรดเสมือนช่วยให้คุณรักษารูปแบบการเขียนโปรแกรมแบบเดิมได้ นั่นคือหนึ่งคำขอต่อหนึ่งเธรด ใช้เธรดเสมือนเหมือนใช้เธรดทั่วไป โปรแกรมจึงต้องปรับเปลี่ยนเพียงเล็กน้อย เธรดเสมือนไม่ได้เพิ่มไวยากรณ์ใหม่ จึงกล่าวได้ว่าต้นทุนการเรียนรู้และการย้ายมาใช้นั้นต่ำมาก

ควรกล่าวด้วยว่าเบื้องหลังของเธรดเสมือนยังคงใช้เฟรมเวิร์ก Fork/Join


ความคิดเห็น

กำลังส่ง

บทความยอดนิยม

เลือกธีมที่ชอบ

ธีมที่เลือกจะใช้ทั่วทั้งเว็บไซต์