การแนะนำ
ดังนั้นเราจึงรู้ว่ามีเธรดใน Java ซึ่งคุณสามารถอ่านได้ในบทวิจารณ์ “ คุณไม่สามารถสปอย Java ด้วยเธรด: ตอนที่ 1 - เธรด ” จำเป็นต้องใช้เธรดในการทำงานพร้อมกัน ดังนั้นจึงมีความเป็นไปได้มากที่เธรดจะมีปฏิสัมพันธ์ซึ่งกันและกัน มาทำความเข้าใจกันว่ามันเกิดขึ้นได้อย่างไร และเรามีการควบคุมพื้นฐานอะไรบ้างผลผลิต
เมธอดThread.yield()นั้นลึกลับและไม่ค่อยมีใครใช้ มีคำอธิบายหลายรูปแบบบนอินเทอร์เน็ต จนถึงจุดที่บางคนเขียนเกี่ยวกับคิวของเธรดบางประเภท ซึ่งเธรดจะเลื่อนลงโดยคำนึงถึงลำดับความสำคัญ มีคนเขียนว่าเธรดจะเปลี่ยนสถานะจากรันเป็นรันได้ (แม้ว่าจะไม่มีการแบ่งออกเป็นสถานะเหล่านี้และ Java ไม่ได้แยกความแตกต่างระหว่างสถานะเหล่านั้น) แต่ในความเป็นจริงแล้ว ทุกอย่างไม่เป็นที่รู้จักมากนัก และในแง่หนึ่ง ง่ายกว่ามาก ในหัวข้อเอกสารวิธีการyield
มีข้อผิดพลาด " JDK-6416721: (spec thread) Fix Thread.yield() javadoc " หากคุณอ่านแล้ว จะเห็นได้ชัดว่าในความเป็นจริงแล้ว วิธีการนี้yield
เป็นเพียงคำแนะนำบางอย่างไปยังตัวกำหนดเวลาเธรด Java ว่าเธรดนี้สามารถให้เวลาดำเนินการน้อยลงได้ แต่สิ่งที่จะเกิดขึ้นจริง ไม่ว่าตัวกำหนดเวลาจะได้ยินคำแนะนำหรือไม่ และสิ่งที่จะทำโดยทั่วไปนั้นขึ้นอยู่กับการใช้งาน JVM และระบบปฏิบัติการ หรืออาจจะมาจากปัจจัยอื่นๆ ความสับสนทั้งหมดน่าจะเกิดจากการคิดใหม่เกี่ยวกับมัลติเธรดในระหว่างการพัฒนาภาษาจาวา คุณสามารถอ่านเพิ่มเติมได้ในบทวิจารณ์ " บทนำโดยย่อเกี่ยวกับ Java Thread.yield() "
นอน - กระทู้หลับ
เธรดอาจเข้าสู่โหมดสลีประหว่างการดำเนินการ นี่เป็นรูปแบบการโต้ตอบที่ง่ายที่สุดกับเธรดอื่น ระบบปฏิบัติการที่ติดตั้งเครื่องเสมือน Java ซึ่งมีการเรียกใช้โค้ด Java มีตัวกำหนดเวลาเธรดของตัวเองที่เรียกว่า Thread Scheduler เขาคือผู้ตัดสินใจว่าจะรันเธรดใดเมื่อใด โปรแกรมเมอร์ไม่สามารถโต้ตอบกับตัวกำหนดเวลานี้โดยตรงจากโค้ด Java แต่เขาสามารถขอให้ผู้กำหนดเวลาหยุดเธรดชั่วคราวครู่หนึ่งเพื่อ "ทำให้โหมดสลีป" คุณสามารถอ่านเพิ่มเติมได้ในบทความ " Thread.sleep() " และ " How Multithreading works " นอกจากนี้ คุณสามารถดูวิธีการทำงานของเธรดใน Windows OS: " 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("Waked 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("Waked up");
} catch (InterruptedException e) {
e.printStackTrace();
}
คุณอาจสังเกตเห็นว่าเราดำเนินการทุกที่InterruptedException
? มาทำความเข้าใจว่าทำไม
การขัดจังหวะเธรดหรือ Thread.interrupt
ประเด็นก็คือในขณะที่เธรดกำลังรออยู่ในความฝันบางคนอาจต้องการขัดขวางการรอนี้ ในกรณีนี้ เราจะจัดการกับข้อยกเว้นดังกล่าวThread.stop
สิ่งนี้เสร็จสิ้นหลังจาก ประกาศวิธี การเลิกใช้แล้ว เช่น ล้าสมัยและไม่พึงประสงค์สำหรับการใช้งาน เหตุผลก็คือเมื่อมีการเรียกใช้เมธอดstop
เธรดนั้นก็ "ถูกฆ่า" ซึ่งเป็นเรื่องที่คาดเดาไม่ได้มาก เราไม่สามารถรู้ได้ว่าโฟลว์จะหยุดเมื่อใด เราไม่สามารถรับประกันความสอดคล้องของข้อมูลได้ ลองนึกภาพว่าคุณกำลังเขียนข้อมูลลงในไฟล์แล้วกระแสข้อมูลจะถูกทำลาย ดังนั้นพวกเขาจึงตัดสินใจว่าจะเป็นเหตุผลมากกว่าที่จะไม่ฆ่าโฟลว์ แต่ต้องแจ้งให้ทราบว่าควรถูกขัดจังหวะ จะตอบสนองต่อสิ่งนี้อย่างไรก็ขึ้นอยู่กับกระแสของตัวเอง รายละเอียดเพิ่มเติมสามารถพบได้ใน " เหตุใด Thread.stop จึงเลิกใช้แล้ว " ของ Oracle ลองดูตัวอย่าง:
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 วินาที แต่จะพิมพ์ 'Interrupted' ทันที เนื่องจากเราเรียกเมธอดของ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 ที่ใช้กับเธรดปัจจุบันเท่านั้น - 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
ค่อนข้างง่าย เนื่องจากเป็นเพียงวิธีการที่มีโค้ดจาวาที่ดำเนินการwait
ในขณะที่เธรดที่ถูกเรียกนั้นยังมีชีวิตอยู่ เมื่อเธรดตาย (เมื่อสิ้นสุด) การรอจะสิ้นสุดลง นั่นคือความมหัศจรรย์ทั้งหมดของวิธีการjoin
นี้ ดังนั้นเรามาดูส่วนที่น่าสนใจที่สุดกันดีกว่า
การตรวจสอบแนวคิด
ในมัลติเธรดมีสิ่งเช่น Monitor โดยทั่วไปคำว่า Monitor แปลจากภาษาละตินว่า "overser" หรือ "overser" ภายในกรอบของบทความนี้เราจะพยายามจดจำสาระสำคัญและสำหรับผู้ที่ต้องการฉันขอให้คุณเจาะลึกเนื้อหาจากลิงก์เพื่อดูรายละเอียด มาเริ่มต้นการเดินทางของเราด้วยข้อกำหนดภาษา Java นั่นคือด้วย JLS: " 17.1. Synchronization " มันบอกว่าต่อไปนี้: ปรากฎว่าเพื่อวัตถุประสงค์ในการซิงโครไนซ์ระหว่างเธรด Java ใช้กลไกบางอย่างที่เรียกว่า "จอภาพ" แต่ละอ็อบเจ็กต์มีจอภาพเชื่อมโยงอยู่ และเธรดสามารถล็อคหรือปลดล็อคได้ ต่อไป เราจะพบบทช่วยสอนการฝึกอบรมบนเว็บไซต์ Oracle: “ Intrinsic Locks and Synchronization ” บทช่วยสอนนี้อธิบายว่าการซิงโครไนซ์ใน Java สร้างขึ้นโดยใช้เอนทิตีภายในที่เรียกว่าการล็อคภายในหรือการล็อคจอภาพ บ่อยครั้งที่การล็อคดังกล่าวเรียกง่ายๆว่า "จอภาพ" เรายังเห็นอีกครั้งว่าทุกอ็อบเจ็กต์ใน Java มีการล็อคภายในที่เชื่อมโยงอยู่ด้วย คุณสามารถอ่าน " Java - Intrinsic Locks and Synchronization " ถัดไป สิ่งสำคัญคือต้องเข้าใจว่าอ็อบเจ็กต์ใน Java สามารถเชื่อมโยงกับมอนิเตอร์ได้อย่างไร แต่ละวัตถุใน Java มีส่วนหัว - ประเภทของข้อมูลเมตาภายในที่โปรแกรมเมอร์ไม่สามารถใช้ได้กับโค้ด แต่เครื่องเสมือนจำเป็นต้องทำงานกับวัตถุอย่างถูกต้อง ส่วนหัวของวัตถุมี MarkWord ที่มีลักษณะดังนี้: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 สามารถลองดำเนินการปรับให้เหมาะสมที่เรียกว่า "การล็อกแบบลำเอียง" ชื่อของออบเจ็กต์ใน Mark Word จะมีแท็กที่เกี่ยวข้องและบันทึกว่าเธรดใดที่มอนิเตอร์แนบอยู่ ซึ่งจะช่วยลดค่าใช้จ่ายเมื่อจับภาพจอภาพ หากจอภาพเคยผูกเข้ากับเธรดอื่นมาก่อน การล็อคนี้จะไม่เพียงพอ JVM สลับไปที่ประเภทการล็อคถัดไป - การล็อคพื้นฐาน ใช้การดำเนินการเปรียบเทียบและสลับ (CAS) ในเวลาเดียวกันส่วนหัวใน Mark Word จะไม่จัดเก็บ Mark Word อีกต่อไป แต่ลิงก์ไปยังที่เก็บข้อมูล + แท็กมีการเปลี่ยนแปลงเพื่อให้ JVM เข้าใจว่าเรากำลังใช้การล็อคพื้นฐาน หากมีความขัดแย้งสำหรับมอนิเตอร์ของหลายเธรด (อันหนึ่งจับมอนิเตอร์และอันที่สองกำลังรอให้มอนิเตอร์ถูกปล่อย) จากนั้นแท็กใน Mark Word จะเปลี่ยนไปและ Mark Word จะเริ่มจัดเก็บการอ้างอิงไปยังมอนิเตอร์เป็น วัตถุ - เอนทิตีภายในบางส่วนของ JVM ตามที่ระบุไว้ใน JEP ในกรณีนี้ จำเป็นต้องมีพื้นที่ในพื้นที่หน่วยความจำ Native Heap เพื่อจัดเก็บเอนทิตีนี้ ลิงก์ไปยังตำแหน่งที่เก็บข้อมูลของเอนทิตีภายในนี้จะอยู่ในออบเจ็กต์ Mark Word ดังที่เราเห็นแล้วว่าจอภาพเป็นกลไกในการรับรองการซิงโครไนซ์การเข้าถึงเธรดหลายรายการไปยังทรัพยากรที่ใช้ร่วมกัน มีการใช้งานกลไกนี้หลายประการที่ 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 เรียกว่า "Monitor" เนื่องจากเธรดถูกบล็อกและไม่สามารถครอบครองจอภาพได้ คุณยังสามารถค้นหาสถานะของเธรดในโค้ดได้ แต่ชื่อของสถานะนี้ไม่ตรงกับเงื่อนไขของ JVisualVM แม้ว่าจะคล้ายกันก็ตาม ในกรณีนี้th1.getState()
การวนซ้ำfor
จะส่งคืนBLOCKEDเนื่องจาก ในขณะที่ลูปกำลังทำงาน เธรดlock
จะถูกครอบครองmain
โดยมอนิเตอร์ และเธรดth1
จะถูกบล็อกและไม่สามารถทำงานได้ต่อไปจนกว่าการล็อคจะถูกส่งกลับ นอกเหนือจากบล็อกการซิงโครไนซ์แล้ว วิธีการทั้งหมดยังสามารถซิงโครไนซ์ได้ ตัวอย่างเช่น วิธีการจากคลาสHashTable
:
public synchronized int size() {
return count;
}
ในหนึ่งหน่วยเวลา วิธีการนี้จะดำเนินการโดยเธรดเดียวเท่านั้น แต่เราต้องการล็อคใช่ไหม? ใช่ ฉันต้องการมัน ในกรณีของวิธีอ็อบเจ็กต์ การล็อคจะเป็นthis
. มีการสนทนาที่น่าสนใจในหัวข้อนี้: " มีข้อได้เปรียบในการใช้วิธีการซิงโครไนซ์แทนบล็อกซิงโครไนซ์หรือไม่ " หากวิธีการเป็นแบบคงที่ การล็อคจะไม่เป็นthis
(เนื่องจากสำหรับวิธีการแบบคงที่นั้นไม่สามารถเป็นthis
) แต่เป็นวัตถุคลาส (ตัวอย่างเช่นInteger.class
)
รอและรอบนมอนิเตอร์ วิธีการแจ้งและแจ้งทั้งหมด
เธรดมีวิธีรออื่นซึ่งเชื่อมต่อกับจอภาพ ไม่เหมือนsleep
และjoin
ไม่สามารถเรียกได้ และชื่อของเขาwait
คือ วิธีการนี้ดำเนินการwait
บนวัตถุที่เราต้องการรอจอภาพ ลองดูตัวอย่าง:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// task будет ждать, пока его не оповестят через lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// После оповещения нас мы будем ждать, пока сможем взять лок
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// Ждём и после этого забираем себе лок, оповещаем и отдаём лок
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
ใน JVisualVM จะมีลักษณะดังนี้: เพื่อให้เข้าใจถึงวิธีการทำงาน คุณควรจำไว้ว่าวิธีการนั้นwait
อ้างอิงnotify
ถึงjava.lang.Object
. ดูเหมือนแปลกที่วิธีการที่เกี่ยวข้องกับเธรดอยู่ในไฟล์Object
. แต่นี่คือคำตอบ อย่างที่เราจำได้ ทุกอ็อบเจ็กต์ใน Java มีส่วนหัว ส่วนหัวประกอบด้วยข้อมูลบริการต่างๆ รวมถึงข้อมูลเกี่ยวกับจอภาพ—ข้อมูลเกี่ยวกับสถานะการล็อค และอย่างที่เราจำได้ แต่ละอ็อบเจ็กต์ (เช่น แต่ละอินสแตนซ์) มีความเชื่อมโยงกับเอนทิตี JVM ภายในที่เรียกว่า intrinsic lock ซึ่งเรียกอีกอย่างว่ามอนิเตอร์ ในตัวอย่างข้างต้น งานอธิบายว่าเราเข้าสู่บล็อกการซิงโครไนซ์บนจอภาพที่เกี่ยวข้องlock
กับ หากเป็นไปได้ที่จะได้รับการล็อคบนจอภาพนี้wait
ดังนั้น เธรดที่ดำเนินการงานนี้จะเป็นการปล่อยมอนิเตอร์lock
แต่จะเข้าร่วมคิวของเธรดที่รอการแจ้งเตือนบนlock
มอนิเตอร์ คิวของเธรดนี้เรียกว่า WAIT-SET ซึ่งสะท้อนถึงสาระสำคัญได้แม่นยำยิ่งขึ้น มันเป็นชุดมากกว่าคิว เธรดmain
จะสร้างเธรดใหม่พร้อมกับงาน เริ่มต้นและรอเป็นเวลา 3 วินาที สิ่งนี้ทำให้มีความเป็นไปได้สูงที่เธรดใหม่จะคว้าล็อคก่อนเธรดmain
และเข้าคิวบนมอนิเตอร์ หลังจากนั้นเธรดmain
จะเข้าสู่บล็อกการซิงโครไนซ์lock
และดำเนินการแจ้งเตือนเธรดบนจอภาพ หลังจากส่งการแจ้งเตือนแล้ว เธรดmain
จะปล่อยมอนิเตอร์lock
และเธรดใหม่ (ซึ่งรอก่อนหน้านี้) lock
จะดำเนินการต่อไปหลังจากรอให้ปล่อยมอนิเตอร์ คุณสามารถส่งการแจ้งเตือนไปยังเธรดเดียวเท่านั้น ( notify
) หรือไปยังเธรดทั้งหมดในคิวพร้อมกัน ( notifyAll
) คุณสามารถอ่านเพิ่มเติมได้ใน " ความแตกต่างระหว่าง notify() และ notifyAll() ใน 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
ระบุขีดจำกัดเวลารอ
วงจรชีวิตของเธรด
ดังที่เราได้เห็นแล้วว่ากระแสจะเปลี่ยนสถานะในวิถีชีวิต โดยพื้นฐานแล้ว การเปลี่ยนแปลงเหล่านี้เป็นวงจรชีวิตของเธรด เมื่อเธรดถูกสร้างขึ้น เธรดนั้นจะมีสถานะใหม่ ในตำแหน่งนี้ ยังไม่ได้เริ่มต้น และ Java Thread Scheduler ยังไม่ทราบอะไรเกี่ยวกับเธรดใหม่ เพื่อให้ตัวกำหนดเวลาเธรดทราบเกี่ยวกับเธรด คุณต้องเรียกไฟล์thread.start()
. จากนั้นเธรดจะเข้าสู่สถานะ RUNNABLE มีโครงร่างที่ไม่ถูกต้องมากมายบนอินเทอร์เน็ตที่สถานะ Runnable และ Running ถูกแยกออกจากกัน แต่นี่เป็นข้อผิดพลาดเพราะ... Java ไม่ได้แยกความแตกต่างระหว่างสถานะ "พร้อมที่จะรัน" และ "กำลังทำงาน" เมื่อเธรดยังมีชีวิตอยู่แต่ไม่ได้ใช้งานอยู่ (ไม่ใช่ Runnable) เธรดนั้นจะอยู่ในสถานะใดสถานะหนึ่งจากสองสถานะ:
- BLOCKED - รอการเข้าสู่ส่วนที่ได้รับการป้องกันเช่น ไปที่
synchonized
บล็อก - กำลังรอ - รอเธรดอื่นตามเงื่อนไข หากเงื่อนไขเป็นจริง ตัวกำหนดเวลาเธรดจะเริ่มเธรด
getState
เมธอด เธรดยังมีวิธีisAlive
ที่คืนค่าเป็นจริงหากเธรดไม่ถูกยุติ
LockSupport และการจอดรถด้าย
ตั้งแต่ Java 1.6 มีกลไกที่น่าสนใจที่เรียกว่าLockSupport คลาสนี้เชื่อมโยง "ใบอนุญาต" หรือการอนุญาตกับแต่ละเธรดที่ใช้งาน การเรียกเมธอดpark
จะส่งกลับทันทีหากมีใบอนุญาต โดยจะใช้ใบอนุญาตเดียวกันนั้นในระหว่างการโทร มิฉะนั้นจะถูกบล็อก การเรียกวิธีการunpark
ทำให้ใบอนุญาตพร้อมใช้งานหากยังไม่มีให้ใช้งาน มีเพียง 1 ใบอนุญาตเท่านั้น ใน Java API แอป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) {
// Просим разрешение и ждём, пока не получим его
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
รหัสนี้จะรอตลอดไปเนื่องจากตอนนี้เซมาฟอร์มีใบอนุญาต 0 และเมื่อมีการเรียกใช้โค้ดacquire
(เช่น ขออนุญาต) เธรดจะรอจนกว่าจะได้รับสิทธิ์ เนื่องจากเรากำลังรอ เราจึงจำเป็นต้องดำเนินการกับInterruptedException
มัน สิ่งที่น่าสนใจคือเซมาฟอร์ใช้สถานะเธรดที่แยกจากกัน ถ้าเราดูใน JVisualVM เราจะเห็นว่าสถานะของเราไม่ใช่ Wait แต่เป็น Park ลองดูตัวอย่างอื่น:
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
//Запаркуем текущий поток
System.err.println("Will be Parked");
LockSupport.park();
// Как только нас распаркуют - начнём действовать
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);
}
สถานะของเธรดจะเป็นการรอ แต่ JVisualVM จะแยกความแตกต่างระหว่างwait
จากsynchronized
และpark
จาก LockSupport
เหตุใดสิ่งนี้จึงสำคัญมากLockSupport
? ลองกลับมาที่ Java API อีกครั้งและดูที่Thread State WAITING อย่างที่คุณเห็นมีเพียงสามวิธีเท่านั้นที่จะเข้าไปได้ 2 วิธี - นี้wait
และjoin
. และอันที่สามก็LockSupport
คือ การล็อกใน Java สร้างขึ้นบนหลักการเดียวกันLockSupport
และเป็นตัวแทนของเครื่องมือระดับที่สูงกว่า มาลองใช้กันดูครับ ลองดูตัวอย่างที่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 ตอนที่สอง การซิงโครไนซ์การเข้าถึงวัตถุที่ไม่แน่นอน " และ " Java Lock API ทฤษฎีและตัวอย่างการใช้งาน " เพื่อให้เข้าใจการใช้งานการล็อคได้ดีขึ้น การอ่านเกี่ยวกับ Phazer ในภาพรวม " Phaser Class " จะเป็นประโยชน์ และเมื่อพูดถึงซิงโครไนเซอร์ต่างๆ คุณต้องอ่านบทความเกี่ยวกับHabré “ Java.util.concurrent.* Synchronizers Reference ”
ทั้งหมด
ในการตรวจสอบนี้ เราได้พิจารณาถึงวิธีหลักๆ ที่เธรดโต้ตอบใน Java วัสดุเพิ่มเติม:- จอภาพ – แนวคิดพื้นฐานของการซิงโครไนซ์ Java
- การอ้างอิงซิงโครไนเซอร์ java.util.concurrent.*
- คำตอบสำหรับคำถามเกี่ยวกับมัลติเธรดในการสัมภาษณ์
GO TO FULL VERSION