JavaRush /จาวาบล็อก /Random-TH /มัลติเธรดใน Java: สาระสำคัญ ข้อดี และข้อผิดพลาดทั่วไป

มัลติเธรดใน Java: สาระสำคัญ ข้อดี และข้อผิดพลาดทั่วไป

เผยแพร่ในกลุ่ม
สวัสดี! ก่อนอื่นขอแสดงความยินดี: คุณมาถึงหัวข้อ Multithreading ใน Java แล้ว! นี่เป็นความสำเร็จที่จริงจัง ยังมีหนทางอีกยาวไกล แต่เตรียมตัวให้พร้อม นี่เป็นหนึ่งในหัวข้อที่ยากที่สุดในหลักสูตร และประเด็นก็คือไม่ใช่ว่ามีการใช้คลาสที่ซับซ้อนหรือวิธีการหลายวิธีที่นี่ แต่กลับมีไม่ถึงสองโหลด้วยซ้ำ ยิ่งต้องเปลี่ยนความคิดนิดหน่อย ก่อนหน้านี้ โปรแกรมของคุณถูกดำเนินการตามลำดับ โค้ดบางบรรทัดเป็นไปตามบรรทัดอื่น บางวิธีเป็นไปตามบรรทัดอื่น และโดยรวมแล้วทุกอย่างชัดเจน ขั้นแรก คำนวณบางอย่าง จากนั้นแสดงผลบนคอนโซล จากนั้นจึงยุติโปรแกรม หากต้องการทำความเข้าใจเกี่ยวกับมัลติเธรด วิธีที่ดีที่สุดคือคิดในแง่ของการทำงานพร้อมกัน เริ่มจากสิ่งง่ายๆ กันก่อน :) มัลติเธรดใน Java: สาระสำคัญข้อดีและข้อผิดพลาดทั่วไป - 1ลองนึกภาพว่าครอบครัวของคุณกำลังย้ายจากบ้านหลังหนึ่งไปอีกบ้านหนึ่ง ส่วนสำคัญของการขนย้ายคือการเก็บหนังสือของคุณ คุณได้สะสมหนังสือไว้มากมาย และคุณต้องใส่มันลงในกล่อง ตอนนี้มีเพียงคุณเท่านั้นที่เป็นอิสระ แม่กำลังเตรียมอาหาร พี่ชายกำลังเก็บเสื้อผ้า และน้องสาวไปที่ร้าน อย่างน้อยคุณสามารถจัดการคนเดียวได้และไม่ช้าก็เร็วคุณก็จะทำงานให้สำเร็จด้วยตัวเอง แต่จะใช้เวลานานมาก อย่างไรก็ตาม ภายใน 20 นาที น้องสาวของคุณจะกลับมาจากร้าน และเธอไม่มีอะไรทำอีกแล้ว เพื่อที่เธอจะได้เข้าร่วมกับคุณ งานยังคงเหมือนเดิม: ใส่หนังสือลงในกล่อง มันวิ่งเร็วเป็นสองเท่า ทำไม เพราะงานทำไปพร้อมๆ กัน “เธรด” สองอันที่แตกต่างกัน (คุณและน้องสาวของคุณ) ทำงานเดียวกันไปพร้อม ๆ กัน และหากไม่มีการเปลี่ยนแปลงใด ๆ ความแตกต่างของเวลาจะมีขนาดใหญ่มากเมื่อเทียบกับสถานการณ์ที่คุณจะทำทุกอย่างเพียงลำพัง ถ้าพี่ชายของคุณทำงานเสร็จเร็ว ๆ นี้ เขาจะช่วยคุณได้และทุกอย่างจะเร็วขึ้นไปอีก

ปัญหาที่มัลติเธรดแก้ไขใน Java

โดยพื้นฐานแล้ว Java multithreading ได้รับการคิดค้นขึ้นเพื่อแก้ไขปัญหาหลักสองประการ:
  1. ดำเนินการหลายอย่างพร้อมกัน

    ในตัวอย่างข้างต้น หัวข้อต่างๆ (เช่น สมาชิกในครอบครัว) ดำเนินการหลายอย่างพร้อมกัน เช่น ล้างจาน ไปที่ร้าน พับสิ่งของ

    สามารถให้ตัวอย่าง "โปรแกรมเมอร์" เพิ่มเติมได้ ลองนึกภาพว่าคุณมีโปรแกรมที่มีส่วนต่อประสานกับผู้ใช้ เมื่อคลิกปุ่มดำเนินการต่อ การคำนวณบางอย่างควรเกิดขึ้นภายในโปรแกรม และผู้ใช้ควรเห็นหน้าจออินเทอร์เฟซต่อไปนี้ หากดำเนินการเหล่านี้ตามลำดับหลังจากคลิกปุ่ม "ดำเนินการต่อ" โปรแกรมก็จะหยุดทำงาน ผู้ใช้จะเห็นหน้าจอเดียวกันกับปุ่ม "ดำเนินการต่อ" จนกว่าการคำนวณภายในทั้งหมดจะเสร็จสิ้นและโปรแกรมจะไปถึงส่วนที่จะเริ่มวาดอินเทอร์เฟซ

    รอสองสามนาที!

    มัลติเธรดใน Java: สาระสำคัญข้อดีและข้อผิดพลาดทั่วไป - 3

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

  2. เร่งความเร็วในการคำนวณ

    ทุกอย่างง่ายกว่ามากที่นี่ หากโปรเซสเซอร์ของเรามีหลายคอร์ และตอนนี้โปรเซสเซอร์ส่วนใหญ่เป็นแบบมัลติคอร์ รายการงานของเราสามารถแก้ไขได้แบบขนานด้วยคอร์หลายคอร์ แน่นอนว่าหากเราจำเป็นต้องแก้ปัญหา 1,000 ปัญหาและแต่ละปัญหาได้รับการแก้ไขในหนึ่งวินาที หนึ่งคอร์จะจัดการกับรายการได้ภายใน 1,000 วินาที สองคอร์ในเวลา 500 วินาที สามคอร์ในเวลาเพียง 333 วินาที และอื่นๆ

แต่ตามที่คุณอ่านในการบรรยายแล้ว ระบบสมัยใหม่นั้นฉลาดมากและแม้แต่ในคอร์ประมวลผลเดียวก็ยังสามารถนำความขนานหรือขนานเทียมมาใช้ได้ เมื่องานถูกดำเนินการสลับกัน เรามาย้ายจากสิ่งทั่วไปไปสู่สิ่งที่เฉพาะเจาะจงและทำความคุ้นเคยกับคลาสหลักในไลบรารี Java ที่เกี่ยวข้องกับมัลติเธรด - java.lang.Thread Threadพูดอย่างเคร่งครัด เธรดใน Java ถูกแสดงโดยอินสแตนซ์ของคลาส นั่นคือ ในการสร้างและรัน 10 เธรด คุณจะต้องมี 10 อ็อบเจ็กต์ของคลาสนี้ ลองเขียนตัวอย่างที่ง่ายที่สุด:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("I'm Thread! My name is " + getName());
   }
}
ในการสร้างและเปิดเธรด เราจำเป็นต้องสร้างคลาสและสืบทอดจากไฟล์java.lang. Threadและแทนที่วิธีการในrun()นั้น อันสุดท้ายมีความสำคัญมาก มันอยู่ในวิธีการที่run()เรากำหนดตรรกะที่เธรดของเราต้องดำเนินการ ตอนนี้ ถ้าเราสร้างอินสแตนซ์MyFirstThreadและรันมัน เมธอดrun()จะพิมพ์บรรทัดที่มีชื่อของมันไปที่คอนโซล: เมธอดgetName()จะพิมพ์ชื่อ “ระบบ” ของเธรด ซึ่งถูกกำหนดโดยอัตโนมัติ แม้ว่าที่จริงแล้วทำไมถึง “ถ้า”? มาสร้างและทดสอบกันเถอะ!
public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
เอาต์พุตคอนโซล: ฉันเป็นเธรด! ฉันชื่อ Thread-2 ฉัน Thread! ฉันชื่อ Thread-1 ฉันชื่อ Thread! ฉันชื่อ Thread-0 ฉันชื่อ Thread! ฉันชื่อเธรด-3 ฉันเธรด! ฉันชื่อเธรด-6 ฉันเธรด! ฉันชื่อ Thread-7 ฉันชื่อ Thread! ฉันชื่อ Thread-4 ฉันชื่อ Thread! ฉันชื่อ Thread-5 ฉันชื่อ Thread! ฉันชื่อเธรด-9 ฉันเธรด! ชื่อของฉันคือ Thread-8 เราสร้าง 10 เธรด (วัตถุ) MyFirstThreadที่สืบทอดมาThreadและเปิดใช้งานโดยการเรียกเมธอดของวัตถุ start()หลังจากเรียกเมธอดแล้วstart()เมธอดของมันก็เริ่มทำงานrun()และตรรกะที่เขียนไว้ก็ถูกดำเนินการ โปรดทราบ: ชื่อเธรดไม่อยู่ในลำดับ มันค่อนข้างแปลก ทำไมพวกเขาไม่ถูกประหารชีวิตตามลำดับ: Thread-0, Thread-1, Thread-2และอื่นๆ? นี่เป็นตัวอย่างที่ชัดเจนเมื่อการคิดแบบ "ต่อเนื่อง" แบบมาตรฐานจะไม่ทำงาน ความจริงก็คือในกรณีนี้เราจะออกคำสั่งเพื่อสร้างและเปิดใช้งาน 10 เธรดเท่านั้น ลำดับที่ควรเปิดตัวจะถูกตัดสินใจโดยตัวกำหนดตารางเวลาเธรด: กลไกพิเศษภายในระบบปฏิบัติการ โครงสร้างนี้มีโครงสร้างอย่างไรและหลักการใดในการตัดสินใจนั้นเป็นหัวข้อที่ซับซ้อนมากและเราจะไม่เจาะลึกในตอนนี้ สิ่งสำคัญที่ต้องจำคือโปรแกรมเมอร์ไม่สามารถควบคุมลำดับการทำงานของเธรดได้ เพื่อให้ทราบถึงความร้ายแรงของสถานการณ์ ให้ลองใช้วิธีการmain()จากตัวอย่างด้านบนอีกสองสามครั้ง เอาต์พุตคอนโซลที่สอง: ฉันเป็น Thread! ฉันชื่อ Thread-0 ฉันชื่อ Thread! ฉันชื่อ Thread-4 ฉันชื่อ Thread! ฉันชื่อเธรด-3 ฉันเธรด! ฉันชื่อ Thread-2 ฉัน Thread! ฉันชื่อ Thread-1 ฉันชื่อ Thread! ฉันชื่อ Thread-5 ฉันชื่อ Thread! ฉันชื่อเธรด-6 ฉันเธรด! ฉันชื่อ Thread-8 ฉันชื่อ Thread! ฉันชื่อเธรด-9 ฉันเธรด! ชื่อของฉันคือ Thread-7 เอาต์พุตคอนโซลที่สาม: ฉันชื่อ Thread! ฉันชื่อ Thread-0 ฉันชื่อ Thread! ฉันชื่อเธรด-3 ฉันเธรด! ฉันชื่อ Thread-1 ฉันชื่อ Thread! ฉันชื่อ Thread-2 ฉัน Thread! ฉันชื่อเธรด-6 ฉันเธรด! ฉันชื่อ Thread-4 ฉันชื่อ Thread! ฉันชื่อเธรด-9 ฉันเธรด! ฉันชื่อ Thread-5 ฉันชื่อ Thread! ฉันชื่อ Thread-7 ฉันชื่อ Thread! ฉันชื่อ กระทู้-8

ปัญหาที่มัลติเธรดสร้างขึ้น

ในตัวอย่างในหนังสือ คุณเห็นว่ามัลติเธรดช่วยแก้ปัญหาที่สำคัญได้ และการใช้งานทำให้โปรแกรมของเราเร็วขึ้น ในหลายกรณี-หลายครั้ง แต่ไม่ใช่เพื่ออะไรที่มัลติเธรดถือเป็นหัวข้อที่ซับซ้อน เพราะหากใช้ไม่ถูกต้องจะสร้างปัญหาแทนที่จะแก้ไข เมื่อฉันพูดว่า “สร้างปัญหา” ฉันไม่ได้หมายถึงสิ่งที่เป็นนามธรรม มีปัญหาเฉพาะสองประการที่อาจทำให้เกิดมัลติเธรด: การหยุดชะงักและสภาวะการแข่งขัน การหยุดชะงักเป็นสถานการณ์ที่หลายเธรดกำลังรอทรัพยากรที่ครอบครองโดยกันและกัน และไม่มีเธรดใดที่สามารถดำเนินการต่อไปได้ เราจะพูดถึงเรื่องนี้เพิ่มเติมในการบรรยายครั้งต่อไป แต่สำหรับตอนนี้ตัวอย่างนี้ก็เพียงพอแล้ว: มัลติเธรดใน Java: สาระสำคัญข้อดีและข้อผิดพลาดทั่วไป - 4 ลองนึกภาพว่า thread-1 กำลังทำงานกับ Object-1 บางตัว และ thread-2 กำลังทำงานกับ Object-2 โปรแกรมเขียนดังนี้:
  1. Thread-1 จะหยุดทำงานกับ Object-1 และสลับไปที่ Object-2 ทันทีที่ Thread-2 หยุดทำงานกับ Object 2 และสลับไปที่ Object-1
  2. Thread-2 จะหยุดทำงานกับ Object-2 และสลับไปที่ Object-1 ทันทีที่ Thread-1 หยุดทำงานกับ Object 1 และสลับไปที่ Object-2
แม้ว่าจะไม่มีความรู้เชิงลึกเกี่ยวกับมัลติเธรด คุณก็สามารถเข้าใจได้อย่างง่ายดายว่าจะไม่มีอะไรเกิดขึ้น สายใยไม่เคยเปลี่ยนสถานที่และจะรอกันและกันตลอดไป ข้อผิดพลาดดูเหมือนชัดเจน แต่ในความเป็นจริงกลับไม่เป็นเช่นนั้น คุณสามารถอนุญาตให้มันเข้าไปในโปรแกรมได้อย่างง่ายดาย เราจะดูตัวอย่างโค้ดที่ทำให้เกิดการหยุดชะงักในการบรรยายต่อไปนี้ อย่างไรก็ตาม Quora มีตัวอย่างในชีวิตจริงที่ยอดเยี่ยมที่อธิบาย ว่า การหยุดชะงัก คือ อะไร “ในบางรัฐในอินเดีย พวกเขาจะไม่ขายที่ดินเพื่อเกษตรกรรมให้คุณ เว้นแต่คุณจะจดทะเบียนเป็นเกษตรกร อย่างไรก็ตาม คุณจะไม่ได้รับการจดทะเบียนเป็นเกษตรกรหากคุณไม่ได้เป็นเจ้าของที่ดินเพื่อเกษตรกรรม” เยี่ยมมาก ฉันจะพูดอะไรได้! :) ตอนนี้เกี่ยวกับสภาพการแข่งขัน - สถานะของการแข่งขัน มัลติเธรดใน Java: สาระสำคัญข้อดีและข้อผิดพลาดทั่วไป - 5สภาวะการแข่งขันคือข้อบกพร่องด้านการออกแบบในระบบหรือแอปพลิเคชันแบบมัลติเธรด ซึ่งการทำงานของระบบหรือแอปพลิเคชันขึ้นอยู่กับลำดับที่ส่วนต่างๆ ของโค้ดถูกดำเนินการ จำตัวอย่างด้วยการรันเธรด:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Выполнен поток " + getName());
   }
}

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
ทีนี้ลองจินตนาการว่าโปรแกรมมีหน้าที่รับผิดชอบการทำงานของหุ่นยนต์ที่เตรียมอาหาร! Thread-0 นำไข่ออกจากตู้เย็น สตรีม 1 รอบบนเตา Stream-2 หยิบกระทะออกมาวางบนเตา สตรีม 3 จุดไฟบนเตา สตรีมที่ 4 เทน้ำมันลงในกระทะ สตรีม 5 ตอกไข่และเทลงในกระทะ สตรีม 6 โยนเชลล์ลงถังขยะ Stream-7 จะนำไข่กวนที่เสร็จแล้วออกจากเตา Potok-8 ตักไข่คนใส่จาน สตรีม-9 ล้างจาน ดูผลลัพธ์ของโปรแกรมของเรา: ดำเนินการ Thread-0 ดำเนินการเธรด-2 ดำเนินการเธรด ดำเนินการเธรด-1 ดำเนินการเธรด ดำเนินการเธรด-4 ดำเนินการเธรด เธรด-9 ดำเนินการเธรด ดำเนินการเธรด-5 ดำเนินการเธรด เธรด-8 ดำเนินการเธรด ดำเนินการเธรด-7 ดำเนินการเธรด เธรด-7 ดำเนินการเธรด -3 เธรด-6 ดำเนินการเธรด สคริปต์สนุกไหม? :) และทั้งหมดเป็นเพราะการทำงานของโปรแกรมของเราขึ้นอยู่กับลำดับการดำเนินการของเธรด เมื่อมีการละเมิดซีเควนซ์เพียงเล็กน้อย ห้องครัวของเราก็กลายเป็นนรก และหุ่นยนต์บ้าคลั่งก็ทำลายทุกสิ่งรอบตัว นี่เป็นปัญหาทั่วไปในการเขียนโปรแกรมแบบมัลติเธรดซึ่งคุณจะได้ยินมากกว่าหนึ่งครั้ง ในช่วงท้ายของการบรรยาย ผมอยากจะแนะนำหนังสือเกี่ยวกับมัลติเธรดให้กับคุณ
มัลติเธรดใน Java: สาระสำคัญข้อดีและข้อผิดพลาดทั่วไป - 6
“Java Concurrency in Practice” เขียนย้อนกลับไปในปี 2549 แต่ไม่ได้สูญเสียความเกี่ยวข้องไป โดยครอบคลุมถึงการเขียนโปรแกรมแบบมัลติเธรดใน Java โดยเริ่มจากพื้นฐานและลงท้ายด้วยรายการข้อผิดพลาดและการต่อต้านรูปแบบที่พบบ่อยที่สุด หากคุณตัดสินใจที่จะเป็นกูรูด้านการเขียนโปรแกรมแบบมัลติเธรด หนังสือเล่มนี้คือสิ่งที่คุณต้องอ่าน พบกันใหม่ในการบรรยายครั้งต่อไป! :)
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION