CodeGym /จาวาบล็อก /สุ่ม /ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ II — การซิงโครไนซ์
John Squirrels
ระดับ
San Francisco

ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ II — การซิงโครไนซ์

เผยแพร่ในกลุ่ม

การแนะนำ

ดังนั้นเราจึงรู้ว่า Java มีเธรด คุณสามารถอ่านเกี่ยวกับเรื่องนี้ได้ในบทวิจารณ์เรื่องBetter together: Java and the Thread class ส่วนที่ 1 — เธรดของการดำเนินการ เธรดจำเป็นสำหรับการทำงานแบบขนาน สิ่งนี้ทำให้มีโอกาสสูงที่เธรดจะโต้ตอบกันเอง มาดูกันว่าสิ่งนี้เกิดขึ้นได้อย่างไรและเรามีเครื่องมือพื้นฐานอะไรบ้าง ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 1

ผลผลิต

Thread.yield()ทำให้ยุ่งเหยิงและไม่ค่อยได้ใช้ มีการอธิบายไว้หลายวิธีบนอินเทอร์เน็ต รวมถึงบางคนเขียนว่ามีคิวของเธรดซึ่งเธรดจะลดลงตามลำดับความสำคัญของเธรด คนอื่นเขียนว่าเธรดจะเปลี่ยนสถานะจาก "กำลังรัน" เป็น "รันได้" (แม้ว่าจะไม่มีความแตกต่างระหว่างสถานะเหล่านี้ เช่น Java ไม่ได้แยกความแตกต่างระหว่างสถานะเหล่านี้) ความจริงก็คือว่ามันเป็นที่รู้จักกันน้อยกว่าและยังง่ายกว่าในแง่ ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 2มีข้อผิดพลาด ( 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: ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 3อย่างที่คุณเห็น ตอนนี้เธรดของเรามีสถานะ "สลีป" อันที่จริง มีวิธีที่สวยงามกว่านี้ในการช่วย กระทู้ของเรามีความฝันอันแสนหวาน:

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 จะมีลักษณะดังนี้: ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 4ด้วยเครื่องมือตรวจสอบ คุณสามารถดูได้ว่าเกิดอะไรขึ้นกับเธรด วิธี นี้joinค่อนข้างง่าย เพราะเป็นเพียงวิธีการที่มีโค้ด Java ที่ดำเนินการwait()ตราบเท่าที่เธรดที่ถูกเรียกใช้นั้นยังมีชีวิตอยู่ ทันทีที่เธรดตาย (เมื่อเสร็จสิ้นการทำงาน) การรอจะถูกขัดจังหวะ และนั่นคือความมหัศจรรย์ของjoin()วิธีการนี้ มาดูสิ่งที่น่าสนใจที่สุดกันดีกว่า

เฝ้าสังเกต

มัลติเธรดรวมถึงแนวคิดของจอภาพ คำว่า monitor มาจากภาษาอังกฤษตามภาษาละตินในศตวรรษที่ 16 และแปลว่า "เครื่องมือหรืออุปกรณ์ที่ใช้สำหรับสังเกต ตรวจสอบ หรือเก็บบันทึกกระบวนการอย่างต่อเนื่อง" ในบริบทของบทความนี้ เราจะพยายามครอบคลุมข้อมูลพื้นฐาน สำหรับใครที่ต้องการรายละเอียดก็เข้าไปดูในลิงค์ได้เลย เราเริ่มต้นการเดินทางด้วย Java Language Specification (JLS): 17.1 การซิงโครไนซ์ มันบอกว่าต่อไปนี้: ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 5ปรากฎว่า Java ใช้กลไก "มอนิเตอร์" สำหรับการซิงโครไนซ์ระหว่างเธรด มอนิเตอร์เชื่อมโยงกับแต่ละออบเจกต์ และเธรดสามารถรับlock()หรือรีลีสunlock()ด้วย ต่อไป เราจะพบบทช่วยสอนบนเว็บไซต์ Oracle: Intrinsic Locks and Synchronization. บทช่วยสอนนี้ระบุว่าการ ซิงโครไนซ์ของ Java นั้นสร้างขึ้นโดยใช้เอนทิตีภายในที่เรียกว่าintrinsic lockหรือmonitor lock ล็อคนี้มักเรียกง่ายๆ ว่า " มอนิเตอร์ " นอกจากนี้เรายังเห็นอีกครั้งว่าทุกวัตถุใน Java มีการล็อคที่แท้จริงที่เกี่ยวข้อง คุณสามารถอ่าน Java - Intrinsic Locks and Synchronization ต่อไป สิ่งสำคัญคือต้องเข้าใจว่าวัตถุใน Java สามารถเชื่อมโยงกับจอภาพได้อย่างไร ใน Java แต่ละออบเจ็กต์มีส่วนหัวที่เก็บข้อมูลเมตาภายในซึ่งโปรแกรมเมอร์ไม่สามารถใช้งานจากโค้ดได้ แต่ต้องใช้เครื่องเสมือนเพื่อให้ทำงานกับออบเจกต์ได้อย่างถูกต้อง ส่วนหัวของวัตถุมี "เครื่องหมายคำ" ซึ่งมีลักษณะดังนี้: ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

นี่คือบทความ JavaWorld ที่มีประโยชน์มาก: เครื่องเสมือน Java ทำการซิงโครไนซ์เธรดอย่างไร บทความนี้ควรรวมกับคำอธิบายจากส่วน "สรุป" ของปัญหาต่อไปนี้ในระบบการติดตามจุดบกพร่องของ JDK: JDK- 8183909 คุณสามารถอ่านสิ่งเดียวกันได้ที่นี่: JEP- 8183909 ดังนั้น ใน Java มอนิเตอร์จะเชื่อมโยงกับออบเจกต์และใช้เพื่อบล็อกเธรดเมื่อเธรดพยายามรับ (หรือรับ) การล็อก นี่คือตัวอย่างที่ง่ายที่สุด:

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 สลับไปมาระหว่างการใช้งานกลไกนี้หลายอย่าง ดังนั้น เพื่อความง่าย เมื่อพูดถึงมอนิเตอร์ เรากำลังพูดถึงล็อคจริงๆ ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 7

ซิงโครไนซ์ (รอล็อค)

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

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 จะมีลักษณะดังนี้ ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 8ดังที่คุณเห็นใน 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 จะมีลักษณะดังนี้: ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 10เพื่อให้เข้าใจวิธีการทำงาน โปรดจำไว้ว่า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()ระบุการหมดเวลา ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 11

https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java

วงจรชีวิตของเธรด

ตลอดอายุการใช้งาน สถานะของเธรดจะเปลี่ยนไป อันที่จริงแล้ว การเปลี่ยนแปลงเหล่านี้ประกอบด้วยวงจรชีวิตของเธรด ทันทีที่เธรดถูกสร้างขึ้น สถานะของเธรดจะเป็นใหม่ ในสถานะนี้ เธรดใหม่ยังไม่ได้รัน และตัวกำหนดตารางเวลาเธรด Java ยังไม่รู้อะไรเกี่ยวกับเธรดนี้ เพื่อให้ตัวกำหนดตารางเวลาของเธรดเรียนรู้เกี่ยวกับเธรด คุณต้องเรียกใช้thread.start()เมธอด จากนั้นเธรดจะเปลี่ยนเป็นสถานะ RUNNABLE อินเทอร์เน็ตมีไดอะแกรมที่ไม่ถูกต้องจำนวนมากซึ่งแยกความแตกต่างระหว่างสถานะ "รันได้" และ "กำลังรัน" แต่นี่เป็นข้อผิดพลาด เนื่องจาก Java ไม่แยกความแตกต่างระหว่าง "พร้อมที่จะทำงาน" (รันได้) และ "กำลังทำงาน" (กำลังรัน) เมื่อเธรดมีชีวิตแต่ไม่แอ็คทีฟ (ไม่สามารถรันได้) จะอยู่ในหนึ่งในสองสถานะ:
  • BLOCKED — กำลังรอเข้าสู่ส่วนสำคัญ เช่นsynchronizedบล็อก
  • WAITING — รอให้เธรดอื่นตรงตามเงื่อนไขบางอย่าง
หากตรงตามเงื่อนไข ดังนั้นตัวกำหนดตารางเวลาของเธรดจะเริ่มต้นเธรด หากเธรดกำลังรอจนถึงเวลาที่กำหนด สถานะของเธรดคือ TIMED_WAITING หากเธรดไม่ทำงานอีกต่อไป (เสร็จสิ้นหรือมีข้อยกเว้นเกิดขึ้น) เธรดจะเข้าสู่สถานะ TERMINATED หากต้องการทราบสถานะของเธรด ให้ใช้getState()เมธอด เธรดยังมีisAlive()เมธอดซึ่งจะคืนค่าจริงหากเธรดไม่ถูก TERMINATED

LockSupport และเธรดที่จอดรถ

เริ่มต้นด้วย Java 1.6 กลไกที่น่าสนใจที่เรียกว่าLockSupportปรากฏขึ้น ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 12คลาสนี้เชื่อมโยง "อนุญาต" กับแต่ละเธรดที่ใช้ การเรียกใช้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 เราจะเห็นว่าสถานะไม่ใช่ "รอ" แต่เป็น "จอด" ดีกว่ากัน: Java และคลาสเธรด  ส่วนที่ II — การซิงโครไนซ์ - 13ลองดูตัวอย่างอื่น:

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 ยังสามารถสร้างการล็อกบน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 StampedLocks vs. ReadWriteLocks and Synchronized and Lock API in Java เพื่อให้เข้าใจวิธีการล็อคได้ดีขึ้น คุณควรอ่านเกี่ยวกับ Phaser ในบทความนี้: Guide to the Java Phaser และเมื่อพูดถึงซิงโครไนซ์ต่างๆ คุณต้องอ่าน บทความ DZone เกี่ยวกับ The Java Synchronizers

บทสรุป

ในการตรวจสอบนี้ เราตรวจสอบวิธีการหลักที่เธรดโต้ตอบใน Java วัสดุเพิ่มเติม: ดีกว่ากัน: Java และคลาสเธรด ส่วนที่ 1 — เธรดของการดำเนินการ ดีขึ้นด้วยกัน: Java และคลาสเธรด ส่วนที่ 3 — ปฏิสัมพันธ์ ร่วมกันได้ดีขึ้น: Java และคลาสเธรด ตอนที่ IV — Callable, Future และผองเพื่อน อยู่ด้วยกันดีกว่า: Java และคลาส Thread ส่วนที่ V — Executor, ThreadPool, Fork/Jin Better together: Java และคลาส Thread ตอนที่ VI — ยิงออกไป!
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION