JavaRush /จาวาบล็อก /Random-TH /คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ II - การซิงโครไนซ...
Viacheslav
ระดับ

คุณไม่สามารถทำลาย Java ด้วยเธรด: ตอนที่ II - การซิงโครไนซ์

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

การแนะนำ

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

ผลผลิต

เมธอดThread.yield()นั้นลึกลับและไม่ค่อยมีใครใช้ มีคำอธิบายหลายรูปแบบบนอินเทอร์เน็ต จนถึงจุดที่บางคนเขียนเกี่ยวกับคิวของเธรดบางประเภท ซึ่งเธรดจะเลื่อนลงโดยคำนึงถึงลำดับความสำคัญ มีคนเขียนว่าเธรดจะเปลี่ยนสถานะจากรันเป็นรันได้ (แม้ว่าจะไม่มีการแบ่งออกเป็นสถานะเหล่านี้และ Java ไม่ได้แยกความแตกต่างระหว่างสถานะเหล่านั้น) แต่ในความเป็นจริงแล้ว ทุกอย่างไม่เป็นที่รู้จักมากนัก และในแง่หนึ่ง ง่ายกว่ามาก คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 2ในหัวข้อเอกสารวิธีการ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Процесса: คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 3อย่างที่คุณเห็น เธรดของเราได้เข้าสู่สถานะสลีปแล้ว ในความเป็นจริงการนอนเธรดปัจจุบันสามารถทำได้สวยงามยิ่งขึ้น:
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 สถานะของเธรดจะเป็นดังนี้: คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 4ด้วยเครื่องมือตรวจสอบ คุณสามารถดูสิ่งที่เกิดขึ้นกับเธรดได้ วิธีการนี้joinค่อนข้างง่าย เนื่องจากเป็นเพียงวิธีการที่มีโค้ดจาวาที่ดำเนินการwaitในขณะที่เธรดที่ถูกเรียกนั้นยังมีชีวิตอยู่ เมื่อเธรดตาย (เมื่อสิ้นสุด) การรอจะสิ้นสุดลง นั่นคือความมหัศจรรย์ทั้งหมดของวิธีการjoinนี้ ดังนั้นเรามาดูส่วนที่น่าสนใจที่สุดกันดีกว่า

การตรวจสอบแนวคิด

ในมัลติเธรดมีสิ่งเช่น Monitor โดยทั่วไปคำว่า Monitor แปลจากภาษาละตินว่า "overser" หรือ "overser" ภายในกรอบของบทความนี้เราจะพยายามจดจำสาระสำคัญและสำหรับผู้ที่ต้องการฉันขอให้คุณเจาะลึกเนื้อหาจากลิงก์เพื่อดูรายละเอียด มาเริ่มต้นการเดินทางของเราด้วยข้อกำหนดภาษา Java นั่นคือด้วย JLS: " 17.1. Synchronization " มันบอกว่าต่อไปนี้: คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 5ปรากฎว่าเพื่อวัตถุประสงค์ในการซิงโครไนซ์ระหว่างเธรด Java ใช้กลไกบางอย่างที่เรียกว่า "จอภาพ" แต่ละอ็อบเจ็กต์มีจอภาพเชื่อมโยงอยู่ และเธรดสามารถล็อคหรือปลดล็อคได้ ต่อไป เราจะพบบทช่วยสอนการฝึกอบรมบนเว็บไซต์ Oracle: “ Intrinsic Locks and Synchronization ” บทช่วยสอนนี้อธิบายว่าการซิงโครไนซ์ใน Java สร้างขึ้นโดยใช้เอนทิตีภายในที่เรียกว่าการล็อคภายในหรือการล็อคจอภาพ บ่อยครั้งที่การล็อคดังกล่าวเรียกง่ายๆว่า "จอภาพ" เรายังเห็นอีกครั้งว่าทุกอ็อบเจ็กต์ใน Java มีการล็อคภายในที่เชื่อมโยงอยู่ด้วย คุณสามารถอ่าน " Java - Intrinsic Locks and Synchronization " ถัดไป สิ่งสำคัญคือต้องเข้าใจว่าอ็อบเจ็กต์ใน Java สามารถเชื่อมโยงกับมอนิเตอร์ได้อย่างไร แต่ละวัตถุใน Java มีส่วนหัว - ประเภทของข้อมูลเมตาภายในที่โปรแกรมเมอร์ไม่สามารถใช้ได้กับโค้ด แต่เครื่องเสมือนจำเป็นต้องทำงานกับวัตถุอย่างถูกต้อง ส่วนหัวของวัตถุมี MarkWord ที่มีลักษณะดังนี้: คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 6

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

บทความจาก Habr มีประโยชน์มากที่นี่: “ แต่มัลติเธรดทำงานอย่างไร ส่วนที่ 1: การซิงโครไนซ์ ” ในบทความนี้ ควรเพิ่มคำอธิบายจากสรุปบล็อกงานจาก Bugtaker JDK: “ JDK-8183909 ” คุณสามารถอ่านสิ่งเดียวกันได้ใน " JEP-8183909 " ดังนั้นใน Java จอภาพจะเชื่อมโยงกับวัตถุและเธรดสามารถบล็อกเธรดนี้ได้ หรือพวกเขายังพูดว่า "get a lock" ตัวอย่างที่ง่ายที่สุด:
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 สลับไปมา ดังนั้นเพื่อความง่าย เมื่อพูดถึงจอภาพ เรากำลังพูดถึงการล็อคจริงๆ คุณไม่สามารถทำลาย 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 เรียกว่า "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 จะมีลักษณะดังนี้: คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 10เพื่อให้เข้าใจถึงวิธีการทำงาน คุณควรจำไว้ว่าวิธีการนั้น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 ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 11

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

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

LockSupport และการจอดรถด้าย

ตั้งแต่ Java 1.6 มีกลไกที่น่าสนใจที่เรียกว่าLockSupport คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 12คลาสนี้เชื่อมโยง "ใบอนุญาต" หรือการอนุญาตกับแต่ละเธรดที่ใช้งาน การเรียกเมธอด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 คุณไม่สามารถทำลาย Java ด้วยเธรดได้: ตอนที่ II - การซิงโครไนซ์ - 13ลองดูตัวอย่างอื่น:
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 วัสดุเพิ่มเติม: #เวียเชสลาฟ
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION