การแนะนำ
ดังนั้นเราจึงรู้ว่า Java มีเธรด คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ในบทวิจารณ์เรื่องBetter together: Java and the Thread class ส่วนที่ 1 — เธรดของการดำเนินการ เธรดจำเป็นสำหรับการทำงานแบบขนาน สิ่งนี้ทำให้มีโอกาสสูงที่เธรดจะโต้ตอบกันเอง มาดูกันว่าสิ่งนี้เกิดขึ้นได้อย่างไรและเรามีเครื่องมือพื้นฐานอะไรบ้างผลผลิต
Thread.yield()ทำให้ยุ่งเหยิงและไม่ค่อยได้ใช้ มีการอธิบายไว้หลายวิธีบนอินเทอร์เน็ต รวมถึงบางคนเขียนว่ามีคิวของเธรดซึ่งเธรดจะลดลงตามลำดับความสำคัญของเธรด คนอื่นเขียนว่าเธรดจะเปลี่ยนสถานะจาก "กำลังรัน" เป็น "รันได้" (แม้ว่าจะไม่มีความแตกต่างระหว่างสถานะเหล่านี้ เช่น Java ไม่ได้แยกความแตกต่างระหว่างสถานะเหล่านี้) ความจริงก็คือว่ามันเป็นที่รู้จักกันน้อยกว่าและยังง่ายกว่าในแง่ มีข้อผิดพลาด ( JDK-6416721: (spec thread) Fix Thread.yield() javadoc ) ที่บันทึกไว้สำหรับyield()
เอกสารประกอบของวิธีการ ถ้าอ่านจะเห็นได้ชัดว่าyield()
จริง ๆ แล้วเมธอดให้คำแนะนำบางอย่างแก่ตัวกำหนดตารางเวลาเธรด Java ที่เธรดนี้สามารถให้เวลาดำเนินการน้อยลง แต่สิ่งที่เกิดขึ้นจริง เช่นว่าตัวกำหนดตารางเวลาจะทำตามคำแนะนำหรือไม่และโดยทั่วไปแล้วจะทำอะไรนั้น ขึ้นอยู่กับการนำ JVM ไปใช้งานและระบบปฏิบัติการ และอาจขึ้นอยู่กับปัจจัยอื่นๆ ด้วย ความสับสนทั้งหมดน่าจะเกิดจากการที่มัลติเธรดได้รับการคิดใหม่ในขณะที่ภาษา Java พัฒนาขึ้น อ่านเพิ่มเติมในภาพรวมที่นี่: บทนำสั้นๆ เกี่ยวกับ Java Thread.yield( )
นอน
เธรดสามารถเข้าสู่โหมดสลีประหว่างการดำเนินการ นี่เป็นประเภทการโต้ตอบที่ง่ายที่สุดกับเธรดอื่นๆ ระบบปฏิบัติการที่รันเครื่องเสมือน Java ที่รันโค้ด Java ของเรามีตัวกำหนดตารางเวลาเธรด ของตัวเอง ตัดสินใจว่าจะเริ่มเธรดใดและเมื่อใด โปรแกรมเมอร์ไม่สามารถโต้ตอบกับตัวกำหนดตารางเวลานี้ได้โดยตรงจากโค้ด Java ผ่าน JVM เท่านั้น เขาหรือเธอสามารถขอให้ตัวกำหนดตารางเวลาหยุดเธรดชั่วคราว กล่าวคือให้เข้าสู่โหมดสลีป คุณสามารถอ่านเพิ่มเติมได้ในบทความเหล่านี้: Thread.sleep()และHow Multithreading works คุณยังสามารถดูว่าเธรดทำงานอย่างไรในระบบปฏิบัติการ Windows: Internals of Windows Thread และตอนนี้เรามาดูด้วยตาของเราเอง บันทึกรหัสต่อไปนี้ในไฟล์ชื่อHelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
อย่างที่คุณเห็น เรามีงานที่รอ 60 วินาที หลังจากนั้นโปรแกรมจะสิ้นสุดลง เราคอมไพล์โดยใช้คำสั่ง " javac HelloWorldApp.java
" แล้วรันโปรแกรมโดยใช้ " java HelloWorldApp
" เป็นการดีที่สุดที่จะเริ่มต้นโปรแกรมในหน้าต่างแยกต่างหาก ตัวอย่างเช่น บน Windows จะเป็นดังนี้: start java HelloWorldApp
. เราใช้คำสั่ง jps เพื่อรับ PID (รหัสกระบวนการ) และเราเปิดรายการของเธรดด้วย " jvisualvm --openpid pid
: อย่างที่คุณเห็น ตอนนี้เธรดของเรามีสถานะ "สลีป" อันที่จริง มีวิธีที่สวยงามกว่านี้ในการช่วย กระทู้ของเรามีความฝันอันแสนหวาน:
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
คุณสังเกตไหมว่าเรากำลังจัดการInterruptedException
ทุกที่? มาทำความเข้าใจว่าทำไม
Thread.interrupt()
สิ่งนี้คือในขณะที่เธรดกำลังรอ/พักอยู่ อาจมีบางคนต้องการขัดจังหวะ ในกรณีนี้ เราจัดการกับไฟล์InterruptedException
. กลไกนี้ถูกสร้างขึ้นหลังจากThread.stop()
ประกาศเมธอดว่าเลิกใช้แล้ว กล่าวคือ ล้าสมัยและไม่เป็นที่พึงปรารถนา เหตุผลก็คือเมื่อมีstop()
การเรียกใช้เมธอด เธรดนั้นถูก "ฆ่า" ซึ่งคาดเดาไม่ได้อย่างมาก เราไม่สามารถทราบได้ว่าเธรดจะหยุดทำงานเมื่อใด และเราไม่สามารถรับประกันความสอดคล้องของข้อมูลได้ ลองนึกภาพว่าคุณกำลังเขียนข้อมูลลงในไฟล์ในขณะที่เธรดถูกฆ่า แทนที่จะหยุดเธรด ผู้สร้าง Java ตัดสินใจว่าจะมีเหตุผลมากกว่าที่จะบอกว่าควรถูกขัดจังหวะ วิธีการตอบสนองต่อข้อมูลนี้เป็นเรื่องของเธรดที่จะตัดสินใจเอง สำหรับรายละเอียดเพิ่มเติม โปรดอ่านเหตุใด Thread.stop จึงเลิกใช้งานบนเว็บไซต์ของออราเคิล ลองดูตัวอย่าง:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
ในตัวอย่างนี้ เราจะไม่รอ 60 วินาที เราจะแสดงข้อความ "ขัดจังหวะ" แทน นี่เป็นเพราะเราเรียกinterrupt()
เมธอดบนเธรด วิธีนี้ตั้งค่าสถานะภายในที่เรียกว่า "สถานะขัดจังหวะ" นั่นคือแต่ละเธรดมีแฟล็กภายในที่ไม่สามารถเข้าถึงได้โดยตรง แต่เรามีวิธีดั้งเดิมในการโต้ตอบกับแฟล็กนี้ แต่นั่นไม่ใช่วิธีเดียว เธรดอาจกำลังทำงานอยู่ ไม่ต้องรอบางสิ่ง เพียงแค่ดำเนินการ แต่อาจคาดหมายว่าคนอื่นจะต้องการยุติการทำงานในเวลาที่กำหนด ตัวอย่างเช่น:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
// Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
ในตัวอย่างข้างต้นwhile
การวนซ้ำจะดำเนินการจนกว่าเธรดจะถูกขัดจังหวะจากภายนอก สำหรับisInterrupted
แฟล็ก สิ่งสำคัญคือต้องรู้ว่าหากเราจับได้InterruptedException
แฟล็ก isInterrupted จะถูกรีเซ็ต และisInterrupted()
จะคืนค่าเป็นเท็จ คลาสเธรดยังมีเมธอดThread.interrupted()แบบสแตติกที่ใช้กับเธรดปัจจุบันเท่านั้น แต่วิธีนี้จะรีเซ็ตแฟล็กเป็นเท็จ! อ่านเพิ่มเติมในบทนี้ที่ชื่อว่าการ หยุดชะงักของเธรด
เข้าร่วม (รอให้เธรดอื่นเสร็จสิ้น)
การรอที่ง่ายที่สุดคือการรอให้เธรดอื่นเสร็จสิ้นpublic static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
ในตัวอย่างนี้ เธรดใหม่จะพัก 5 วินาที ในเวลาเดียวกัน เธรดหลักจะรอจนกว่าเธรดที่หลับอยู่จะตื่นขึ้นและเสร็จสิ้นการทำงาน หากคุณดูสถานะของเธรดใน JVisualVM จะมีลักษณะดังนี้: ด้วยเครื่องมือตรวจสอบ คุณสามารถดูได้ว่าเกิดอะไรขึ้นกับเธรด วิธี นี้join
ค่อนข้างง่าย เพราะเป็นเพียงวิธีการที่มีโค้ด Java ที่ดำเนินการwait()
ตราบเท่าที่เธรดที่ถูกเรียกใช้นั้นยังมีชีวิตอยู่ ทันทีที่เธรดตาย (เมื่อเสร็จสิ้นการทำงาน) การรอจะถูกขัดจังหวะ และนั่นคือความมหัศจรรย์ของjoin()
วิธีการนี้ มาดูสิ่งที่น่าสนใจที่สุดกันดีกว่า
เฝ้าสังเกต
มัลติเธรดรวมถึงแนวคิดของจอภาพ คำว่า monitor มาจากภาษาอังกฤษตามภาษาละตินในศตวรรษที่ 16 และแปลว่า "เครื่องมือหรืออุปกรณ์ที่ใช้สำหรับสังเกต ตรวจสอบ หรือเก็บบันทึกกระบวนการอย่างต่อเนื่อง" ในบริบทของบทความนี้ เราจะพยายามครอบคลุมข้อมูลพื้นฐาน สำหรับใครที่ต้องการรายละเอียดก็เข้าไปดูในลิงค์ได้เลย เราเริ่มต้นการเดินทางด้วย Java Language Specification (JLS): 17.1 การซิงโครไนซ์ มันบอกว่าต่อไปนี้: ปรากฎว่า Java ใช้กลไก "มอนิเตอร์" สำหรับการซิงโครไนซ์ระหว่างเธรด มอนิเตอร์เชื่อมโยงกับแต่ละออบเจกต์ และเธรดสามารถรับlock()
หรือรีลีสunlock()
ด้วย ต่อไป เราจะพบบทช่วยสอนบนเว็บไซต์ Oracle: Intrinsic Locks and Synchronization. บทช่วยสอนนี้ระบุว่าการ ซิงโครไนซ์ของ Java นั้นสร้างขึ้นโดยใช้เอนทิตีภายในที่เรียกว่าintrinsic lockหรือmonitor lock ล็อคนี้มักเรียกง่ายๆ ว่า " มอนิเตอร์ " นอกจากนี้เรายังเห็นอีกครั้งว่าทุกวัตถุใน Java มีการล็อคที่แท้จริงที่เกี่ยวข้อง คุณสามารถอ่าน Java - Intrinsic Locks and Synchronization ต่อไป สิ่งสำคัญคือต้องเข้าใจว่าวัตถุใน Java สามารถเชื่อมโยงกับจอภาพได้อย่างไร ใน Java แต่ละออบเจ็กต์มีส่วนหัวที่เก็บข้อมูลเมตาภายในซึ่งโปรแกรมเมอร์ไม่สามารถใช้งานจากโค้ดได้ แต่ต้องใช้เครื่องเสมือนเพื่อให้ทำงานกับออบเจกต์ได้อย่างถูกต้อง ส่วนหัวของวัตถุมี "เครื่องหมายคำ" ซึ่งมีลักษณะดังนี้:
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
ที่นี่ เธรดปัจจุบัน (เธรดที่รันโค้ดบรรทัดเหล่านี้) ใช้คีย์เวิร์ดsynchronized
เพื่อพยายามใช้มอนิเตอร์ที่เกี่ยวข้องกับobject"\
ตัวแปรเพื่อรับ / รับล็อค ถ้าไม่มีใครแย่งมอนิเตอร์ (เช่น ไม่มีใครรันโค้ดที่ซิงโครไนซ์โดยใช้ออบเจกต์เดียวกัน) ดังนั้น Java อาจพยายามทำการปรับให้เหมาะสมที่เรียกว่า "การล็อกแบบลำเอียง" แท็กที่เกี่ยวข้องและเรกคอร์ดเกี่ยวกับเธรดที่เป็นเจ้าของการล็อกจอภาพจะถูกเพิ่มลงในคำที่ทำเครื่องหมายในส่วนหัวของวัตถุ ซึ่งช่วยลดค่าใช้จ่ายที่ต้องใช้ในการล็อกจอภาพ หากก่อนหน้านี้เธรดอื่นเป็นเจ้าของจอภาพแสดงว่าการล็อคดังกล่าวไม่เพียงพอ JVM เปลี่ยนไปใช้การล็อกประเภทถัดไป: "การล็อกพื้นฐาน" ใช้การดำเนินการเปรียบเทียบและแลกเปลี่ยน (CAS) ยิ่งไปกว่านั้น คำทำเครื่องหมายของส่วนหัวของออบเจ็กต์เองจะไม่เก็บคำทำเครื่องหมายอีกต่อไป แต่เป็นการอ้างอิงถึงตำแหน่งที่จัดเก็บ และแท็กจะเปลี่ยนเพื่อให้ JVM เข้าใจว่าเรากำลังใช้การล็อกพื้นฐาน หากเธรดหลายเธรดแข่งขันกัน (ช่วงชิง) สำหรับมอนิเตอร์ (หนึ่งเธรดได้รับการล็อก และเธรดที่สองกำลังรอให้คลายล็อก) แท็กในคำทำเครื่องหมายจะเปลี่ยนไป และตอนนี้คำทำเครื่องหมายจะเก็บข้อมูลอ้างอิงไปยังมอนิเตอร์ เป็นวัตถุ — เอนทิตีภายในบางอย่างของ JVM ตามที่ระบุไว้ใน JDK Enchancement Proposal (JEP) สถานการณ์นี้ต้องการพื้นที่ในพื้นที่ Native Heap ของหน่วยความจำเพื่อจัดเก็บเอนทิตีนี้ การอ้างอิงถึงตำแหน่งหน่วยความจำของเอนทิตีภายในนี้จะถูกเก็บไว้ในคำทำเครื่องหมายของส่วนหัวของวัตถุ ดังนั้น มอนิเตอร์จึงเป็นกลไกสำหรับการซิงโครไนซ์การเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดต่างๆ JVM สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ และวินาทีกำลังรอให้ปลดล็อค) จากนั้นแท็กในมาร์กเวิร์ดจะเปลี่ยนไป และมาร์กเวิร์ดจะเก็บการอ้างอิงถึงมอนิเตอร์เป็นอ็อบเจกต์ — เอนทิตีภายในของ JVM ตามที่ระบุไว้ใน JDK Enchancement Proposal (JEP) สถานการณ์นี้ต้องการพื้นที่ในพื้นที่ Native Heap ของหน่วยความจำเพื่อจัดเก็บเอนทิตีนี้ การอ้างอิงถึงตำแหน่งหน่วยความจำของเอนทิตีภายในนี้จะถูกเก็บไว้ในคำทำเครื่องหมายของส่วนหัวของวัตถุ ดังนั้น มอนิเตอร์จึงเป็นกลไกสำหรับการซิงโครไนซ์การเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดต่างๆ JVM สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ และวินาทีกำลังรอให้ปลดล็อค) จากนั้นแท็กในมาร์กเวิร์ดจะเปลี่ยนไป และมาร์กเวิร์ดจะเก็บการอ้างอิงถึงมอนิเตอร์เป็นอ็อบเจกต์ — เอนทิตีภายในของ JVM ตามที่ระบุไว้ใน JDK Enchancement Proposal (JEP) สถานการณ์นี้ต้องการพื้นที่ในพื้นที่ Native Heap ของหน่วยความจำเพื่อจัดเก็บเอนทิตีนี้ การอ้างอิงถึงตำแหน่งหน่วยความจำของเอนทิตีภายในนี้จะถูกเก็บไว้ในคำทำเครื่องหมายของส่วนหัวของวัตถุ ดังนั้น มอนิเตอร์จึงเป็นกลไกสำหรับการซิงโครไนซ์การเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดต่างๆ JVM สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ และตอนนี้คำทำเครื่องหมายจะเก็บการอ้างอิงถึงจอภาพเป็นวัตถุ — เอนทิตีภายในบางส่วนของ JVM ตามที่ระบุไว้ใน JDK Enchancement Proposal (JEP) สถานการณ์นี้ต้องการพื้นที่ในพื้นที่ Native Heap ของหน่วยความจำเพื่อจัดเก็บเอนทิตีนี้ การอ้างอิงถึงตำแหน่งหน่วยความจำของเอนทิตีภายในนี้จะถูกเก็บไว้ในคำทำเครื่องหมายของส่วนหัวของวัตถุ ดังนั้น มอนิเตอร์จึงเป็นกลไกสำหรับการซิงโครไนซ์การเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดต่างๆ JVM สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ และตอนนี้คำทำเครื่องหมายจะเก็บการอ้างอิงถึงจอภาพเป็นวัตถุ — เอนทิตีภายในบางส่วนของ JVM ตามที่ระบุไว้ใน JDK Enchancement Proposal (JEP) สถานการณ์นี้ต้องการพื้นที่ในพื้นที่ Native Heap ของหน่วยความจำเพื่อจัดเก็บเอนทิตีนี้ การอ้างอิงถึงตำแหน่งหน่วยความจำของเอนทิตีภายในนี้จะถูกเก็บไว้ในคำทำเครื่องหมายของส่วนหัวของวัตถุ ดังนั้น มอนิเตอร์จึงเป็นกลไกสำหรับการซิงโครไนซ์การเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดต่างๆ JVM สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ การอ้างอิงถึงตำแหน่งหน่วยความจำของเอนทิตีภายในนี้จะถูกเก็บไว้ในคำทำเครื่องหมายของส่วนหัวของวัตถุ ดังนั้น มอนิเตอร์จึงเป็นกลไกสำหรับการซิงโครไนซ์การเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดต่างๆ JVM สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ การอ้างอิงถึงตำแหน่งหน่วยความจำของเอนทิตีภายในนี้จะถูกเก็บไว้ในคำทำเครื่องหมายของส่วนหัวของวัตถุ ดังนั้น มอนิเตอร์จึงเป็นกลไกสำหรับการซิงโครไนซ์การเข้าถึงทรัพยากรที่ใช้ร่วมกันระหว่างเธรดต่างๆ JVM สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ
ซิงโครไนซ์ (รอล็อค)
ดังที่เราเห็นก่อนหน้านี้ แนวคิดของ "ซิงโครไนซ์บล็อก" (หรือ "ส่วนสำคัญ") นั้นเกี่ยวข้องอย่างใกล้ชิดกับแนวคิดของจอภาพ ลองดูตัวอย่าง:public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized(lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized(lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
ในที่นี้ เธรดหลักจะส่งวัตถุงานไปยังเธรดใหม่ก่อน จากนั้นจึงรับการล็อกทันทีและดำเนินการเป็นเวลานาน (8 วินาที) ตลอดเวลานี้ งานไม่สามารถดำเนินการต่อได้ เนื่องจากไม่สามารถเข้าสู่บล็อกได้synchronized
เนื่องจากล็อกได้รับมาแล้ว หากเธรดไม่สามารถล็อกได้ เธรดจะรอจอภาพ ทันทีที่ได้รับการล็อคก็จะดำเนินการต่อไป เมื่อเธรดออกจากมอนิเตอร์ มันจะคลายล็อค ใน JVisualVM จะมีลักษณะดังนี้ ดังที่คุณเห็นใน JVisualVM สถานะคือ "จอภาพ" หมายความว่าเธรดถูกบล็อกและไม่สามารถรับจอภาพได้ คุณยังสามารถใช้รหัสเพื่อระบุสถานะของเธรด แต่ชื่อของสถานะที่กำหนดด้วยวิธีนี้ไม่ตรงกับชื่อที่ใช้ใน JVisualVM แม้ว่าจะคล้ายกันก็ตาม ในกรณีนี้th1.getState()
คำสั่งในลูปจะส่งกลับBLOCKEDเนื่องจากตราบใดที่ลูปยังทำงานอยู่ เธรดlock
ของออบเจกต์จะถูกครอบครองmain
และth1
เธรดจะถูกบล็อกและไม่สามารถดำเนินการต่อได้จนกว่าจะคลายล็อก นอกจากบล็อกที่ซิงโครไนซ์แล้ว ยังสามารถซิงโครไนซ์เมธอดทั้งหมดได้อีกด้วย ตัวอย่างเช่น นี่คือเมธอดจากHashTable
คลาส:
public synchronized int size() {
return count;
}
วิธีนี้จะดำเนินการโดยเธรดเดียวเท่านั้นในเวลาใดก็ตาม เราต้องการล็อคหรือไม่? ใช่ เราต้องการมัน ในกรณีของเมธอดอินสแตนซ์ วัตถุ "นี้" (วัตถุปัจจุบัน) จะทำหน้าที่เป็นตัวล็อก มีการอภิปรายที่น่าสนใจเกี่ยวกับหัวข้อนี้ที่นี่: มีข้อได้เปรียบในการใช้วิธีซิงโครไนซ์แทนบล็อกซิงโครไนซ์หรือไม่? . หากเมธอดเป็นแบบสแตติก การล็อกจะไม่ใช่วัตถุ "นี้" (เนื่องจากไม่มีวัตถุ "นี้" สำหรับเมธอดแบบสแตติก) แต่เป็นวัตถุคลาส (เช่นInteger.class
)
รอ (รอจอภาพ) แจ้ง () และแจ้งทั้งหมด () วิธีการ
คลาสเธรดมีวิธีรออื่นที่เกี่ยวข้องกับมอนิเตอร์ ไม่เหมือนกับsleep()
และjoin()
ไม่สามารถเรียกเมธอดนี้ได้ง่ายๆ ชื่อของมันwait()
คือ เมธอด นี้wait
ถูกเรียกบนวัตถุที่เกี่ยวข้องกับจอภาพที่เราต้องการรอ ลองดูตัวอย่าง:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// The task object will wait until it is notified via lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// After we are notified, we will wait until we can acquire the lock
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// We sleep. Then we acquire the lock, notify, and release the lock
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
ใน JVisualVM จะมีลักษณะดังนี้: เพื่อให้เข้าใจวิธีการทำงาน โปรดจำไว้ว่าwait()
and notify()
วิธีการนั้นเชื่อมโยงกับjava.lang.Object
. อาจดูแปลกที่วิธีการเกี่ยวกับเธรดอยู่ในObject
ชั้นเรียน แต่เหตุผลที่ตอนนี้แฉ คุณจะจำได้ว่าทุกวัตถุใน Java มีส่วนหัว ส่วนหัวประกอบด้วยข้อมูลการดูแลทำความสะอาดต่างๆ รวมถึงข้อมูลเกี่ยวกับจอภาพ เช่น สถานะของการล็อก โปรดจำไว้ว่า แต่ละออบเจกต์หรืออินสแตนซ์ของคลาสเชื่อมโยงกับเอนทิตีภายในใน JVM ซึ่งเรียกว่าอินทรินซิกล็อกหรือมอนิเตอร์ ในตัวอย่างข้างต้น รหัสสำหรับวัตถุงานระบุว่าเราป้อนบล็อกซิงโครไนซ์สำหรับจอภาพที่เกี่ยวข้องกับlock
วัตถุ หากเราประสบความสำเร็จในการรับล็อคสำหรับจอภาพนี้wait()
ถูกเรียก. เธรดที่เรียกใช้งานจะปล่อยlock
มอนิเตอร์ของอ็อบเจ็กต์ แต่จะเข้าสู่คิวของเธรดที่รอการแจ้งเตือนจากlock
มอนิเตอร์ของอ็อบเจ็กต์ คิวของเธรดนี้เรียกว่า WAIT SET ซึ่งสะท้อนถึงจุดประสงค์ได้ถูกต้องกว่า นั่นคือมันเป็นชุดมากกว่าคิว เธรดmain
สร้างเธรดใหม่ด้วยวัตถุงาน เริ่มต้น และรอ 3 วินาที สิ่งนี้ทำให้มีโอกาสสูงที่เธรดใหม่จะสามารถรับการล็อกก่อนmain
เธรด และเข้าสู่คิวของมอนิเตอร์ หลังจากนั้นmain
เธรดจะเข้าสู่lock
บล็อกซิงโครไนซ์ของวัตถุและดำเนินการแจ้งเตือนเธรดโดยใช้จอภาพ หลังจากส่งการแจ้งเตือนแล้วmain
เธรดจะเผยแพร่lock
มอนิเตอร์ของออบเจกต์และเธรดใหม่ซึ่งก่อนหน้านี้กำลังรอให้lock
มอนิเตอร์ของอ็อบเจ็กต์ถูกปล่อยออก จะดำเนินการต่อไป เป็นไปได้ที่จะส่งการแจ้งเตือนไปยังเธรดเดียวเท่านั้น ( notify()
) หรือพร้อมกันไปยังเธรดทั้งหมดในคิว ( notifyAll()
) อ่านเพิ่มเติมที่นี่: ความแตกต่างระหว่าง alert() และ alertAll() ใน Java สิ่งสำคัญคือต้องสังเกตว่าลำดับการแจ้งเตือนขึ้นอยู่กับการนำ JVM ไปใช้ อ่านเพิ่มเติมที่นี่: วิธีแก้ปัญหาความอดอยากด้วยการแจ้งเตือนและการแจ้งเตือนทั้งหมด? . การซิงโครไนซ์สามารถทำได้โดยไม่ต้องระบุวัตถุ คุณสามารถทำได้เมื่อเมธอดทั้งหมดถูกซิงโครไนซ์แทนที่จะเป็นโค้ดบล็อกเดียว ตัวอย่างเช่น สำหรับเมธอดแบบสแตติก การล็อกจะเป็นออบเจกต์คลาส (ได้รับจาก.class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
ในแง่ของการใช้ล็อคทั้งสองวิธีจะเหมือนกัน หากเมธอดไม่คงที่ การซิงโครไนซ์จะดำเนินการโดยใช้ปัจจุบันinstance
นั่นคือการthis
ใช้ อย่างไรก็ตาม เราได้กล่าวไว้ก่อนหน้านี้ว่าคุณสามารถใช้getState()
เมธอดเพื่อรับสถานะของเธรดได้ ตัวอย่างเช่น สำหรับเธรดในคิวที่รอมอนิเตอร์ สถานะจะเป็น WAITING หรือ TIMED_WAITING หากเมธอดwait()
ระบุการหมดเวลา
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
วงจรชีวิตของเธรด
ตลอดอายุการใช้งาน สถานะของเธรดจะเปลี่ยนไป อันที่จริงแล้ว การเปลี่ยนแปลงเหล่านี้ประกอบด้วยวงจรชีวิตของเธรด ทันทีที่เธรดถูกสร้างขึ้น สถานะของเธรดจะเป็นใหม่ ในสถานะนี้ เธรดใหม่ยังไม่ได้รัน และตัวกำหนดตารางเวลาเธรด Java ยังไม่รู้อะไรเกี่ยวกับเธรดนี้ เพื่อให้ตัวกำหนดตารางเวลาของเธรดเรียนรู้เกี่ยวกับเธรด คุณต้องเรียกใช้thread.start()
เมธอด จากนั้นเธรดจะเปลี่ยนเป็นสถานะ RUNNABLE อินเทอร์เน็ตมีไดอะแกรมที่ไม่ถูกต้องจำนวนมากซึ่งแยกความแตกต่างระหว่างสถานะ "รันได้" และ "กำลังรัน" แต่นี่เป็นข้อผิดพลาด เนื่องจาก Java ไม่แยกความแตกต่างระหว่าง "พร้อมที่จะทำงาน" (รันได้) และ "กำลังทำงาน" (กำลังรัน) เมื่อเธรดมีชีวิตแต่ไม่แอ็คทีฟ (ไม่สามารถรันได้) จะอยู่ในหนึ่งในสองสถานะ:
- BLOCKED — กำลังรอเข้าสู่ส่วนสำคัญ เช่น
synchronized
บล็อก - WAITING — รอให้เธรดอื่นตรงตามเงื่อนไขบางอย่าง
getState()
เมธอด เธรดยังมีisAlive()
เมธอดซึ่งจะคืนค่าจริงหากเธรดไม่ถูก TERMINATED
LockSupport และเธรดที่จอดรถ
เริ่มต้นด้วย Java 1.6 กลไกที่น่าสนใจที่เรียกว่าLockSupportปรากฏขึ้น คลาสนี้เชื่อมโยง "อนุญาต" กับแต่ละเธรดที่ใช้ การเรียกใช้park()
เมธอดจะส่งคืนทันทีหากมีใบอนุญาต ซึ่งใช้ใบอนุญาตในกระบวนการ ไม่งั้นก็บล็อค การเรียกunpark
เมธอดจะทำให้ใบอนุญาตใช้ได้หากยังไม่มี มีใบอนุญาตเพียง 1 ใบเท่านั้น เอกสาร Java สำหรับLockSupport
การอ้างถึงSemaphore
คลาส ลองดูตัวอย่างง่ายๆ:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Request the permit and wait until we get it
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
รหัสนี้จะรอเสมอ เพราะตอนนี้สัญญาณมี 0 สิทธิ์ และเมื่อacquire()
ถูกเรียกในรหัส (เช่น ขออนุญาต) เธรดจะรอจนกว่าจะได้รับใบอนุญาต เนื่องจากเรากำลังรอเราจึงต้องInterruptedException
จัดการ ที่น่าสนใจคือสัญญาณได้รับสถานะของเธรดแยกต่างหาก หากเราดูใน JVisualVM เราจะเห็นว่าสถานะไม่ใช่ "รอ" แต่เป็น "จอด" ลองดูตัวอย่างอื่น:
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Park the current thread
System.err.println("Will be Parked");
LockSupport.park();
// As soon as we are unparked, we will start to act
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
สถานะของเธรดจะเป็น WAITING แต่ JVisualVM แยกความแตกต่างระหว่างwait
จากsynchronized
คีย์เวิร์ดและpark
จากLockSupport
คลาส ทำไมสิ่งนี้LockSupport
จึงสำคัญ เรากลับไปที่เอกสาร Java อีกครั้งและดูสถานะเธรดที่รอ อย่างที่คุณเห็นมีเพียงสามวิธีเท่านั้นที่จะเข้าไปได้ สองวิธีนั้นคือwait()
และjoin()
. และอย่างที่สามLockSupport
คือ ใน Java ยังสามารถสร้างการล็อกบนLockSuppor
t และเสนอเครื่องมือระดับสูงกว่าได้ ลองนำไปใช้กันดูนะครับ ตัวอย่างเช่น ลองดูที่ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
เช่นเดียวกับในตัวอย่างก่อนหน้านี้ ทุกอย่างเรียบง่ายที่นี่ วัตถุlock
กำลังรอให้ใครบางคนปล่อยทรัพยากรที่ใช้ร่วมกัน หากเราดูใน JVisualVM เราจะเห็นว่าเธรดใหม่จะถูกพักไว้จนกว่าmain
เธรดจะคลายล็อก คุณสามารถอ่านเพิ่มเติมเกี่ยวกับการล็อคได้ที่นี่: Java 8 StampedLocks vs. ReadWriteLocks and Synchronized and Lock API in Java เพื่อให้เข้าใจวิธีการล็อคได้ดีขึ้น คุณควรอ่านเกี่ยวกับ Phaser ในบทความนี้: Guide to the Java Phaser และเมื่อพูดถึงซิงโครไนซ์ต่างๆ คุณต้องอ่าน บทความ DZone เกี่ยวกับ The Java Synchronizers
บทสรุป
ในการตรวจสอบนี้ เราตรวจสอบวิธีการหลักที่เธรดโต้ตอบใน Java วัสดุเพิ่มเติม:- ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ 1 - เธรดของการดำเนินการ
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions