JavaRush /Java Blog /Random-TL /Deadlock sa Java at mga pamamaraan upang labanan ito
articles
Antas

Deadlock sa Java at mga pamamaraan upang labanan ito

Nai-publish sa grupo
Kapag bumubuo ng mga multi-threaded na application, madalas na lumitaw ang isang dilemma: ang mas mahalaga ay ang pagiging maaasahan o pagganap ng application. Halimbawa, ginagamit namin ang pag-synchronize para sa kaligtasan ng thread, at sa mga kaso kung saan hindi tama ang pagkakasunud-sunod ng pag-synchronize, maaari kaming magdulot ng mga deadlock. Gumagamit din kami ng mga thread pool at semaphores upang limitahan ang pagkonsumo ng mapagkukunan, at ang isang error sa disenyo na ito ay maaaring humantong sa deadlock dahil sa kakulangan ng mga mapagkukunan. Sa artikulong ito ay pag-uusapan natin kung paano maiwasan ang deadlock, pati na rin ang iba pang mga problema sa pagganap ng application. Titingnan din natin kung paano maisusulat ang isang aplikasyon sa paraang maka-recover sa mga kaso ng deadlock. Deadlock sa Java at mga paraan upang labanan ito - 1Ang deadlock ay isang sitwasyon kung saan ang dalawa o higit pang mga proseso na sumasakop sa ilang mga mapagkukunan ay nagsisikap na makakuha ng ilang iba pang mga mapagkukunan na inookupahan ng iba pang mga proseso at wala sa mga proseso ang maaaring sumakop sa mapagkukunan na kailangan nila at, nang naaayon, ilabas ang inookupahan. Ang kahulugan na ito ay masyadong pangkalahatan at samakatuwid ay mahirap maunawaan; para sa isang mas mahusay na pag-unawa, titingnan natin ang mga uri ng deadlock gamit ang mga halimbawa.

Pag-synchronize ng Order Mutual Locking

Isaalang-alang ang sumusunod na gawain: kailangan mong magsulat ng isang paraan na nagsasagawa ng isang transaksyon upang maglipat ng isang tiyak na halaga ng pera mula sa isang account patungo sa isa pa. Ang solusyon ay maaaring magmukhang ganito:
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);
			}
		}
	}
}
Sa unang tingin, medyo normal na naka-synchronize ang code na ito; mayroon kaming atomic na operasyon ng pagsuri at pagbabago ng estado ng source account at pagpapalit ng patutunguhang account. Gayunpaman, sa diskarte sa pag-synchronize na ito, maaaring magkaroon ng deadlock na sitwasyon. Tingnan natin ang isang halimbawa kung paano ito nangyayari. Kinakailangang gumawa ng dalawang transaksyon: ilipat ang x pera mula sa account A sa account B, at ilipat ang y pera mula sa account B patungo sa account A. Kadalasan ang sitwasyong ito ay hindi magdudulot ng deadlock, gayunpaman, sa isang hindi magandang hanay ng mga pangyayari, ang transaksyon 1 ay sasakupin ang account monitor A, ang transaksyon 2 ay sasakupin ang account monitor B. Ang resulta ay isang deadlock: ang transaksyon 1 ay naghihintay para sa transaksyon 2 na ilabas ang account monitor B, ngunit dapat ma-access ng transaksyon 2 ang monitor A, na inookupahan ng transaksyon 1. Isa sa malaking problema sa mga deadlock ay hindi madaling mahanap ang mga ito sa pagsubok. Kahit na sa sitwasyong inilarawan sa halimbawa, ang mga thread ay maaaring hindi harangan, iyon ay, ang sitwasyong ito ay hindi patuloy na muling gagawin, na makabuluhang kumplikado sa mga diagnostic. Sa pangkalahatan, ang inilarawang problema ng non-determinism ay tipikal para sa multithreading (bagaman hindi nito ginagawang mas madali). Samakatuwid, ang pagsusuri ng code ay gumaganap ng isang mahalagang papel sa pagpapabuti ng kalidad ng mga multi-threaded na application, dahil pinapayagan ka nitong tukuyin ang mga error na mahirap kopyahin sa panahon ng pagsubok. Ito, siyempre, ay hindi nangangahulugan na ang application ay hindi kailangang masuri; hindi lamang natin dapat kalimutan ang tungkol sa pagsusuri ng code. Ano ang dapat kong gawin upang maiwasan ang code na ito na magdulot ng deadlock? Ang pagharang na ito ay sanhi ng katotohanan na ang pag-synchronize ng account ay maaaring mangyari sa ibang pagkakasunud-sunod. Alinsunod dito, kung magpapakilala ka ng ilang pagkakasunud-sunod sa mga account (ito ay ilang panuntunan na nagbibigay-daan sa iyong sabihin na ang account A ay mas mababa kaysa sa account B), pagkatapos ay aalisin ang problema. Paano ito gagawin? Una, kung ang mga account ay may ilang uri ng natatanging identifier (halimbawa, isang account number) numerical, lowercase, o iba pa na may natural na konsepto ng pagkakasunud-sunod (maaaring ihambing ang mga string sa lexicographical na pagkakasunud-sunod, kung gayon maaari nating ituring ang ating sarili na masuwerte, at gagawin natin. palagi Maaari muna nating sakupin ang monitor ng mas maliit na account, at pagkatapos ay ang mas malaki (o vice versa).
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)}
			}
		}
	}
}
Ang pangalawang opsyon, kung wala kaming ganoong identifier, kami mismo ang magbuo nito. Maaari naming, sa unang pagtataya, ihambing ang mga bagay sa pamamagitan ng hash code. Malamang na magkaiba sila. Pero paano kung magkapareho sila? Pagkatapos ay kailangan mong magdagdag ng isa pang bagay para sa pag-synchronize. Maaaring mukhang medyo sopistikado, ngunit ano ang maaari mong gawin? At bukod pa, ang ikatlong bagay ay bihirang gagamitin. Magiging ganito ang resulta:
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)
				}
			}
		}
	}
}

Deadlock sa pagitan ng mga bagay

Ang inilarawan na mga kondisyon sa pagharang ay kumakatawan sa pinakamadaling kaso ng deadlock upang masuri. Kadalasan sa mga multi-threaded na application, sinusubukan ng iba't ibang bagay na i-access ang parehong naka-synchronize na mga bloke. Ito ay maaaring magdulot ng deadlock. Isaalang-alang ang sumusunod na halimbawa: isang application ng flight dispatcher. Sinasabi ng mga eroplano sa controller kapag nakarating na sila sa kanilang destinasyon at humihiling ng pahintulot na lumapag. Iniimbak ng controller ang lahat ng impormasyon tungkol sa sasakyang panghimpapawid na lumilipad sa kanyang direksyon at maaaring i-plot ang kanilang posisyon sa mapa.
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;
	}
}
Ang pag-unawa na mayroong bug sa code na ito na maaaring humantong sa deadlock ay mas mahirap kaysa sa nauna. Sa unang tingin, wala itong muling pag-sync, ngunit hindi ito ang kaso. Marahil ay napansin mo na na ang mga pamamaraan setLocationng klase Planeat getMapklase Dispatcheray naka-synchronize at tumatawag sa mga naka-synchronize na pamamaraan ng iba pang mga klase sa loob ng kanilang sarili. Ito ay karaniwang masamang kasanayan. Kung paano ito maiwawasto ay tatalakayin sa susunod na seksyon. Bilang resulta, kung dumating ang eroplano sa lokasyon, sa sandaling magpasya ang isang tao na kunin ang card, maaaring magkaroon ng deadlock. Iyon ay, ang mga pamamaraan getMapat tatawagin setLocation, na sasakupin ang mga instance monitor Dispatcherat Planeayon sa pagkakabanggit. Pagkatapos ay getMaptatawag ang pamamaraan plane.getLocation(partikular sa instance Planena kasalukuyang abala) na maghihintay para maging libre ang monitor para sa bawat isa sa mga instance Plane. Kasabay nito, ang pamamaraan setLocationay tatawagin dispatcher.requestLanding, habang ang instance monitor Dispatcheray nananatiling abala sa pagguhit ng mapa. Ang resulta ay isang deadlock.

Buksan ang mga tawag

Upang maiwasan ang mga sitwasyon tulad ng inilarawan sa nakaraang seksyon, inirerekumenda na gumamit ng mga pampublikong tawag sa mga pamamaraan ng iba pang mga bagay. Iyon ay, tumawag sa mga pamamaraan ng iba pang mga bagay sa labas ng naka-synchronize na bloke. Kung ang mga pamamaraan ay muling isinulat gamit ang prinsipyo ng bukas na mga tawag setLocation, getMapang posibilidad ng deadlock ay aalisin. Ito ay magiging hitsura, halimbawa, tulad nito:
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;
}

Deadlock ng mapagkukunan

Ang mga deadlock ay maaari ding mangyari kapag sinusubukang i-access ang ilang mga mapagkukunan na isang thread lamang ang magagamit sa isang pagkakataon. Ang isang halimbawa ay isang database connection pool. Kung ang ilang mga thread ay kailangang mag-access ng dalawang koneksyon sa parehong oras at ma-access nila ang mga ito sa magkaibang mga order, maaari itong humantong sa deadlock. Sa pangunahin, ang ganitong uri ng pag-lock ay hindi naiiba sa pag-lock ng pagkakasunud-sunod ng pagkakasunud-sunod, maliban na ito ay nangyayari hindi kapag sinusubukang magsagawa ng ilang code, ngunit kapag sinusubukang i-access ang mga mapagkukunan.

Paano maiwasan ang deadlocks?

Siyempre, kung ang code ay isinulat nang walang anumang mga error (mga halimbawa kung saan nakita namin sa mga nakaraang seksyon), pagkatapos ay walang mga deadlock sa loob nito. Ngunit sino ang magagarantiya na ang kanyang code ay nakasulat nang walang mga pagkakamali? Siyempre, nakakatulong ang pagsubok upang matukoy ang isang makabuluhang bahagi ng mga error, ngunit tulad ng nakita natin kanina, ang mga error sa multi-threaded code ay hindi madaling masuri at kahit na pagkatapos ng pagsubok ay hindi ka makakatiyak na walang mga deadlock na sitwasyon. Maaari ba nating protektahan ang ating sarili mula sa pagharang? Ang sagot ay oo. Ang mga katulad na pamamaraan ay ginagamit sa mga database engine, na kadalasang kailangang mabawi mula sa mga deadlock (na nauugnay sa mekanismo ng transaksyon sa database). Ang interface Lockat ang mga pagpapatupad nito na magagamit sa package java.util.concurrent.locksay nagbibigay-daan sa iyo upang subukang sakupin ang monitor na nauugnay sa isang halimbawa ng klase na ito gamit ang pamamaraan tryLock(nagbabalik ng totoo kung posible na sakupin ang monitor). Ipagpalagay na mayroon kaming isang pares ng mga bagay na nagpapatupad ng isang interface Lockat kailangan naming sakupin ang kanilang mga monitor sa paraang maiwasan ang magkaparehong pagharang. Maaari mong ipatupad ito tulad nito:
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();
			}
		}
	}
}
Tulad ng nakikita mo sa programang ito, sinasakop namin ang dalawang monitor, habang inaalis ang posibilidad ng pag-block sa isa't isa. Pakitandaan na ang block try- finallyay kinakailangan dahil ang mga klase sa package java.util.concurrent.locksay hindi awtomatikong naglalabas ng monitor, at kung ang ilang pagbubukod ay nangyari sa panahon ng pagpapatupad ng iyong gawain, ang monitor ay mai-stuck sa isang naka-lock na estado. Paano mag-diagnose ng deadlocks? Binibigyang-daan ka ng JVM na i-diagnose ang mga deadlock sa pamamagitan ng pagpapakita ng mga ito sa mga thread dump. Kasama sa mga naturang dump ang impormasyon tungkol sa kung anong estado ang thread. Kung ito ay naharang, ang dump ay naglalaman ng impormasyon tungkol sa monitor na hinihintay ng thread na ilabas. Bago ang paglalaglag ng mga thread, tinitingnan ng JVM ang graph ng naghihintay (abala) na mga monitor, at kung nakahanap ito ng mga cycle, nagdaragdag ito ng deadlock na impormasyon, na nagpapahiwatig ng mga kalahok na monitor at thread. Ang isang dump ng deadlocked na mga thread ay ganito ang hitsura:
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)
Ang dump sa itaas ay malinaw na nagpapakita na ang dalawang thread na nagtatrabaho sa database ay hinarangan ang isa't isa. Upang masuri ang mga deadlock gamit ang tampok na JVM na ito, kinakailangan na tumawag sa operasyon ng thread dump sa iba't ibang lugar sa programa at subukan ang application. Susunod, dapat mong pag-aralan ang mga resultang log. Kung ipahiwatig nila na nagkaroon ng deadlock, ang impormasyon mula sa dump ay makakatulong upang makita ang mga kondisyon kung saan ito nangyari. Sa pangkalahatan, dapat mong iwasan ang mga sitwasyon tulad ng nasa mga halimbawa ng deadlock. Sa ganitong mga kaso, ang application ay malamang na gagana nang matatag. Ngunit huwag kalimutan ang tungkol sa pagsubok at pagsusuri ng code. Makakatulong ito na matukoy ang mga problema kung mangyari ang mga ito. Sa mga kaso kung saan bubuo ka ng isang sistema kung saan ang pagbawi ng deadlock field ay kritikal, maaari mong gamitin ang paraang inilarawan sa seksyong "Paano maiiwasan ang mga deadlock?". Sa kasong ito, ang paraan ng lockInterruptiblyinterface Lockmula sa java.util.concurrent.locks. Pinapayagan ka nitong matakpan ang thread na sumakop sa monitor gamit ang pamamaraang ito (at sa gayon ay palayain ang monitor).
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION