JavaRush /وبلاگ جاوا /Random-FA /بن بست در جاوا و روش های مبارزه با آن
articles
مرحله

بن بست در جاوا و روش های مبارزه با آن

در گروه منتشر شد
هنگام توسعه برنامه های کاربردی چند رشته ای، اغلب یک معضل پیش می آید: آنچه مهمتر است قابلیت اطمینان یا عملکرد برنامه است. به عنوان مثال برای ایمنی نخ از همگام سازی استفاده می کنیم و در مواردی که ترتیب همگام سازی درست نیست، می توانیم بن بست ایجاد کنیم. همچنین از Thread Pool و Semaphores برای محدود کردن مصرف منابع استفاده می کنیم و خطا در این طراحی می تواند به دلیل کمبود منابع منجر به بن بست شود. در این مقاله در مورد نحوه جلوگیری از بن بست و همچنین سایر مشکلات در عملکرد برنامه صحبت خواهیم کرد. همچنین بررسی خواهیم کرد که چگونه یک برنامه کاربردی می تواند به گونه ای نوشته شود که در موارد بن بست بتواند بازیابی شود. بن بست در جاوا و روش های مبارزه با آن - 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 به شما این امکان را می دهد که بن بست ها را با نمایش آن ها در thread dumps تشخیص دهید. چنین تخلیه‌هایی شامل اطلاعاتی در مورد وضعیتی است که نخ در آن قرار دارد. اگر مسدود شود، dump حاوی اطلاعاتی در مورد مانیتور است که نخ در انتظار انتشار آن است. قبل از ریختن نخ ها، JVM به نمودار مانیتورهای منتظر (مشغول) نگاه می کند و اگر چرخه هایی را پیدا کند، اطلاعات بن بست را اضافه می کند و مانیتورها و رشته های شرکت کننده را نشان می دهد. یک Dump از موضوعات بن بست مانند این است:
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)
Dump بالا به وضوح نشان می دهد که دو رشته که با پایگاه داده کار می کنند یکدیگر را مسدود کرده اند. برای تشخیص بن بست ها با استفاده از این قابلیت JVM، لازم است تا عملیات thread dump را در مکان های مختلف برنامه قرار داده و اپلیکیشن را تست کنید. در مرحله بعد، باید لاگ های به دست آمده را تجزیه و تحلیل کنید. اگر آنها نشان دهند که بن بست رخ داده است، اطلاعات حاصل از تخلیه به شناسایی شرایطی که در آن رخ داده است کمک می کند. به طور کلی، شما باید از موقعیت هایی مانند مواردی که در نمونه های بن بست وجود دارد اجتناب کنید. در چنین مواردی، برنامه به احتمال زیاد با ثبات کار می کند. اما تست و بررسی کد را فراموش نکنید. این به شناسایی مشکلات در صورت وقوع کمک می کند. در مواردی که در حال توسعه سیستمی هستید که بازیابی فیلد بن بست برای آن حیاتی است، می توانید از روشی که در بخش «چگونه از بن بست جلوگیری کنیم؟» استفاده کنید. lockInterruptiblyدر این مورد، روش رابط Lockاز بسته نیز ممکن است مفید باشد java.util.concurrent.locks. این به شما امکان می دهد با استفاده از این روش رشته ای را که مانیتور را اشغال کرده است قطع کنید (و در نتیجه مانیتور را آزاد کنید).
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION