ไม่ว่าคุณจะติดตามหรือไม่ก็ตามเว็บแอปพลิเคชัน Java ใช้พูลเธรดเพื่อจัดการคำขอในระดับที่มากขึ้นหรือน้อยลง รายละเอียดการใช้งานของพูลเธรดอาจถูกละเว้น แต่จำเป็นต้องเข้าใจไม่ช้าก็เร็วเกี่ยวกับการใช้งานและการปรับแต่งพูลเธรด บทความนี้ส่วนใหญ่แนะนำการใช้พูลเธรด Java และวิธีกำหนดค่าพูลเธรดอย่างถูกต้อง
เกลียวเดี่ยว
เริ่มต้นด้วยพื้นฐาน ไม่ว่าแอปพลิเคชันเซิร์ฟเวอร์หรือเฟรมเวิร์กใด (เช่น Tomcat, Jetty, ฯลฯ ) มีการใช้งานพื้นฐานที่คล้ายกัน พื้นฐานของบริการเว็บคือซ็อกเก็ตซึ่งรับผิดชอบในการฟังพอร์ตรอการเชื่อมต่อ TCP และยอมรับการเชื่อมต่อ TCP เมื่อยอมรับการเชื่อมต่อ TCP ข้อมูลสามารถอ่านและส่งจากการเชื่อมต่อ TCP ที่สร้างขึ้นใหม่
เพื่อที่จะเข้าใจกระบวนการข้างต้นเราไม่ได้ใช้แอปพลิเคชันเซิร์ฟเวอร์ใด ๆ โดยตรง แต่สร้างบริการเว็บอย่างง่ายตั้งแต่เริ่มต้น บริการนี้เป็นพิภพเล็ก ๆ ของแอปพลิเคชันเซิร์ฟเวอร์ส่วนใหญ่ บริการเว็บแบบเธรดเดี่ยวที่เรียบง่ายมีลักษณะเช่นนี้:
Serversocket Listener = New Serversocket (8080); ลอง {ในขณะที่ (จริง) {ซ็อกเก็ตซ็อกเก็ต = listener.accept (); ลอง {handlerequest (ซ็อกเก็ต); } catch (ioexception e) {e.printstacktrace (); }}} ในที่สุด {listener.close ();}รหัสด้านบนสร้าง ซ็อกเก็ตเซิร์ฟเวอร์ (Serversocket) รับฟังพอร์ต 8080 จากนั้นลูปเพื่อตรวจสอบซ็อกเก็ตเพื่อดูว่ามีการเชื่อมต่อใหม่หรือไม่ เมื่อยอมรับการเชื่อมต่อใหม่ซ็อกเก็ตจะถูกส่งผ่านไปยังวิธี handlerequest วิธีนี้จะวิเคราะห์ข้อมูลสตรีมข้อมูลลงในคำขอ HTTP ตอบสนองและเขียนข้อมูลการตอบกลับ ในตัวอย่างง่ายๆนี้วิธี Handlerequest เพียงแค่ใช้การอ่านของสตรีมข้อมูลและส่งคืนข้อมูลการตอบกลับอย่างง่าย ในการใช้งานทั่วไปวิธีนี้จะซับซ้อนมากขึ้นเช่นการอ่านข้อมูลจากฐานข้อมูล ฯลฯ
การตอบสนองสตริงคงที่สุดท้าย = "http/1.0 200 ok/r/n" + "เนื้อหาประเภท: ข้อความ/plain/r/n" + "/r/n" + "Hello World/R/N"; โมฆะสาธารณะคงที่ handlerequest (ซ็อกเก็ตซ็อกเก็ต) พ่น IOException {// อ่านกระแสอินพุตและกลับ "200 ตกลง" ลอง {bufferedReader in = new bufferedReader (ใหม่ inputStreamReader (socket.getInputStream ()); log.info (in.readline ()); outputStream out = socket.getOutputStream (); out.write (response.getBytes (StandardCharsets.UTF_8)); } ในที่สุด {socket.close (); -เนื่องจากมีเพียงเธรดเดียวเท่านั้นที่จะดำเนินการตามคำขอแต่ละคำขอจะต้องรอคำขอก่อนหน้านี้ที่จะดำเนินการก่อนที่จะสามารถตอบกลับได้ สมมติว่าเวลาตอบสนองคำขอคือ 100 มิลลิวินาทีจำนวนการตอบสนองต่อวินาที (TPS) ของเซิร์ฟเวอร์นี้มีเพียง 10
มัลติเธรด
แม้ว่าวิธี Handlerequest อาจบล็อก IO แต่ CPU ยังสามารถจัดการคำขอได้มากขึ้น แต่ในกรณีที่มีเกลียวเดียวสิ่งนี้ไม่สามารถทำได้ ดังนั้นความสามารถในการประมวลผลแบบขนานของเซิร์ฟเวอร์สามารถปรับปรุงได้โดยการสร้างวิธีการหลายเธรด
ชั้นเรียนคงที่ระดับสาธารณะ handlerequestrunnable ใช้งานได้ {ซ็อกเก็ตซ็อกเก็ตสุดท้าย; Public Handlerequestrunnable (ซ็อกเก็ตซ็อกเก็ต) {this.socket = ซ็อกเก็ต; } โมฆะสาธารณะเรียกใช้ () {ลอง {handlerequest (ซ็อกเก็ต); } catch (ioexception e) {e.printstacktrace (); }}} Serversocket Listener = ใหม่ Serversocket (8080); ลอง {ในขณะที่ (จริง) {ซ็อกเก็ตซ็อกเก็ต = listener.accept (); เธรดใหม่ (ใหม่ handlerequestrunnable (ซ็อกเก็ต)). start (); }} ในที่สุด {listener.close ();}ที่นี่วิธีการยอมรับ () ยังคงถูกเรียกในเธรดหลัก แต่เมื่อการเชื่อมต่อ TCP ถูกสร้างขึ้นเธรดใหม่จะถูกสร้างขึ้นเพื่อจัดการคำขอใหม่ซึ่งก็คือการดำเนินการวิธี Handlerequest ในข้อความก่อนหน้าในเธรดใหม่
ด้วยการสร้างเธรดใหม่เธรดหลักสามารถรับการเชื่อมต่อ TCP ใหม่ต่อไปและคำขอเหล่านี้สามารถประมวลผลได้ในแบบคู่ขนาน วิธีนี้เรียกว่า "หนึ่งเธรดต่อคำขอ" แน่นอนว่ามีวิธีอื่นในการปรับปรุงประสิทธิภาพการประมวลผลเช่นโมเดลที่ขับเคลื่อนด้วยเหตุการณ์แบบอะซิงโครนัสที่ใช้โดย Nginx และ Node.js แต่พวกเขาไม่ได้ใช้พูลเธรดและไม่ครอบคลุมโดยบทความนี้
ในแต่ละคำขอการใช้งานเธรดหนึ่งการสร้างเธรด (และการทำลายล้างที่ตามมา) ค่าใช้จ่ายมีราคาแพงมากเพราะทั้ง JVM และระบบปฏิบัติการจำเป็นต้องจัดสรรทรัพยากร นอกจากนี้การใช้งานข้างต้นยังมีปัญหานั่นคือจำนวนเธรดที่สร้างขึ้นนั้นไม่สามารถควบคุมได้ซึ่งอาจทำให้ทรัพยากรระบบหมดลงอย่างรวดเร็ว
ทรัพยากรที่หมดลง
แต่ละเธรดต้องใช้พื้นที่หน่วยความจำสแต็กจำนวนหนึ่ง ใน JVM 64 บิตล่าสุดขนาดสแต็กเริ่มต้นคือ 1024KB หากเซิร์ฟเวอร์ได้รับการร้องขอจำนวนมากหรือวิธี Handlerequest ดำเนินการช้าเซิร์ฟเวอร์อาจขัดข้องเนื่องจากการสร้างเธรดจำนวนมาก ตัวอย่างเช่นมีคำขอแบบขนาน 1,000 รายการและเธรด 1,000 ตัวที่สร้างขึ้นจำเป็นต้องใช้หน่วยความจำ JVM 1GB เป็นพื้นที่สแต็กเธรด นอกจากนี้วัตถุที่สร้างขึ้นในระหว่างการดำเนินการของรหัสแต่ละเธรดอาจถูกสร้างขึ้นบนกอง หากสถานการณ์นี้แย่ลงมันจะเกินหน่วยความจำ JVM Heap และสร้างการดำเนินการรวบรวมขยะจำนวนมากซึ่งในที่สุดจะทำให้หน่วยความจำล้น (outofMemoryErrors)
เธรดเหล่านี้ไม่เพียง แต่ใช้หน่วยความจำเท่านั้น แต่ยังใช้ทรัพยากรที่ จำกัด อื่น ๆ เช่นด้ามจับไฟล์การเชื่อมต่อฐานข้อมูล ฯลฯ เธรดการสร้างที่ไม่สามารถควบคุมได้อาจทำให้เกิดข้อผิดพลาดและข้อผิดพลาดประเภทอื่น ๆ ดังนั้นวิธีที่สำคัญในการหลีกเลี่ยงการอ่อนเพลียของทรัพยากรคือการหลีกเลี่ยงโครงสร้างข้อมูลที่ไม่สามารถควบคุมได้
โดยวิธีการเนื่องจากปัญหาหน่วยความจำที่เกิดจากขนาดสแต็กเธรดขนาดสแต็กสามารถปรับได้ผ่านสวิตช์ -XSS หลังจากลดขนาดสแต็กของด้ายแล้วค่าใช้จ่ายต่อเธรดสามารถลดลงได้ แต่อาจมีการยกสแต็กล้น (stackoverflowerrors) สำหรับแอปพลิเคชันทั่วไปค่าเริ่มต้น 1024KB นั้นอุดมไปด้วยและอาจเหมาะสมกว่าที่จะลดลงเป็น 256KB หรือ 512KB ค่าต่ำสุดที่อนุญาตใน Java คือ 160kB
สระว่ายน้ำ
เพื่อหลีกเลี่ยงการสร้างเธรดใหม่อย่างต่อเนื่องคุณสามารถ จำกัด ขีด จำกัด สูงสุดของพูลเธรดโดยใช้พูลเธรดแบบง่าย พูลเธรดจัดการเธรดทั้งหมด หากจำนวนเธรดยังไม่ถึงขีด จำกัด บนพูลเธรดจะสร้างเธรดไปยังขีด จำกัด บนและนำเธรดฟรีมาใช้ใหม่ให้มากที่สุด
Serversocket Listener = New Serversocket (8080); ExecutoRservice Executor = Executors.newFixedThreadPool (4); ลอง {ในขณะที่ (จริง) {ซ็อกเก็ตซ็อกเก็ต = Listener.accept (); Executor.submit (ใหม่ handlerequestrunnable (ซ็อกเก็ต)); }} ในที่สุด {listener.close ();}ในตัวอย่างนี้แทนที่จะสร้างเธรดโดยตรงจะใช้ ExecutorService มันส่งงานที่ต้องดำเนินการ (จำเป็นต้องใช้อินเตอร์เฟส Runnables) ไปยังพูลเธรดและดำเนินการรหัสโดยใช้เธรดในพูลเธรด ในตัวอย่างพูลเธรดขนาดคงที่ที่มีเธรดจำนวน 4 ใช้เพื่อประมวลผลคำขอทั้งหมด นี่เป็นการ จำกัด จำนวนเธรดที่จัดการคำขอและ จำกัด การใช้ทรัพยากร
นอกเหนือจากการสร้างพูลเธรดขนาดคงที่ผ่านวิธี newfixedthreadpool แล้วคลาส Executors ยังมีวิธีการ NEWCACHEDTHREADPOOL การนำพูลเธรดมาใช้ซ้ำอาจนำไปสู่จำนวนเธรดที่ไม่สามารถควบคุมได้ แต่มันจะใช้เธรดที่ไม่ได้ใช้งานที่ถูกสร้างขึ้นมาก่อนมากที่สุด โดยปกติแล้วพูลเธรดประเภทนี้เหมาะสำหรับงานสั้น ๆ ที่ไม่ได้ถูกบล็อกโดยทรัพยากรภายนอก
คิวงาน
หลังจากใช้พูลเธรดขนาดคงที่หากเธรดทั้งหมดไม่ว่างจะเกิดอะไรขึ้นถ้ามีคำขออื่นมา? ThreadPoolexecutor ใช้คิวเพื่อค้างไว้ที่รอการร้องขอและพูลเธรดขนาดคงที่ใช้รายการที่เชื่อมโยงไม่ จำกัด ตามค่าเริ่มต้น โปรดทราบว่าสิ่งนี้อาจทำให้เกิดปัญหาความอ่อนเพลียของทรัพยากร แต่จะไม่เกิดขึ้นตราบใดที่ความเร็วในการประมวลผลเธรดมากกว่าอัตราการเติบโตของคิว จากนั้นในตัวอย่างก่อนหน้าคำขอคิวแต่ละครั้งจะมีซ็อกเก็ตซึ่งในระบบปฏิบัติการบางระบบจะใช้การจัดการไฟล์ เนื่องจากระบบปฏิบัติการ จำกัด จำนวนไฟล์ที่เปิดโดยกระบวนการจึงเป็นการดีที่สุดที่จะ จำกัด ขนาดของคิวงาน
Public ExecutorService NewBoundedEdThreadPool (int nthreads, ความจุ int) {ส่งคืน ThreadPoolexecutor ใหม่ (nthreads, nthreads, 0l, timeunit.milliseconds, LinkedBlockingQueue ใหม่ <Runnable> (ความสามารถ) BoundedThreadPoolServersocket () พ่น IOException {Serversocket Listener = ใหม่ Serversocket (8080); ExecutorService Executor = newBoundedEdFixedThreadPool (4, 16); ลอง {ในขณะที่ (จริง) {ซ็อกเก็ตซ็อกเก็ต = listener.accept (); Executor.submit (ใหม่ handlerequestrunnable (ซ็อกเก็ต)); }} ในที่สุด {listener.close (); -ที่นี่แทนที่จะใช้เมธอด NewFixedThreadPool โดยตรงเพื่อสร้างพูลเธรดเราสร้างวัตถุ ThreadPoolexecutor ตัวเองและ จำกัด ความยาวของคิวการทำงานให้กับองค์ประกอบ 16
หากเธรดทั้งหมดไม่ว่างงานใหม่จะถูกกรอกเข้าคิว เนื่องจากคิว จำกัด ขนาดถึง 16 องค์ประกอบหากเกินขีด จำกัด นี้จึงจำเป็นต้องได้รับการจัดการโดยพารามิเตอร์สุดท้ายเมื่อสร้างวัตถุ Threadpoolexecutor ในตัวอย่างจะใช้ discardpolicy นั่นคือเมื่อคิวมาถึงขีด จำกัด สูงสุดงานใหม่จะถูกยกเลิก นอกเหนือจากครั้งแรกแล้วยังมีนโยบายการยกเลิก (abortpolicy) และนโยบายการดำเนินการของผู้โทร (Callerrunspolicy) อดีตจะโยนข้อยกเว้นในขณะที่หลังจะทำงานในเธรดผู้โทร
สำหรับเว็บแอปพลิเคชันนโยบายเริ่มต้นที่ดีที่สุดควรยกเลิกหรือยกเลิกนโยบายและส่งคืนข้อผิดพลาดไปยังลูกค้า (เช่นข้อผิดพลาด HTTP 503) แน่นอนว่ามันเป็นไปได้ที่จะหลีกเลี่ยงการละทิ้งคำขอของลูกค้าโดยการเพิ่มความยาวของคิวงาน แต่โดยทั่วไปคำขอของผู้ใช้ไม่เต็มใจที่จะรอเป็นเวลานานและสิ่งนี้จะใช้ทรัพยากรเซิร์ฟเวอร์มากขึ้น วัตถุประสงค์ของคิวงานไม่ได้ตอบสนองต่อคำขอของลูกค้าโดยไม่มีการ จำกัด แต่เพื่อให้ราบรื่นและระเบิด โดยปกติคิวงานควรว่างเปล่า
การปรับจำนวนเธรด
ตัวอย่างก่อนหน้านี้แสดงวิธีการสร้างและใช้พูลเธรด แต่ปัญหาหลักของการใช้พูลเธรดคือจำนวนเธรดที่ควรใช้ ก่อนอื่นเราต้องตรวจสอบให้แน่ใจว่าเมื่อถึงขีด จำกัด ของเธรดทรัพยากรจะไม่หมด ทรัพยากรที่นี่รวมถึงหน่วยความจำ (กองและสแต็ก) จำนวนที่จับไฟล์เปิดจำนวนการเชื่อมต่อ TCP จำนวนการเชื่อมต่อฐานข้อมูลระยะไกลและทรัพยากรที่ จำกัด อื่น ๆ โดยเฉพาะอย่างยิ่งหากงานเธรดมีการคำนวณอย่างเข้มข้นจำนวนคอร์ CPU ก็เป็นหนึ่งในข้อ จำกัด ของทรัพยากร โดยทั่วไปจำนวนเธรดไม่ควรเกินจำนวนคอร์ CPU
เนื่องจากการเลือกจำนวนเธรดขึ้นอยู่กับประเภทของแอปพลิเคชันอาจต้องใช้การทดสอบประสิทธิภาพจำนวนมากก่อนที่จะได้ผลลัพธ์ที่ดีที่สุด แน่นอนคุณสามารถปรับปรุงประสิทธิภาพของแอปพลิเคชันของคุณโดยเพิ่มจำนวนทรัพยากร ตัวอย่างเช่นปรับเปลี่ยนขนาดหน่วยความจำ JVM HEAP หรือแก้ไขขีด จำกัด สูงสุดของมือจับไฟล์ของระบบปฏิบัติการ ฯลฯ จากนั้นการปรับเหล่านี้จะมีขีด จำกัด สูงสุดทางทฤษฎี
กฎของน้อย
กฎของ Little อธิบายถึงความสัมพันธ์ระหว่างตัวแปรสามตัวในระบบที่มีเสถียรภาพ
ในกรณีที่ L แสดงถึงจำนวนเฉลี่ยของคำขอλแสดงถึงความถี่ของการร้องขอและ W แสดงถึงเวลาเฉลี่ยในการตอบสนองต่อคำขอ ตัวอย่างเช่นหากจำนวนการร้องขอต่อวินาทีคือ 10 และแต่ละครั้งการดำเนินการตามคำขอคือ 1 วินาทีจากนั้นเมื่อใดก็ตามที่มีการดำเนินการตามคำขอ 10 ครั้ง กลับไปที่หัวข้อของเราต้องใช้ 10 เธรดในการประมวลผล หากเวลาการประมวลผลของคำขอเดียวเป็นสองเท่าจำนวนเธรดที่ประมวลผลจะเพิ่มขึ้นเป็นสองเท่า
หลังจากทำความเข้าใจกับผลกระทบของเวลาในการประมวลผลต่อประสิทธิภาพการประมวลผลคำขอเราจะพบว่าขีด จำกัด สูงสุดทางทฤษฎีอาจไม่ใช่ค่าที่เหมาะสมที่สุดสำหรับขนาดพูลเธรด ขีด จำกัด บนของพูลเธรดยังต้องใช้เวลาในการประมวลผลงานอ้างอิง
สมมติว่า JVM สามารถประมวลผลงาน 1,000 รายการในแบบคู่ขนานหากแต่ละเวลาการร้องขอการดำเนินการไม่เกิน 30 วินาทีจากนั้นในกรณีที่เลวร้ายที่สุดที่ 33.3 คำขอต่อวินาทีสามารถดำเนินการได้ อย่างไรก็ตามหากคำขอแต่ละครั้งใช้เวลาเพียง 500 มิลลิวินาทีแอปพลิเคชันสามารถดำเนินการตามคำขอ 2000 ต่อวินาที
สระว่ายน้ำด้าย
ใน microservices หรือสถาปัตยกรรมที่มุ่งเน้นบริการ (SOA) มักจะต้องใช้บริการแบ็กเอนด์หลายรายการ หากหนึ่งในบริการดำเนินการลดลงอาจทำให้พูลเธรดหมดเธรดซึ่งส่งผลกระทบต่อการร้องขอไปยังบริการอื่น ๆ
วิธีที่มีประสิทธิภาพในการจัดการกับความล้มเหลวของบริการแบ็กเอนด์คือการแยกพูลเธรดที่ใช้โดยแต่ละบริการ ในโหมดนี้ยังมีพูลเธรดที่ส่งมอบซึ่งส่งงานไปยังพูลเธรดคำขอแบ็กเอนด์ที่แตกต่างกัน พูลเธรดนี้อาจไม่มีภาระเนื่องจากแบ็กเอนด์ที่ช้าและโอนภาระไปยังพูลเธรดที่ร้องขอแบ็กเอนด์ช้า
นอกจากนี้โหมดการรวมหลายเธรดยังจำเป็นต้องหลีกเลี่ยงปัญหาการหยุดชะงัก หากแต่ละเธรดถูกปิดกั้นขณะที่รอผลการร้องขอที่ยังไม่ผ่านกระบวนการจะมีการหยุดชะงักเกิดขึ้น ดังนั้นในโหมดพูลแบบมัลติเธรดจำเป็นต้องเข้าใจงานที่ดำเนินการโดยแต่ละพูลเธรดและการพึ่งพาระหว่างพวกเขาเพื่อหลีกเลี่ยงปัญหาการหยุดชะงักมากที่สุด
สรุป
แม้ว่าพูลเธรดจะไม่ได้ใช้โดยตรงในแอปพลิเคชัน แต่ก็มีแนวโน้มที่จะใช้งานทางอ้อมโดยแอปพลิเคชันเซิร์ฟเวอร์หรือเฟรมเวิร์กในแอปพลิเคชัน เฟรมเวิร์กเช่น Tomcat, JBoss, undertow, dropwizard ฯลฯ ทั้งหมดมีตัวเลือกในการปรับพูลเธรด (พูลเธรดที่ใช้โดย Servlet Execution)
ฉันหวังว่าบทความนี้สามารถปรับปรุงความเข้าใจของคุณเกี่ยวกับพูลเธรดและช่วยให้คุณเรียนรู้