JavaRush /Java blogi /Random-UZ /Java-dagi boshi berk ko'cha va unga qarshi kurashish usul...
articles
Daraja

Java-dagi boshi berk ko'cha va unga qarshi kurashish usullari

Guruhda nashr etilgan
Ko'p bosqichli ilovalarni ishlab chiqishda ko'pincha dilemma paydo bo'ladi: bu dasturning ishonchliligi yoki ishlashi muhimroqdir. Misol uchun, biz ip xavfsizligi uchun sinxronizatsiyadan foydalanamiz va sinxronlash tartibi noto'g'ri bo'lsa, biz blokirovkalarni keltirib chiqarishimiz mumkin. Resurs iste'molini cheklash uchun biz ip hovuzlari va semaforlardan ham foydalanamiz va bu dizayndagi xato resurslar etishmasligi tufayli boshi berk ko'chaga olib kelishi mumkin. Ushbu maqolada biz o'likdan qanday qochish kerakligi, shuningdek, dasturning ishlashidagi boshqa muammolar haqida gapiramiz. Biz, shuningdek, o'lik holatlarida tiklanish imkoniyatiga ega bo'lgan tarzda ariza qanday yozilishi mumkinligini ko'rib chiqamiz. Java-dagi boshi berk ko'cha va unga qarshi kurashish usullari - 1O'tkir blokirovka - bu ba'zi resurslarni egallagan ikki yoki undan ortiq jarayonlar boshqa jarayonlar tomonidan egallangan boshqa resurslarni olishga harakat qiladigan va jarayonlarning hech biri kerakli resursni egallamasligi va shunga mos ravishda egallanganini bo'shata olmaydigan holat. Ushbu ta'rif juda umumiy va shuning uchun tushunish qiyin; yaxshiroq tushunish uchun biz misollar yordamida o'lik turlarini ko'rib chiqamiz.

Sinxronizatsiya tartibi O'zaro qulflash

Quyidagi vazifani ko'rib chiqing: ma'lum miqdordagi pulni bir hisobdan boshqasiga o'tkazish uchun operatsiyani amalga oshiradigan usulni yozishingiz kerak. Yechim quyidagicha ko'rinishi mumkin:
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);
			}
		}
	}
}
Bir qarashda, bu kod normal tarzda sinxronlashtiriladi; bizda manba hisobining holatini tekshirish va o'zgartirish va maqsadli hisobni o'zgartirish bo'yicha atom operatsiyasi mavjud. Biroq, ushbu sinxronizatsiya strategiyasi bilan o'lik vaziyat yuzaga kelishi mumkin. Keling, bu qanday sodir bo'lishining bir misolini ko'rib chiqaylik. Ikkita operatsiyani bajarish kerak: A hisobvarag'idan B hisobvarag'iga x pul o'tkazish va B hisobvarag'idan A hisob raqamiga y pul o'tkazish. Ko'pincha bu holat boshi berk ko'chaga olib kelmaydi, biroq baxtsiz vaziyatlarda 1 tranzaksiya A hisob qaydnomasi monitorini, 2 tranzaksiya B hisob monitorini egallaydi. Natijada boshi berk bo'ladi: 1 tranzaksiya hisob monitorini chiqarishni kutadi. B, lekin tranzaksiya 2 1-tranzaksiya bilan band bo'lgan A monitoriga kirishi kerak. Tugatishlar bilan bog'liq katta muammolardan biri shundaki, ularni testda topish oson emas. Hatto misolda tasvirlangan vaziyatda ham iplar bloklanmasligi mumkin, ya'ni bu holat doimiy ravishda takrorlanmaydi, bu diagnostikani sezilarli darajada murakkablashtiradi. Umuman olganda, tavsiflangan noaniq-determinizm muammosi ko'p ish zarralari uchun odatiy holdir (garchi bu buni osonlashtirmasa ham). Shuning uchun kodni ko'rib chiqish ko'p bosqichli ilovalar sifatini yaxshilashda muhim rol o'ynaydi, chunki u sinov paytida takrorlanishi qiyin bo'lgan xatolarni aniqlash imkonini beradi. Bu, albatta, dasturni sinab ko'rish kerak emas degani emas, biz kodni ko'rib chiqishni unutmasligimiz kerak. Ushbu kod blokirovkaga olib kelmasligi uchun nima qilishim kerak? Ushbu blokirovka hisobni sinxronlash boshqa tartibda sodir bo'lishi mumkinligi sababli yuzaga keladi. Shunga ko'ra, agar siz hisob-kitoblar bo'yicha qandaydir tartibni joriy qilsangiz (bu A hisobi B hisobidan kamroq ekanligini aytishga imkon beradigan ba'zi qoida), unda muammo bartaraf etiladi. Buni qanday qilish kerak? Birinchidan, agar hisoblarda qandaydir noyob identifikator (masalan, hisob raqami) raqamli, kichik harflar yoki boshqa tabiiy tartib tushunchasi bo'lsa (satrlarni leksikografik tartibda solishtirish mumkin), biz o'zimizni omadli deb hisoblashimiz mumkin va biz shunday qilamiz. har doim Biz birinchi navbatda kichikroq hisobning monitorini, keyin esa kattaroqini (yoki aksincha) egallashimiz mumkin.
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)}
			}
		}
	}
}
Ikkinchi variant, agar bizda bunday identifikator bo'lmasa, biz o'zimiz bilan chiqishimiz kerak. Biz, birinchi navbatda, ob'ektlarni xesh-kod bo'yicha solishtirishimiz mumkin. Katta ehtimol bilan ular boshqacha bo'ladi. Ammo ular bir xil bo'lib chiqsa-chi? Keyin sinxronlash uchun boshqa ob'ektni qo'shishingiz kerak bo'ladi. Bu biroz murakkab ko'rinishi mumkin, lekin nima qila olasiz? Bundan tashqari, uchinchi ob'ekt juda kam ishlatiladi. Natija quyidagicha ko'rinadi:
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)
				}
			}
		}
	}
}

Ob'ektlar orasidagi qulf

Ta'riflangan blokirovka shartlari tashxis qo'yish uchun eng oson o'lik holatini ifodalaydi. Ko'pincha ko'p tarmoqli ilovalarda turli ob'ektlar bir xil sinxronlashtirilgan bloklarga kirishga harakat qiladi. Bu blokirovkaga olib kelishi mumkin. Quyidagi misolni ko'rib chiqing: parvoz dispetcheri ilovasi. Samolyotlar nazoratchiga belgilangan manzilga yetib kelganini aytadi va qo‘nishga ruxsat so‘raydi. Kontroller o'z yo'nalishi bo'yicha uchayotgan samolyotlar haqidagi barcha ma'lumotlarni saqlaydi va xaritada ularning o'rnini chiza oladi.
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;
	}
}
Ushbu kodda boshi berk ko'chaga olib kelishi mumkin bo'lgan xatolik borligini tushunish avvalgisiga qaraganda qiyinroq. Bir qarashda, u qayta sinxronlashlarga ega emas, lekin bu unday emas. Ehtimol siz allaqachon setLocationsinf Planeva getMapsinf usullari Dispatchersinxronlashtirilganligini va boshqa sinflarning sinxronlashtirilgan usullarini o'z ichida chaqirishini payqagan bo'lsangiz kerak. Bu odatda yomon amaliyotdir. Buni qanday tuzatish mumkinligi keyingi bo'limda muhokama qilinadi. Natijada, agar samolyot manzilga yetib kelgan bo'lsa, kimdir kartani olishga qaror qilgan paytda, tiqilib qolishi mumkin. Ya'ni getMapva usullari chaqiriladi, ular navbati bilan monitorlarni va mos ravishda setLocationegallaydi . Keyin usul qo'ng'iroq qiladi (ayniqsa, hozir band bo'lgan misolda ), bu har bir misol uchun monitor bo'sh bo'lishini kutadi . Shu bilan birga, usul chaqiriladi , monitor misoli xaritani chizish bilan band bo'lib qoladi. Natijada boshi berk ko'chaga tushib qoladi. DispatcherPlanegetMapplane.getLocationPlanePlanesetLocationdispatcher.requestLandingDispatcher

Qo'ng'iroqlarni ochish

Oldingi bo'limda tasvirlangan holatlarga yo'l qo'ymaslik uchun boshqa ob'ektlarning usullariga umumiy qo'ng'iroqlardan foydalanish tavsiya etiladi. Ya'ni, sinxronlashtirilgan blokdan tashqaridagi boshqa ob'ektlarning usullarini chaqirish. Agar usullar ochiq qo'ng'iroqlar printsipi yordamida qayta yozilsa setLocation, getMapo'lik blokirovka qilish ehtimoli yo'qoladi. U, masalan, quyidagicha ko'rinadi:
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;
}

Resurs blokirovkasi

Bir vaqtning o'zida faqat bitta ipdan foydalanishi mumkin bo'lgan ba'zi manbalarga kirishga urinishda blokirovkalar ham paydo bo'lishi mumkin. Misol sifatida ma'lumotlar bazasiga ulanish puli bo'lishi mumkin. Agar ba'zi mavzular bir vaqtning o'zida ikkita ulanishga kirishi kerak bo'lsa va ular turli xil tartibda kirishsa, bu blokirovkaga olib kelishi mumkin. Asosan, bu turdagi qulflash sinxronizatsiya tartibini blokirovka qilishdan farq qilmaydi, bundan tashqari u ba'zi kodlarni bajarishga urinayotganda emas, balki resurslarga kirishga harakat qilganda sodir bo'ladi.

Qanday qilib o'liklardan qochish kerak?

Albatta, agar kod hech qanday xatosiz yozilgan bo'lsa (biz oldingi bo'limlarda ko'rgan misollar), unda hech qanday to'siqlar bo'lmaydi. Lekin uning kodi xatosiz yozilganiga kim kafolat bera oladi? Albatta, test xatolarning muhim qismini aniqlashga yordam beradi, lekin yuqorida aytib o'tganimizdek, ko'p tarmoqli koddagi xatolarni aniqlash oson emas va hatto sinovdan o'tgandan keyin ham boshi berk ko'chaga tushib qolish holatlari yo'qligiga ishonch hosil qila olmaysiz. Biz qandaydir tarzda o'zimizni blokirovka qilishdan himoya qila olamizmi? Javob ha. Shunga o'xshash usullar ma'lumotlar bazasi dvigatellarida qo'llaniladi, ular ko'pincha blokirovkalardan xalos bo'lishlari kerak (ma'lumotlar bazasidagi tranzaksiya mexanizmi bilan bog'liq). LockPaketda mavjud bo'lgan interfeys va uni amalga oshirish java.util.concurrent.locksusuli yordamida ushbu sinfning namunasi bilan bog'langan monitorni egallashga harakat qilish imkonini beradi tryLock(agar monitorni egallash mumkin bo'lsa, true qiymatini qaytaradi). Aytaylik, bizda interfeysni amalga oshiradigan bir juft ob'ekt bor Lockva biz ularning monitorlarini o'zaro blokirovkadan saqlaydigan tarzda egallashimiz kerak. Siz buni quyidagicha amalga oshirishingiz mumkin:
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();
			}
		}
	}
}
Ushbu dasturda ko'rib turganingizdek, biz ikkita monitorni egallab, o'zaro blokirovka qilish imkoniyatini yo'q qilamiz. E'tibor bering, blokirovka try- finallyzarur, chunki paketdagi sinflar java.util.concurrent.locksmonitorni avtomatik ravishda chiqarmaydi va agar sizning vazifangizni bajarish paytida biron bir istisno yuzaga kelsa, monitor qulflangan holatda qoladi. O'liklarni qanday aniqlash mumkin? JVM sizga blokirovkalarni diagnostika qilish imkonini beradi, ularni ish zarrachalarida ko'rsatish orqali. Bunday axlatlar ipning qanday holatda ekanligi haqidagi ma'lumotlarni o'z ichiga oladi. Agar u bloklangan bo'lsa, chiqindixonada ipning chiqarilishi kutilayotgan monitor haqidagi ma'lumotlar mavjud. Mavzularni tashlab yuborishdan oldin, JVM kutayotgan (band) monitorlar grafigiga qaraydi va agar u tsikllarni topsa, ishtirokchi monitorlar va iplarni ko'rsatuvchi blokirovka ma'lumotlarini qo'shadi. Tugallangan iplar to'plami quyidagicha ko'rinadi:
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)
Yuqoridagi dump ma'lumotlar bazasi bilan ishlaydigan ikkita ip bir-birini bloklaganligini aniq ko'rsatmoqda. Ushbu JVM funksiyasidan foydalangan holda blokirovkalarni diagnostika qilish uchun dasturning turli joylarida ipni tashlash operatsiyasiga qo'ng'iroqlarni joylashtirish va dasturni sinab ko'rish kerak. Keyinchalik, olingan jurnallarni tahlil qilishingiz kerak. Agar ular tiqilib qolganligini ko'rsatsa, axlatxonadan olingan ma'lumotlar uning yuzaga kelgan sharoitlarini aniqlashga yordam beradi. Umuman olganda, o'lik misollardagi kabi vaziyatlardan qochishingiz kerak. Bunday hollarda dastur katta ehtimol bilan barqaror ishlaydi. Ammo test va kodni tekshirish haqida unutmang. Bu, agar ular yuzaga kelsa, muammolarni aniqlashga yordam beradi. Agar siz blokirovka maydonini tiklash muhim bo'lgan tizimni ishlab chiqayotgan bo'lsangiz, siz "O'liklarni qanday oldini olish mumkin?" Bo'limida tavsiflangan usuldan foydalanishingiz mumkin. Bunday holda, dan lockInterruptiblyinterfeys usuli . Ushbu usul yordamida monitorni egallagan ipni to'xtatishga imkon beradi (va shu bilan monitorni bo'shatadi). Lockjava.util.concurrent.locks
Izohlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION