JavaRush /Java Blogu /Random-AZ /Java-da çıxılmaz vəziyyət və onunla mübarizə üsulları
articles
Səviyyə

Java-da çıxılmaz vəziyyət və onunla mübarizə üsulları

Qrupda dərc edilmişdir
Çox yivli proqramlar hazırlayarkən tez-tez dilemma yaranır: daha vacib olan proqramın etibarlılığı və ya performansıdır. Məsələn, biz mövzu təhlükəsizliyi üçün sinxronizasiyadan istifadə edirik və sinxronizasiya sırasının səhv olduğu hallarda, biz kilidlərə səbəb ola bilərik. Biz həmçinin resurs istehlakını məhdudlaşdırmaq üçün mövzu hovuzlarından və semaforlardan istifadə edirik və bu dizayndakı səhv resursların olmaması səbəbindən dalana səbəb ola bilər. Bu yazıda biz çıxılmaz vəziyyətdən necə qaçmaq, eləcə də tətbiqin işində digər problemlər haqqında danışacağıq. Müraciətin çıxılmaz vəziyyətlərdə bərpa oluna biləcək şəkildə necə yazıla biləcəyinə də baxacağıq. Java-da çıxılmaz vəziyyət və onunla mübarizə üsulları - 1Deadlock, bəzi resursları əhatə edən iki və ya daha çox prosesin digər proseslər tərəfindən işğal edilmiş bəzi digər resursları əldə etməyə çalışdığı və proseslərin heç birinin ehtiyac duyduğu resursu tuta bilmədiyi və müvafiq olaraq işğal edilmiş resursu buraxdığı bir vəziyyətdir. Bu tərif çox ümumidir və buna görə də başa düşmək çətindir; daha yaxşı başa düşmək üçün nümunələrdən istifadə edərək dalana dirənmə növlərinə baxacağıq.

Sinxronizasiya Sifarişi Qarşılıqlı Kilidləmə

Aşağıdakı tapşırığı nəzərdən keçirin: bir hesabdan digərinə müəyyən miqdarda pul köçürmək üçün əməliyyat həyata keçirən bir üsul yazmalısınız. Həll belə görünə bilər:
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);
			}
		}
	}
}
İlk baxışdan bu kod olduqca normal şəkildə sinxronlaşdırılır; mənbə hesabının vəziyyətini yoxlamaq və dəyişdirmək və təyinat hesabını dəyişdirmək üçün atomik əməliyyatımız var. Bununla belə, bu sinxronizasiya strategiyası ilə çıxılmaz vəziyyət yarana bilər. Bunun necə baş verdiyinə dair bir nümunəyə baxaq. İki əməliyyat etmək lazımdır: A hesabından B hesabına x pul köçürmək və B hesabından A hesabına y pul köçürmək. Çox vaxt bu vəziyyət dalana səbəb olmur, lakin uğursuz şəraitdə 1-ci əməliyyat A hesab monitorunu, 2-ci əməliyyat B hesab monitorunu tutacaq. Nəticə dalana dirənir: 1-ci əməliyyat 2-ci əməliyyatın hesab monitorunun buraxılmasını gözləyir. B, lakin tranzaksiya 2 1-ci tranzaksiya ilə məşğul olan A monitoruna daxil olmalıdır. Kilidlərlə bağlı böyük problemlərdən biri sınaq zamanı onları tapmaq asan olmamasıdır. Nümunədə təsvir olunan vəziyyətdə belə, iplər bloklana bilməz, yəni bu vəziyyət daim təkrarlanmayacaq, bu da diaqnostikanı əhəmiyyətli dərəcədə çətinləşdirir. Ümumiyyətlə, təsvir edilmiş qeyri-determinizm problemi çoxilliklər üçün xarakterikdir (baxmayaraq ki, bu işi asanlaşdırmır). Buna görə də, kodun nəzərdən keçirilməsi çox yivli tətbiqlərin keyfiyyətinin yaxşılaşdırılmasında mühüm rol oynayır, çünki bu, sınaq zamanı təkrar istehsalı çətin olan səhvləri müəyyən etməyə imkan verir. Bu, əlbəttə ki, tətbiqin sınaqdan keçirilməsinə ehtiyac olmadığını ifadə etmir; sadəcə kodun nəzərdən keçirilməsini unutmamalıyıq. Bu kodun blokadaya səbəb olmasının qarşısını almaq üçün nə etməliyəm? Bu bloklama hesab sinxronizasiyasının fərqli qaydada baş verə biləcəyi ilə əlaqədardır. Müvafiq olaraq, əgər siz hesablar üzrə bəzi qaydalar tətbiq etsəniz (bu, A hesabının B hesabından az olduğunu söyləməyə imkan verən bəzi qaydadır), onda problem aradan qaldırılacaq. Bunu necə etmək olar? Birincisi, əgər hesabların bir növ unikal identifikatoru (məsələn, hesab nömrəsi) ədədi, kiçik hərf və ya təbii qayda anlayışı olan başqa hərflər varsa (sətirlər leksikoqrafik ardıcıllıqla müqayisə oluna bilər), onda biz özümüzü şanslı hesab edə bilərik və biz bunu edəcəyik. həmişə Biz əvvəlcə kiçik hesabın monitorunu, sonra isə daha böyük hesabın monitorunu tuta bilərik (və ya əksinə).
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)}
			}
		}
	}
}
İkinci seçim, əgər belə bir identifikatorumuz yoxdursa, biz bunu özümüz hazırlamalıyıq. Biz ilk təxmini olaraq obyektləri hash kodu ilə müqayisə edə bilərik. Çox güman ki, onlar fərqli olacaqlar. Bəs onların eyni olduğu ortaya çıxsa nə olacaq? Sonra sinxronizasiya üçün başqa bir obyekt əlavə etməli olacaqsınız. Bir az mürəkkəb görünə bilər, amma nə edə bilərsən? Bundan əlavə, üçüncü obyekt olduqca nadir hallarda istifadə olunacaq. Nəticə belə görünəcək:
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)
				}
			}
		}
	}
}

Obyektlər arasında çıxılmaz vəziyyət

Təsvir edilən bloklama şərtləri diaqnostika üçün ən asan çıxılmaz vəziyyətdir. Çox vaxt çox yivli proqramlarda müxtəlif obyektlər eyni sinxronlaşdırılmış bloklara daxil olmağa çalışırlar. Bu, dalana səbəb ola bilər. Aşağıdakı nümunəyə nəzər salın: uçuş dispetçer tətbiqi. Təyyarələr təyinat yerinə çatdıqda nəzarətçiyə xəbər verir və eniş üçün icazə tələb edirlər. Nəzarətçi onun istiqamətində uçan təyyarələr haqqında bütün məlumatları saxlayır və onların mövqeyini xəritədə çəkə bilir.
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;
	}
}
Bu kodda dalana səbəb ola biləcək bir səhvin olduğunu başa düşmək əvvəlkindən daha çətindir. İlk baxışdan onun yenidən sinxronizasiyası yoxdur, lakin bu belə deyil. setLocationYəqin ki, artıq sinif PlanegetMapsinif metodlarının Dispatchersinxronlaşdırıldığını və öz daxilində digər siniflərin sinxronlaşdırılmış üsullarını çağırdığını fərq etmisiniz . Bu ümumiyyətlə pis təcrübədir. Bunun necə düzəldiləcəyi növbəti hissədə müzakirə olunacaq. Nəticədə, əgər təyyarə yerə çatarsa, kiminsə kartı almağa qərar verdiyi anda dalana dirəniş yarana bilər. Yəni, müvafiq olaraq instansiya monitorlarını tutacaq getMapvə metodları çağırılacaq . Metod daha sonra zəng edəcək (xüsusilə hazırda məşğul olan nümunədə ) və monitorun hər bir nümunə üçün boşalmasını gözləyəcək . Eyni zamanda, nümunə monitoru xəritə çəkməklə məşğul olarkən üsul çağırılacaq . Nəticə dalana dirənir. setLocationDispatcherPlanegetMapplane.getLocationPlanePlanesetLocationdispatcher.requestLandingDispatcher

Zəngləri açın

Əvvəlki bölmədə təsvir olunan halların qarşısını almaq üçün digər obyektlərin metodlarına ictimai zənglərdən istifadə etmək tövsiyə olunur. Yəni sinxronlaşdırılmış blokdan kənar digər obyektlərin metodlarını çağırır. Açıq zənglər prinsipindən istifadə edərək metodlar yenidən yazılarsa setLocation, getMapdalana dirənmə ehtimalı aradan qaldırılacaq. Məsələn, bu kimi görünəcək:
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 blokadası

Kilidlər eyni zamanda yalnız bir başlığın istifadə edə biləcəyi bəzi resurslara daxil olmağa çalışarkən də baş verə bilər. Buna misal olaraq verilənlər bazası əlaqə hovuzu ola bilər. Bəzi mövzuların eyni anda iki əlaqəyə daxil olması lazımdırsa və onlar müxtəlif sıralarda daxil olurlarsa, bu, dalana səbəb ola bilər. Prinsipcə, bu cür kilidləmə sinxronizasiya sifarişinin kilidlənməsindən heç bir fərqi yoxdur, yalnız bəzi kodu yerinə yetirməyə çalışarkən deyil, resurslara daxil olmaq istəyərkən baş verir.

Çıxışlardan necə qaçmaq olar?

Əlbəttə ki, kod heç bir səhvsiz yazılırsa (bunların nümunələrini əvvəlki bölmələrdə gördük), onda heç bir dalana dirənməz. Bəs onun kodunun səhvsiz yazılmasına kim zəmanət verə bilər? Əlbəttə ki, sınaq səhvlərin əhəmiyyətli bir hissəsini müəyyən etməyə kömək edir, lakin əvvəllər gördüyümüz kimi, çox yivli kodda səhvləri diaqnoz etmək asan deyil və hətta sınaqdan sonra heç bir çıxılmaz vəziyyətin olmadığına əmin ola bilməzsiniz. Biz özümüzü hansısa şəkildə blokdan qoruya bilərikmi? Cavab bəli. Oxşar üsullar verilənlər bazası mühərriklərində istifadə olunur ki, onlar tez-tez çıxılmaz vəziyyətlərdən (verilənlər bazasında əməliyyat mexanizmi ilə bağlıdır) bərpa edilməlidir. LockPaketdə mövcud olan interfeys və onun tətbiqləri java.util.concurrent.locksmetoddan istifadə edərək bu sinfin nümunəsi ilə əlaqəli monitoru tutmağa cəhd etməyə imkan verir tryLock(monitoru zəbt etmək mümkün olarsa, həqiqəti qaytarır). Tutaq ki, interfeysi həyata keçirən bir cüt obyektimiz var Lockvə onların monitorlarını qarşılıqlı bloklanmadan qaçınmaq üçün tutmalıyıq. Bunu belə həyata keçirə bilərsiniz:
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();
			}
		}
	}
}
Bu proqramda gördüyünüz kimi, qarşılıqlı bloklanma ehtimalını aradan qaldıraraq, iki monitor tuturuq. Nəzərə alın ki, bloka try- finallyehtiyac var, çünki paketdəki siniflər java.util.concurrent.locksmonitoru avtomatik buraxmır və tapşırığınızın icrası zamanı bəzi istisnalar baş verərsə, monitor kilidli vəziyyətdə qalacaq. Ölçülərə necə diaqnoz qoymaq olar? JVM sizə tıxacların zibilliklərində göstərərək onları diaqnostika etməyə imkan verir. Belə zibillərə ipin hansı vəziyyətdə olduğu haqqında məlumatlar daxildir. Əgər bloklanıbsa, zibil qutusunun buraxılmasını gözlədiyi monitor haqqında məlumat var. Mövzuları boşaltmazdan əvvəl JVM gözləyən (məşğul) monitorların qrafikinə baxır və dövrlər tapsa, iştirak edən monitorları və mövzuları göstərərək, blokada məlumat əlavə edir. Kilidlənmiş mövzuların zibilliyi belə görünür:
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)
Yuxarıdakı zibil açıq şəkildə göstərir ki, verilənlər bazası ilə işləyən iki mövzu bir-birini bloklayıb. Bu JVM funksiyasından istifadə edərək çıxılmaz vəziyyətlərin diaqnostikası üçün proqramın müxtəlif yerlərində iş parçacığı boşaltma əməliyyatına zənglər yerləşdirmək və tətbiqi sınaqdan keçirmək lazımdır. Sonra, ortaya çıxan qeydləri təhlil etməlisiniz. Əgər onlar dalana dirənişin baş verdiyini bildirirlərsə, zibildən alınan məlumatlar onun baş verdiyi şərtləri aşkar etməyə kömək edəcək. Ümumiyyətlə, dalana dirənmiş nümunələrdəki vəziyyətlərdən qaçınmalısınız. Belə hallarda, proqram çox güman ki, sabit işləyəcək. Ancaq test və kodu nəzərdən keçirməyi unutmayın. Bu, baş verərsə, problemləri müəyyən etməyə kömək edəcəkdir. Ölçü sahəsinin bərpası kritik əhəmiyyət kəsb edən bir sistem inkişaf etdirdiyiniz hallarda, "Ölümlərin qarşısını necə almaq olar?" bölməsində təsvir olunan üsuldan istifadə edə bilərsiniz. Bu halda paketdən lockInterruptiblyinterfeys metodu da faydalı ola bilər . Bu üsuldan istifadə edərək monitoru tutan ipi kəsməyə imkan verir (və beləliklə monitoru azad edir). Lockjava.util.concurrent.locks
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION