JavaRush /จาวาบล็อก /Random-TH /การหยุดชะงักใน Java และวิธีการต่อสู้กับมัน
articles
ระดับ

การหยุดชะงักใน Java และวิธีการต่อสู้กับมัน

เผยแพร่ในกลุ่ม
เมื่อพัฒนาแอปพลิเคชันแบบมัลติเธรด มักเกิดปัญหาขึ้น: สิ่งที่สำคัญกว่าคือความน่าเชื่อถือหรือประสิทธิภาพของแอปพลิเคชัน ตัวอย่างเช่น เราใช้การซิงโครไนซ์เพื่อความปลอดภัยของเธรด และในกรณีที่ลำดับการซิงโครไนซ์ไม่ถูกต้อง เราอาจทำให้เกิดการหยุดชะงักได้ นอกจากนี้เรายังใช้กลุ่มเธรดและเซมาฟอร์เพื่อจำกัดการใช้ทรัพยากร และข้อผิดพลาดในการออกแบบนี้อาจนำไปสู่การหยุดชะงักเนื่องจากขาดทรัพยากร ในบทความนี้เราจะพูดถึงวิธีหลีกเลี่ยงการหยุดชะงักรวมถึงปัญหาอื่น ๆ ในประสิทธิภาพของแอปพลิเคชัน นอกจากนี้เรายังจะดูว่าแอปพลิเคชันสามารถเขียนในลักษณะที่สามารถกู้คืนในกรณีที่เกิดการหยุดชะงักได้อย่างไร การหยุดชะงักใน Java และวิธีการต่อสู้กับมัน - 1การหยุดชะงักเป็นสถานการณ์ที่กระบวนการตั้งแต่สองกระบวนการขึ้นไปครอบครองทรัพยากรบางส่วนกำลังพยายามรับทรัพยากรอื่น ๆ ที่ถูกครอบครองโดยกระบวนการอื่น และไม่มีกระบวนการใดที่สามารถครอบครองทรัพยากรที่พวกเขาต้องการได้ และด้วยเหตุนี้ จึงปล่อยกระบวนการที่ถูกครอบครองออกไป คำจำกัดความนี้กว้างเกินไปจึงเข้าใจยาก เพื่อความเข้าใจที่ดีขึ้น เราจะดูประเภทของการหยุดชะงักโดยใช้ตัวอย่าง

การซิงโครไนซ์คำสั่งการล็อคร่วมกัน

พิจารณางานต่อไปนี้: คุณต้องเขียนวิธีการดำเนินธุรกรรมเพื่อโอนเงินจำนวนหนึ่งจากบัญชีหนึ่งไปยังอีกบัญชีหนึ่ง วิธีแก้ปัญหาอาจมีลักษณะดังนี้:
public void transferMoney(Account fromAccount, Account toAccount, Amount amount) throws InsufficientFundsException {
	synchronized (fromAccount) {
		synchronized (toAccount) {
			if (fromAccount.getBalance().compareTo(amount) < 0)
				throw new InsufficientFundsException();
			else {
				fromAccount.debit(amount);
				toAccount.credit(amount);
			}
		}
	}
}
เมื่อดูแวบแรก รหัสนี้จะถูกซิงโครไนซ์ตามปกติ เรามีการดำเนินการแบบอะตอมมิกในการตรวจสอบและเปลี่ยนสถานะของบัญชีต้นทางและเปลี่ยนบัญชีปลายทาง อย่างไรก็ตาม ด้วยกลยุทธ์การซิงโครไนซ์นี้ สถานการณ์การชะงักงันอาจเกิดขึ้นได้ ลองดูตัวอย่างว่าสิ่งนี้เกิดขึ้นได้อย่างไร จำเป็นต้องทำธุรกรรมสองรายการ: โอนเงิน x เงินจากบัญชี A ไปยังบัญชี B และโอนเงิน y จากบัญชี B ไปยังบัญชี A บ่อยครั้งที่สถานการณ์นี้จะไม่ทำให้เกิดการหยุดชะงัก อย่างไรก็ตาม ในสถานการณ์ที่โชคร้าย ธุรกรรม 1 จะครอบครองการตรวจสอบบัญชี A ธุรกรรม 2 จะครอบครองการตรวจสอบบัญชี B ผลลัพธ์คือการหยุดชะงัก: ธุรกรรม 1 รอธุรกรรม 2 เพื่อปล่อยการตรวจสอบบัญชี B แต่ธุรกรรมที่ 2 ต้องเข้าถึงมอนิเตอร์ A ซึ่งถูกครอบครองโดยธุรกรรมที่ 1 ปัญหาใหญ่ประการหนึ่งเกี่ยวกับการหยุดชะงักคือไม่พบได้ง่ายในการทดสอบ แม้ในสถานการณ์ที่อธิบายไว้ในตัวอย่าง เธรดอาจไม่บล็อก นั่นคือ สถานการณ์นี้จะไม่ถูกทำซ้ำอย่างต่อเนื่อง ซึ่งทำให้การวินิจฉัยมีความซับซ้อนอย่างมาก โดยทั่วไป ปัญหาที่อธิบายไว้ของการไม่กำหนดระดับเป็นเรื่องปกติสำหรับมัลติเธรด (แม้ว่าจะไม่ได้ทำให้ง่ายขึ้นก็ตาม) ดังนั้นการตรวจสอบโค้ดจึงมีบทบาทสำคัญในการปรับปรุงคุณภาพของแอปพลิเคชันแบบมัลติเธรด เนื่องจากช่วยให้คุณระบุข้อผิดพลาดที่ยากต่อการทำซ้ำในระหว่างการทดสอบ แน่นอนว่านี่ไม่ได้หมายความว่าไม่จำเป็นต้องทดสอบแอปพลิเคชัน แต่เราไม่ควรลืมเกี่ยวกับการตรวจสอบโค้ด ฉันควรทำอย่างไรเพื่อป้องกันไม่ให้รหัสนี้ทำให้เกิดการหยุดชะงัก การบล็อกนี้เกิดจากการที่การซิงโครไนซ์บัญชีสามารถเกิดขึ้นได้ในลำดับอื่น ดังนั้น หากคุณแนะนำคำสั่งซื้อบางอย่างในบัญชี (นี่คือกฎบางอย่างที่อนุญาตให้คุณบอกว่าบัญชี A น้อยกว่าบัญชี B) ปัญหาก็จะหมดไป ทำอย่างไร? ประการแรก หากบัญชีมีตัวระบุที่ไม่ซ้ำกัน (เช่น หมายเลขบัญชี) ที่เป็นตัวเลข ตัวพิมพ์เล็ก หรืออย่างอื่นที่มีแนวคิดในการเรียงลำดับตามธรรมชาติ (สามารถเปรียบเทียบสตริงตามลำดับพจนานุกรมได้ เราก็ถือว่าเราโชคดี และเราจะ เสมอ เราสามารถครอบครองมอนิเตอร์ของบัญชีที่เล็กกว่าได้ก่อน แล้วตามด้วยบัญชีที่ใหญ่กว่า (หรือกลับกัน)
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromId= fromAcct.getId();
	int toId = fromAcct.getId();
	if (fromId < toId) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	} else  {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount)}
			}
		}
	}
}
ตัวเลือกที่สอง ถ้าเราไม่มีตัวระบุดังกล่าว เราจะต้องคิดขึ้นมาเอง ในการประมาณครั้งแรก เราสามารถเปรียบเทียบวัตถุด้วยรหัสแฮชได้ ส่วนใหญ่แล้วพวกเขาจะแตกต่างกัน แต่จะเกิดอะไรขึ้นถ้าพวกเขากลายเป็นคนเดียวกัน? จากนั้นคุณจะต้องเพิ่มวัตถุอื่นเพื่อการซิงโครไนซ์ อาจดูซับซ้อนเล็กน้อย แต่คุณจะทำอย่างไร? และอีกอย่างวัตถุชิ้นที่สามนั้นจะถูกใช้ค่อนข้างน้อย ผลลัพธ์จะมีลักษณะดังนี้:
private static final Object tieLock = new Object();
private void doTransfer(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	if (fromAcct.getBalance().compareTo(amount) < 0)
		throw new InsufficientFundsException();
	else {
		fromAcct.debit(amount);
		toAcct.credit(amount);
	}
}
public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException {
	int fromHash = System.identityHashCode(fromAcct);
	int toHash = System.identityHashCode(toAcct);
	if (fromHash < toHash) {
		synchronized (fromAcct) {
			synchronized (toAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else if (fromHash > toHash) {
		synchronized (toAcct) {
			synchronized (fromAcct) {
				doTransfer(fromAcct, toAcct, amount);
			}
		}
	} else {
		synchronized (tieLock) {
			synchronized (fromAcct) {
				synchronized (toAcct) {
					doTransfer(fromAcct, toAcct, amount)
				}
			}
		}
	}
}

การหยุดชะงักระหว่างวัตถุ

เงื่อนไขการบล็อกที่อธิบายไว้แสดงถึงกรณีการหยุดชะงักที่ง่ายที่สุดในการวินิจฉัย บ่อยครั้งในแอปพลิเคชันแบบมัลติเธรด อ็อบเจ็กต์ที่แตกต่างกันพยายามเข้าถึงบล็อกที่ซิงโครไนซ์เดียวกัน นี่อาจทำให้เกิดการหยุดชะงัก พิจารณาตัวอย่างต่อไปนี้: แอปพลิเคชันผู้มอบหมายงานเที่ยวบิน เครื่องบินจะบอกผู้ควบคุมเมื่อมาถึงจุดหมายปลายทางแล้วและขออนุญาตลงจอด ผู้ควบคุมจะจัดเก็บข้อมูลทั้งหมดเกี่ยวกับเครื่องบินที่บินไปในทิศทางของเขาและสามารถวางแผนตำแหน่งบนแผนที่ได้
class Plane {
	private Point location, destination;
	private final Dispatcher dispatcher;

	public Plane(Dispatcher dispatcher) {
		this.dispatcher = dispatcher;
	}
	public synchronized Point getLocation() {
		return location;
	}
	public synchronized void setLocation(Point location) {
		this.location = location;
		if (location.equals(destination))
		dispatcher.requestLanding(this);
	}
}

class Dispatcher {
	private final Set<Plane> planes;
	private final Set<Plane> planesPendingLanding;

	public Dispatcher() {
		planes = new HashSet<Plane>();
		planesPendingLanding = new HashSet<Plane>();
	}
	public synchronized void requestLanding(Plane plane) {
		planesPendingLanding.add(plane);
	}
	public synchronized Image getMap() {
		Image image = new Image();
		for (Plane plane : planes)
			image.drawMarker(plane.getLocation());
		return image;
	}
}
การทำความเข้าใจว่ามีจุดบกพร่องในโค้ดนี้ที่สามารถนำไปสู่การหยุดชะงักนั้นทำได้ยากกว่าโค้ดก่อนหน้า เมื่อมองแวบแรก จะไม่มีการซิงค์ซ้ำ แต่ก็ไม่เป็นเช่นนั้น คุณคงสังเกตเห็นแล้วว่าsetLocationคลาสPlaneและ เมธอด getMapคลาสนั้นDispatcherซิงโครไนซ์และเรียกเมธอดซิงโครไนซ์ของคลาสอื่นภายในตัวมันเอง นี่เป็นแนวทางปฏิบัติที่ไม่ดีโดยทั่วไป วิธีแก้ไขนี้จะกล่าวถึงในหัวข้อถัดไป ผลก็คือ หากเครื่องบินมาถึงสถานที่นั้น ทันทีที่มีคนตัดสินใจรับบัตร ก็อาจเกิดการหยุดชะงักได้ นั่นคือ วิธีการ getMapและ จะถูกเรียก setLocationซึ่งจะครอบครองการตรวจสอบอินสแตนซ์DispatcherและPlaneตามลำดับ จากนั้นเมธอดจะgetMapเรียก (โดยเฉพาะ บนplane.getLocationอินสแตนซ์Planeที่กำลังยุ่งอยู่) ซึ่งจะรอให้จอภาพว่างสำหรับแต่ละอินสแตนซ์ Planeในเวลาเดียวกัน วิธีการนี้setLocationจะถูกเรียกdispatcher.requestLandingในขณะที่ตัวตรวจสอบอินสแตนซ์Dispatcherยังคงยุ่งอยู่กับการวาดแผนที่ ผลลัพธ์ที่ได้คือการหยุดชะงัก

เปิดสาย

เพื่อหลีกเลี่ยงสถานการณ์เช่นเดียวกับที่อธิบายไว้ในส่วนก่อนหน้านี้ ขอแนะนำให้ใช้การเรียกสาธารณะไปยังเมธอดของอ็อบเจ็กต์อื่น นั่นคือการเรียกเมธอดของอ็อบเจ็กต์อื่นที่อยู่นอกบล็อกที่ซิงโครไนซ์ หากวิธีการถูกเขียนใหม่โดยใช้หลักการของการโทรแบบเปิดsetLocationความgetMapเป็นไปได้ของการหยุดชะงักจะหมดไป มันจะมีลักษณะเช่นนี้:
public void setLocation(Point location) {
	boolean reachedDestination;
	synchronized(this){
		this.location = location;
		reachedDestination = location.equals(destination);
	}
	if (reachedDestination)
		dispatcher.requestLanding(this);
}
………………………………………………………………………………
public Image getMap() {
	Set<Plane> copy;
	synchronized(this){
		copy = new HashSet<Plane>( planes);
	}
	Image image = new Image();
	for (Plane plane : copy)
		image.drawMarker(plane.getLocation());
	return image;
}

การหยุดชะงักของทรัพยากร

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

จะหลีกเลี่ยงการหยุดชะงักได้อย่างไร?

แน่นอนว่าหากเขียนโค้ดโดยไม่มีข้อผิดพลาด (ตัวอย่างที่เราเห็นในส่วนก่อนหน้า) ก็จะไม่มีการหยุดชะงักในนั้น แต่ใครจะรับประกันได้ว่าโค้ดของเขาเขียนโดยไม่มีข้อผิดพลาด? แน่นอนว่าการทดสอบช่วยในการระบุส่วนสำคัญของข้อผิดพลาด แต่อย่างที่เราได้เห็นก่อนหน้านี้ ข้อผิดพลาดในโค้ดแบบมัลติเธรดนั้นไม่ใช่เรื่องง่ายที่จะวินิจฉัย และแม้หลังจากการทดสอบแล้ว คุณก็ไม่สามารถแน่ใจได้ว่าจะไม่มีสถานการณ์การหยุดชะงัก เราสามารถป้องกันตัวเองจากการปิดกั้นได้หรือไม่? คำตอบคือใช่ เทคนิคที่คล้ายกันนี้ใช้ในเอ็นจิ้นฐานข้อมูล ซึ่งมักจะต้องกู้คืนจากการหยุดชะงัก (เกี่ยวข้องกับกลไกธุรกรรมในฐานข้อมูล) อินเทอร์เฟซLockและการใช้งานที่มีอยู่ในแพ็คเกจjava.util.concurrent.locksช่วยให้คุณพยายามครอบครองจอภาพที่เกี่ยวข้องกับอินสแตนซ์ของคลาสนี้โดยใช้วิธีการtryLock(คืนค่าจริงหากเป็นไปได้ที่จะครอบครองจอภาพ) สมมติว่าเรามีอ็อบเจ็กต์คู่หนึ่งที่ใช้อินเทอร์เฟซLockและเราจำเป็นต้องครอบครองมอนิเตอร์เพื่อหลีกเลี่ยงการบล็อกซึ่งกันและกัน คุณสามารถนำไปใช้ได้ดังนี้:
public void twoLocks(Lock A,  Lock B){
	while(true){
		if(A.tryLock()){
			if(B.tryLock())
			{
				try{
					//do something
				} finally{
					B.unlock();
					A.unlock();
				}
			} else{
				A.unlock();
			}
		}
	}
}
ดังที่คุณเห็นในโปรแกรมนี้ เราใช้จอภาพสองจอ ขณะเดียวกันก็ขจัดความเป็นไปได้ของการบล็อกซึ่งกันและกัน โปรดทราบว่าจำเป็นต้องมีการบล็อกtry- finallyเนื่องจากคลาสในแพ็คเกจjava.util.concurrent.locksไม่ปล่อยมอนิเตอร์โดยอัตโนมัติ และหากมีข้อยกเว้นบางประการเกิดขึ้นระหว่างการปฏิบัติงานของคุณ มอนิเตอร์จะติดอยู่ในสถานะล็อค จะวินิจฉัยการหยุดชะงักได้อย่างไร? JVM อนุญาตให้คุณวินิจฉัยการชะงักงันโดยการแสดงไว้ในเธรดดัมพ์ การดัมพ์ดังกล่าวรวมถึงข้อมูลเกี่ยวกับสถานะของเธรด หากถูกบล็อก ดัมพ์ประกอบด้วยข้อมูลเกี่ยวกับมอนิเตอร์ที่เธรดกำลังรอการปล่อย ก่อนที่จะดัมพ์เธรด JVM จะดูกราฟของการมอนิเตอร์ที่รอ (ไม่ว่าง) และหากพบรอบ JVM จะเพิ่มข้อมูลการหยุดชะงัก โดยระบุมอนิเตอร์และเธรดที่เข้าร่วม ดัมพ์ของเธรดที่ล็อคตายมีลักษณะดังนี้:
Found one Java-level deadlock:
=============================
"ApplicationServerThread":
waiting to lock monitor 0x0f0d80cc (a MyDBConnection),
which is held by "ApplicationServerThread"
"ApplicationServerThread":
waiting to lock monitor 0x0f0d8fed (a MyDBCallableStatement),
which is held by "ApplicationServerThread"
Java stack information for the threads listed above:
"ApplicationServerThread":
at MyDBConnection.remove_statement
- waiting to lock <0x6f50f730> (a MyDBConnection)
at MyDBStatement.close
- locked <0x604ffbb0> (a MyDBCallableStatement)
...
"ApplicationServerThread":
at MyDBCallableStatement.sendBatch
- waiting to lock <0x604ffbb0> (a MyDBCallableStatement)
at MyDBConnection.commit
- locked <0x6f50f730> (a MyDBConnection)
ดัมพ์ด้านบนแสดงให้เห็นอย่างชัดเจนว่าสองเธรดที่ทำงานกับฐานข้อมูลได้บล็อกซึ่งกันและกัน เพื่อวินิจฉัยการหยุดชะงักโดยใช้คุณลักษณะ JVM นี้ จำเป็นต้องทำการเรียกการดำเนินการดัมพ์เธรดในตำแหน่งต่างๆ ในโปรแกรมและทดสอบแอปพลิเคชัน ถัดไป คุณควรวิเคราะห์บันทึกผลลัพธ์ หากบ่งชี้ว่าเกิดการชะงักงันเกิดขึ้น ข้อมูลจากดัมพ์จะช่วยตรวจสอบเงื่อนไขที่เกิดขึ้น โดยทั่วไป คุณควรหลีกเลี่ยงสถานการณ์เช่นเดียวกับในตัวอย่างการหยุดชะงัก ในกรณีเช่นนี้ แอปพลิเคชันจะทำงานได้อย่างเสถียร แต่อย่าลืมเกี่ยวกับการทดสอบและการตรวจทานโค้ด ซึ่งจะช่วยระบุปัญหาหากเกิดขึ้น ในกรณีที่คุณกำลังพัฒนาระบบที่การกู้คืนฟิลด์การชะงักงันเป็นสิ่งสำคัญ คุณสามารถใช้วิธีที่อธิบายไว้ในส่วน “วิธีการหลีกเลี่ยงการชะงักงัน?” ในกรณีนี้ วิธีการ lockInterruptiblyอินเทอร์เฟซLockจากไฟล์java.util.concurrent.locks. ช่วยให้คุณสามารถขัดจังหวะเธรดที่ครอบครองมอนิเตอร์โดยใช้วิธีนี้ (และทำให้มอนิเตอร์ว่าง)
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION