JavaRush /مدونة جافا /Random-AR /الجمود في جافا وطرق مكافحته
articles
مستوى

الجمود في جافا وطرق مكافحته

نشرت في المجموعة
عند تطوير تطبيقات متعددة الخيوط، غالبًا ما تنشأ معضلة: الأهم هو موثوقية التطبيق أو أدائه. على سبيل المثال، نستخدم المزامنة لسلامة سلسلة المحادثات، وفي الحالات التي يكون فيها ترتيب المزامنة غير صحيح، يمكن أن نتسبب في حالة توقف تام. نستخدم أيضًا تجمعات الخيوط والإشارات للحد من استهلاك الموارد، وقد يؤدي أي خطأ في هذا التصميم إلى حالة توقف تام بسبب نقص الموارد. سنتحدث في هذه المقالة عن كيفية تجنب حالة الجمود، بالإضافة إلى مشاكل أخرى في أداء التطبيق. سننظر أيضًا في كيفية كتابة الطلب بطريقة تمكننا من التعافي في حالات الجمود. الجمود في جافا وطرق مكافحته - 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والفصل تتم مزامنتها وتستدعي الأساليب المتزامنة للفئات الأخرى داخل نفسها. هذه ممارسة سيئة بشكل عام. كيف يمكن تصحيح ذلك سيتم مناقشته في القسم التالي. ونتيجة لذلك، إذا وصلت الطائرة إلى الموقع، في اللحظة التي يقرر فيها شخص ما الحصول على البطاقة، يمكن أن يحدث طريق مسدود. وهذا يعني أنه سيتم استدعاء الأساليب التي ستشغل مراقبي المثيلات على التوالي. ستقوم الطريقة بعد ذلك باستدعاء (تحديدًا على المثيل المشغول حاليًا) والذي سينتظر حتى تصبح الشاشة مجانية لكل حالة من المثيلات . في الوقت نفسه، سيتم استدعاء الطريقة ، بينما يظل مراقب المثيل مشغولاً برسم الخريطة. والنتيجة هي طريق مسدود. getMapDispatchergetMapsetLocationDispatcherPlanegetMapplane.getLocationPlanePlanesetLocationdispatcher.requestLandingDispatcher

المكالمات المفتوحة

لتجنب مواقف مثل تلك الموصوفة في القسم السابق، يوصى باستخدام الاستدعاءات العامة لطرق كائنات أخرى. أي استدعاء أساليب كائنات أخرى خارج الكتلة المتزامنة. إذا تمت إعادة كتابة الأساليب باستخدام مبدأ المكالمات المفتوحة 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 إلى الرسم البياني لشاشات الانتظار (المشغولة)، وإذا وجد دورات، فإنه يضيف معلومات حالة توقف تام، مما يشير إلى الشاشات والخيوط المشاركة. يبدو تفريغ المواضيع التي وصلت إلى طريق مسدود كما يلي:
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