Java 101: ทำความเข้าใจกับเธรด Java ตอนที่ 1: แนะนำเธรดและ runnables

บทความนี้เป็นบทความแรกในซีรีส์Java 101สี่ส่วนที่สำรวจเธรด Java แม้ว่าคุณอาจคิดว่าเธรดใน Java จะยากที่จะเข้าใจ แต่ฉันตั้งใจจะแสดงให้คุณเห็นว่าเธรดนั้นเข้าใจง่าย ในบทความนี้ฉันแนะนำคุณเกี่ยวกับเธรด Java และ runnables ในบทความต่อไปเราจะสำรวจการซิงโครไนซ์ (ผ่านการล็อก) ปัญหาการซิงโครไนซ์ (เช่นการหยุดชะงัก) กลไกการรอ / แจ้งเตือนการตั้งเวลา (มีและไม่มีลำดับความสำคัญ) การหยุดชะงักของเธรดตัวจับเวลาความผันผวนกลุ่มเธรดและตัวแปรภายในเธรด .

โปรดทราบว่าบทความนี้ (ส่วนหนึ่งของไฟล์เก็บถาวร JavaWorld) ได้รับการอัปเดตด้วยรายการโค้ดใหม่และซอร์สโค้ดที่ดาวน์โหลดได้ในเดือนพฤษภาคม 2013

การทำความเข้าใจเธรด Java - อ่านทั้งชุด

  • ส่วนที่ 1: แนะนำเธรดและ runnables
  • ส่วนที่ 2: การซิงโครไนซ์
  • ส่วนที่ 3: การตั้งเวลาเธรดและรอ / แจ้งเตือน
  • ส่วนที่ 4: กลุ่มเธรดและความผันผวน

เธรดคืออะไร?

แนวคิดของเธรดนั้นไม่ยากที่จะเข้าใจ: เป็นเส้นทางการทำงานที่เป็นอิสระผ่านโค้ดโปรแกรม เมื่อเธรดหลายเธรดดำเนินการเส้นทางของเธรดหนึ่งผ่านรหัสเดียวกันมักจะแตกต่างจากเธรดอื่น ๆ ตัวอย่างเช่นสมมติว่าเธรดหนึ่งรันโค้ดไบต์ที่เทียบเท่ากับifส่วนของคำสั่ง if-else ในขณะที่เธรดอื่นรันโค้ดไบต์ที่เทียบเท่ากับพาร์elseท JVM ติดตามการดำเนินการของแต่ละเธรดอย่างไร JVM ให้แต่ละเธรดเป็นสแต็กการเรียกใช้เมธอดของตัวเอง นอกเหนือจากการติดตามคำสั่งรหัสไบต์ปัจจุบันเมธอดคอลสแต็กยังติดตามตัวแปรโลคัลพารามิเตอร์ที่ JVM ส่งผ่านไปยังเมธอดและค่าส่งคืนของเมธอด

เมื่อหลายหัวข้อดำเนินการลำดับคำแนะนำไบต์รหัสในโปรแกรมเดียวกันซึ่งเป็นการกระทำที่เป็นที่รู้จักกันmultithreading มัลติเธรดมีประโยชน์ต่อโปรแกรมในรูปแบบต่างๆ:

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

Java ทำมัลติเธรดผ่านjava.lang.Threadคลาสได้สำเร็จ แต่ละThreadออบเจ็กต์อธิบายเธรดเดียวของการดำเนินการ การดำเนินการที่เกิดขึ้นในThread's run()วิธี เนื่องจากrun()เมธอดเริ่มต้นไม่ทำอะไรเลยคุณต้องคลาสย่อยThreadและลบล้างrun()เพื่อทำงานที่มีประโยชน์ให้สำเร็จ สำหรับรสชาติของเธรดและมัลติเธรดในบริบทของThreadให้ตรวจสอบ Listing 1:

รายการ 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

รายการ 1 แสดงซอร์สโค้ดไปยังแอปพลิเคชันที่ประกอบด้วยคลาสThreadDemoและMyThread. คลาสThreadDemoขับเคลื่อนแอ็พพลิเคชันโดยการสร้างMyThreadอ็อบเจ็กต์เริ่มเธรดที่เชื่อมโยงกับอ็อบเจ็กต์นั้นและรันโค้ดเพื่อพิมพ์ตารางสี่เหลี่ยม ในทางตรงกันข้ามMyThreadการแทนที่Thread's run()วิธีการพิมพ์ (บนกระแสออกมาตรฐาน) เป็นรูปสามเหลี่ยมมุมขวาประกอบด้วยอักขระเครื่องหมายดอกจัน

การตั้งเวลาเธรดและ JVM

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

เมื่อคุณพิมพ์java ThreadDemoเพื่อรันแอ็พพลิเคชัน JVM จะสร้างเธรดเริ่มต้นของการดำเนินการซึ่งเรียกใช้main()เมธอด โดยการดำเนินmt.start ();การเธรดเริ่มต้นจะบอกให้ JVM สร้างเธรดที่สองของการดำเนินการที่รันคำสั่งไบต์โค้ดที่ประกอบด้วยเมธอดMyThreadของอ็อบเจ็กต์ run()เมื่อstart()วิธีการคืนค่าเธรดเริ่มต้นจะเรียกใช้การforวนซ้ำเพื่อพิมพ์ตารางสี่เหลี่ยมในขณะที่เธรดใหม่เรียกใช้run()วิธีการพิมพ์สามเหลี่ยมมุมฉาก

ผลลัพธ์มีลักษณะอย่างไร? วิ่งThreadDemoไปหา. คุณจะสังเกตเห็นเอาต์พุตของแต่ละเธรดมีแนวโน้มที่จะสลับกับเอาต์พุตของอีกฝ่าย ผลลัพธ์นั้นเนื่องจากเธรดทั้งสองส่งเอาต์พุตไปยังสตรีมเอาต์พุตมาตรฐานเดียวกัน

คลาสเธรด

เพื่อเพิ่มความเชี่ยวชาญในการเขียนโค้ดแบบมัลติเธรดก่อนอื่นคุณต้องเข้าใจวิธีการต่างๆที่ประกอบเป็นThreadคลาส ส่วนนี้จะอธิบายวิธีการเหล่านั้นมากมาย โดยเฉพาะคุณได้เรียนรู้เกี่ยวกับวิธีการเริ่มต้นเธรดการตั้งชื่อเธรดการทำให้เธรดเข้าสู่โหมดสลีปการพิจารณาว่าเธรดยังมีชีวิตอยู่หรือไม่รวมเธรดหนึ่งกับเธรดอื่นและระบุเธรดที่แอ็คทีฟทั้งหมดในกลุ่มเธรดและกลุ่มย่อยของเธรดปัจจุบัน ฉันยังพูดคุยเกี่ยวกับความThreadช่วยเหลือในการดีบักและเธรดผู้ใช้กับเธรด daemon

ฉันจะนำเสนอวิธีการที่เหลือThreadในบทความต่อ ๆ ไปยกเว้นวิธีที่ Sun เลิกใช้แล้ว

เลิกใช้วิธีการ

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

การสร้างเธรด

Threadมีตัวสร้างแปดตัว ง่ายที่สุดคือ:

  • Thread()ซึ่งสร้างThreadวัตถุที่มีชื่อเริ่มต้น
  • Thread(String name)ซึ่งสร้างThreadวัตถุที่มีชื่อที่nameอาร์กิวเมนต์ระบุ

ก่อสร้างงานง่ายถัดไปและThread(Runnable target) Thread(Runnable target, String name)นอกเหนือจากRunnableพารามิเตอร์แล้วตัวสร้างเหล่านั้นยังเหมือนกับตัวสร้างดังกล่าวข้างต้น ความแตกต่าง: Runnableพารามิเตอร์ระบุอ็อบเจ็กต์ภายนอกThreadที่ให้run()เมธอด (คุณเรียนรู้เกี่ยวกับRunnableต่อไปในบทความนี้.) สี่คนสุดท้ายก่อสร้างคล้ายThread(String name), Thread(Runnable target)และThread(Runnable target, String name); อย่างไรก็ตามผู้สร้างขั้นสุดท้ายยังรวมถึงการThreadGroupโต้แย้งเพื่อวัตถุประสงค์ขององค์กร

ตัวสร้างหนึ่งในสี่ตัวสุดท้ายThread(ThreadGroup group, Runnable target, String name, long stackSize)มีความน่าสนใจตรงที่ช่วยให้คุณสามารถระบุขนาดที่ต้องการของสแต็กการเรียกใช้เมธอดของเธรดได้ ความสามารถในการระบุขนาดนั้นพิสูจน์ได้ว่ามีประโยชน์ในโปรแกรมด้วยวิธีการที่ใช้การเรียกซ้ำซึ่งเป็นเทคนิคการดำเนินการโดยวิธีการเรียกตัวเองซ้ำ ๆ เพื่อแก้ปัญหาบางอย่างได้อย่างสวยงาม โดยการกำหนดขนาดสแต็กอย่างชัดเจนบางครั้งคุณสามารถป้องกันStackOverflowErrors อย่างไรก็ตามขนาดที่ใหญ่เกินไปอาจส่งผลให้เกิดOutOfMemoryErrors นอกจากนี้ Sun ยังถือว่าขนาดของสแต็กการเรียกเมธอดนั้นขึ้นอยู่กับแพลตฟอร์ม ขนาดของสแต็กการเรียกเมธอดอาจเปลี่ยนไปทั้งนี้ขึ้นอยู่กับแพลตฟอร์ม Thread(ThreadGroup group, Runnable target, String name, long stackSize)ดังนั้นคิดอย่างรอบคอบเกี่ยวกับเครือข่ายในการเขียนโปรแกรมของคุณก่อนที่จะเขียนรหัสที่โทร

สตาร์ทยานพาหนะของคุณ

เธรดมีลักษณะคล้ายยานพาหนะ: ย้ายโปรแกรมตั้งแต่ต้นจนจบThreadและThreadอ็อบเจ็กต์คลาสย่อยไม่ใช่เธรด แต่จะอธิบายแอตทริบิวต์ของเธรดเช่นชื่อและมีโค้ด (ผ่านrun()วิธีการ) ที่เธรดดำเนินการ เมื่อถึงเวลาที่เธรดใหม่จะดำเนินการrun()เธรดอื่นจะเรียกใช้เมธอดThreadของอ็อบเจ็กต์ 's หรือ subclass start()ตัวอย่างเช่นในการเริ่มหัวข้อที่สองแอพลิเคชันเริ่มต้นด้ายที่รัน-callsmain() start()ในการตอบสนองโค้ดการจัดการเธรดของ JVM จะทำงานร่วมกับแพลตฟอร์มเพื่อให้แน่ใจว่าเธรดเริ่มต้นอย่างถูกต้องและเรียกใช้เมธอดThreadของอ็อบเจ็กต์หรือคลาสย่อยrun()

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

แผนภูมิแสดงช่วงเวลาสำคัญหลายช่วง:

  • การเริ่มต้นของเธรดเริ่มต้น
  • ช่วงเวลาที่เธรดเริ่มทำงาน main()
  • ช่วงเวลาที่เธรดเริ่มทำงาน start()
  • ช่วงเวลาstart()สร้างเธรดใหม่และกลับไปที่main()
  • การเริ่มต้นของเธรดใหม่
  • ช่วงเวลาที่เธรดใหม่เริ่มทำงาน run()
  • ช่วงเวลาที่แตกต่างกันในแต่ละเธรดจะสิ้นสุดลง

โปรดทราบว่าการเริ่มต้นของเธรดใหม่การดำเนินการrun()และการสิ้นสุดจะเกิดขึ้นพร้อมกันกับการดำเนินการของเธรดเริ่มต้น นอกจากนี้โปรดทราบว่าหลังจากการเรียกเธรดstart()แล้วการเรียกใช้เมธอดนั้นในภายหลังก่อนที่run()เมธอดจะออกจากสาเหตุstart()เพื่อโยนjava.lang.IllegalThreadStateExceptionวัตถุ

ชื่ออะไร?

ในระหว่างเซสชันการดีบักการแยกความแตกต่างของเธรดหนึ่งออกจากอีกเธรดหนึ่งในรูปแบบที่เป็นมิตรกับผู้ใช้จะเป็นประโยชน์ เพื่อแยกความแตกต่างระหว่างเธรด Java เชื่อมโยงชื่อกับเธรด ชื่อนั้นมีค่าเริ่มต้นThreadเป็นอักขระยัติภังค์และเลขจำนวนเต็มศูนย์ คุณสามารถยอมรับชื่อเธรดเริ่มต้นของ Java หรือคุณสามารถเลือกได้เอง เพื่อรองรับชื่อที่กำหนดเองThreadให้จัดเตรียมตัวสร้างที่รับnameอาร์กิวเมนต์และsetName(String name)วิธีการ ThreadยังมีgetName()วิธีที่ส่งคืนชื่อปัจจุบัน รายการ 2 แสดงให้เห็นถึงวิธีการสร้างชื่อที่กำหนดเองผ่านตัวThread(String name)สร้างและเรียกชื่อปัจจุบันในrun()วิธีการโดยการเรียกgetName():

รายการที่ 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

คุณสามารถส่งอาร์กิวเมนต์ชื่อทางเลือกไปยังMyThreadบรรทัดคำสั่ง ตัวอย่างเช่นjava NameThatThread Xสร้างXเป็นชื่อของเธรด หากคุณไม่สามารถระบุชื่อคุณจะเห็นผลลัพธ์ต่อไปนี้:

My name is: Thread-1

ถ้าคุณต้องการคุณสามารถเปลี่ยนsuper (name);การโทรในMyThread (String name)คอนสตรัคจะโทรไปsetName (String name)-as setName (name);ใน super (name);ที่เรียกวิธีหลังประสบความสำเร็จเป็นชื่อเดียวกันวัตถุประสงค์การจัดตั้งของเธรด ฉันปล่อยให้เป็นแบบฝึกหัดสำหรับคุณ

การตั้งชื่อหลัก

Java กำหนดชื่อmainให้กับเธรดที่รันmain()เมธอดเธรดเริ่มต้น โดยทั่วไปคุณจะเห็นชื่อนั้นในException in thread "main"ข้อความที่ตัวจัดการข้อยกเว้นดีฟอลต์ของ JVM พิมพ์เมื่อเธรดเริ่มต้นพ่นอ็อบเจ็กต์ข้อยกเว้น

จะนอนหรือไม่นอน

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

เพื่อแสดงให้เห็นsleep(long millis)ว่าฉันได้เขียนCalcPI1ใบสมัคร แอปพลิเคชันนั้นเริ่มเธรดใหม่ที่ใช้อัลกอริทึมทางคณิตศาสตร์เพื่อคำนวณค่าของ pi คงที่ทางคณิตศาสตร์ ในขณะที่การคำนวณหัวข้อใหม่ที่เริ่มต้นหยุดด้าย 10 sleep(long millis)มิลลิวินาทีโดยการเรียก หลังจากตื่นด้ายเริ่มต้นจะพิมพ์ค่า Pi piซึ่งร้านค้าหัวข้อใหม่ในตัวแปร รายการ 3 นำเสนอCalcPI1ซอร์สโค้ดของ:

รายการที่ 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

หากคุณรันโปรแกรมนี้คุณจะเห็นผลลัพธ์ที่คล้ายกัน (แต่อาจไม่เหมือนกัน) ดังต่อไปนี้:

pi = -0.2146197014017295 Finished calculating PI