JavaRush /Blog Java /Random-MS /Kebuntuan dalam Java dan kaedah untuk memeranginya
articles
Tahap

Kebuntuan dalam Java dan kaedah untuk memeranginya

Diterbitkan dalam kumpulan
Apabila membangunkan aplikasi berbilang benang, dilema sering timbul: apa yang lebih penting ialah kebolehpercayaan atau prestasi aplikasi. Sebagai contoh, kami menggunakan penyegerakan untuk keselamatan benang, dan dalam kes apabila susunan penyegerakan tidak betul, kami boleh menyebabkan kebuntuan. Kami juga menggunakan kumpulan benang dan semafor untuk mengehadkan penggunaan sumber, dan ralat dalam reka bentuk ini boleh menyebabkan kebuntuan kerana kekurangan sumber. Dalam artikel ini kita akan bercakap tentang cara mengelakkan kebuntuan, serta masalah lain dalam prestasi aplikasi. Kami juga akan melihat bagaimana permohonan boleh ditulis sedemikian rupa untuk dapat pulih dalam kes kebuntuan. Kebuntuan dalam Java dan kaedah untuk memeranginya - 1Kebuntuan ialah keadaan di mana dua atau lebih proses yang menduduki beberapa sumber cuba memperoleh beberapa sumber lain yang diduduki oleh proses lain dan tiada satu pun proses boleh menduduki sumber yang mereka perlukan dan, dengan itu, melepaskan sumber yang diduduki. Takrifan ini terlalu umum dan oleh itu sukar untuk difahami; untuk pemahaman yang lebih baik, kita akan melihat jenis kebuntuan menggunakan contoh.

Penguncian Bersama Pesanan Penyegerakan

Pertimbangkan tugas berikut: anda perlu menulis kaedah yang menjalankan transaksi untuk memindahkan sejumlah wang dari satu akaun ke akaun yang lain. Penyelesaiannya mungkin kelihatan seperti ini:
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);
			}
		}
	}
}
Pada pandangan pertama, kod ini disegerakkan seperti biasa; kami mempunyai operasi atom untuk menyemak dan menukar keadaan akaun sumber dan menukar akaun destinasi. Walau bagaimanapun, dengan strategi penyegerakan ini, situasi kebuntuan mungkin berlaku. Mari kita lihat contoh bagaimana ini berlaku. Ia perlu membuat dua transaksi: pindahkan x wang dari akaun A ke akaun B, dan pindahkan wang y dari akaun B ke akaun A. Selalunya keadaan ini tidak akan menyebabkan kebuntuan, bagaimanapun, dalam satu set keadaan yang malang, transaksi 1 akan menduduki monitor akaun A, transaksi 2 akan menduduki monitor akaun B. Hasilnya adalah kebuntuan: transaksi 1 menunggu transaksi 2 untuk mengeluarkan monitor akaun B, tetapi transaksi 2 mesti mengakses monitor A, yang diduduki oleh transaksi 1. Salah satu masalah besar dengan kebuntuan ialah ia tidak mudah ditemui dalam ujian. Walaupun dalam situasi yang diterangkan dalam contoh, benang mungkin tidak menyekat, iaitu, keadaan ini tidak akan sentiasa dihasilkan semula, yang secara signifikan merumitkan diagnostik. Secara umum, masalah bukan determinisme yang diterangkan adalah tipikal untuk multithreading (walaupun ini tidak memudahkannya). Oleh itu, semakan kod memainkan peranan penting dalam meningkatkan kualiti aplikasi berbilang benang, kerana ia membolehkan anda mengenal pasti ralat yang sukar untuk dihasilkan semula semasa ujian. Ini, sudah tentu, tidak bermakna bahawa aplikasi itu tidak perlu diuji; kita tidak sepatutnya melupakan semakan kod. Apakah yang perlu saya lakukan untuk mengelakkan kod ini daripada menyebabkan kebuntuan? Penyekatan ini disebabkan oleh fakta bahawa penyegerakan akaun boleh berlaku dalam susunan yang berbeza. Sehubungan itu, jika anda memperkenalkan beberapa pesanan pada akaun (ini adalah beberapa peraturan yang membolehkan anda mengatakan bahawa akaun A adalah kurang daripada akaun B), maka masalah itu akan dihapuskan. Bagaimana hendak melakukannya? Pertama, jika akaun mempunyai beberapa jenis pengecam unik (contohnya, nombor akaun) berangka, huruf kecil atau yang lain dengan konsep tertib semula jadi (rentetan boleh dibandingkan dalam susunan leksikografi, maka kita boleh menganggap diri kita bertuah, dan kita akan sentiasa Kita boleh mula-mula menduduki monitor akaun yang lebih kecil, dan kemudian yang lebih besar (atau sebaliknya).
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)}
			}
		}
	}
}
Pilihan kedua, jika kami tidak mempunyai pengecam sedemikian, kami perlu menghasilkannya sendiri. Kita boleh, pada anggaran pertama, membandingkan objek dengan kod cincang. Kemungkinan besar mereka akan berbeza. Tetapi bagaimana jika mereka ternyata sama? Kemudian anda perlu menambah objek lain untuk penyegerakan. Ia mungkin kelihatan sedikit canggih, tetapi apa yang boleh anda lakukan? Dan selain itu, objek ketiga akan digunakan agak jarang. Hasilnya akan kelihatan seperti ini:
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)
				}
			}
		}
	}
}

Kebuntuan antara objek

Keadaan menyekat yang diterangkan mewakili kes kebuntuan yang paling mudah untuk didiagnosis. Selalunya dalam aplikasi berbilang benang, objek berbeza cuba mengakses blok disegerakkan yang sama. Ini boleh menyebabkan kebuntuan. Pertimbangkan contoh berikut: aplikasi penghantar penerbangan. Pesawat memberitahu pengawal apabila mereka tiba di destinasi mereka dan meminta kebenaran untuk mendarat. Pengawal menyimpan semua maklumat tentang pesawat terbang ke arahnya dan boleh merancang kedudukannya pada peta.
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;
	}
}
Memahami bahawa terdapat pepijat dalam kod ini yang boleh membawa kepada kebuntuan adalah lebih sukar daripada yang sebelumnya. Pada pandangan pertama, ia tidak mempunyai penyegerakan semula, tetapi ini tidak berlaku. Anda mungkin telah perasan bahawa kaedah setLocationkelas Planedan getMapkelas Dispatcherdisegerakkan dan memanggil kaedah disegerakkan bagi kelas lain dalam diri mereka sendiri. Ini secara amnya adalah amalan buruk. Bagaimana ini boleh diperbetulkan akan dibincangkan dalam bahagian seterusnya. Akibatnya, jika pesawat tiba di lokasi, apabila seseorang memutuskan untuk mendapatkan kad, kebuntuan boleh berlaku. Iaitu, kaedah getMapdan akan dipanggil, setLocationyang akan menduduki pemantau contoh Dispatcherdan Planemasing-masing. Kaedah kemudiannya akan getMapmemanggil plane.getLocation(khususnya pada contoh Planeyang sedang sibuk) yang akan menunggu monitor menjadi percuma untuk setiap kejadian Plane. Pada masa yang sama, kaedah setLocationakan dipanggil dispatcher.requestLanding, manakala monitor contoh Dispatcherkekal sibuk melukis peta. Hasilnya adalah kebuntuan.

Buka panggilan

Untuk mengelakkan situasi seperti yang diterangkan dalam bahagian sebelumnya, adalah disyorkan untuk menggunakan panggilan awam ke kaedah objek lain. Iaitu, kaedah panggilan objek lain di luar blok disegerakkan. Jika kaedah ditulis semula menggunakan prinsip panggilan terbuka setLocation, getMapkemungkinan kebuntuan akan dihapuskan. Ia akan kelihatan, sebagai contoh, seperti ini:
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;
}

Kebuntuan sumber

Kebuntuan juga boleh berlaku apabila cuba mengakses beberapa sumber yang hanya boleh digunakan satu utas pada satu masa. Contohnya ialah kumpulan sambungan pangkalan data. Jika sesetengah utas perlu mengakses dua sambungan pada masa yang sama dan mereka mengaksesnya dalam susunan yang berbeza, ini boleh menyebabkan kebuntuan. Pada asasnya, penguncian jenis ini tidak berbeza daripada penguncian pesanan penyegerakan, kecuali ia berlaku bukan semasa cuba melaksanakan beberapa kod, tetapi apabila cuba mengakses sumber.

Bagaimana untuk mengelakkan kebuntuan?

Sudah tentu, jika kod itu ditulis tanpa sebarang ralat (contoh yang kita lihat di bahagian sebelumnya), maka tidak akan ada kebuntuan di dalamnya. Tetapi siapa yang boleh menjamin bahawa kodnya ditulis tanpa ralat? Sudah tentu, ujian membantu mengenal pasti sebahagian besar ralat, tetapi seperti yang telah kita lihat sebelum ini, ralat dalam kod berbilang benang tidak mudah untuk didiagnosis dan walaupun selepas ujian anda tidak dapat memastikan bahawa tiada situasi kebuntuan. Bolehkah kita melindungi diri kita daripada menyekat? Jawapannya ya. Teknik yang sama digunakan dalam enjin pangkalan data, yang selalunya perlu pulih daripada kebuntuan (dikaitkan dengan mekanisme transaksi dalam pangkalan data). Antara muka Lockdan pelaksanaannya yang tersedia dalam pakej java.util.concurrent.locksmembolehkan anda mencuba untuk menduduki monitor yang dikaitkan dengan contoh kelas ini menggunakan kaedah tryLock(mengembalikan benar jika mungkin untuk menduduki monitor). Katakan kita mempunyai sepasang objek yang melaksanakan antara muka Lockdan kita perlu menduduki monitor mereka sedemikian rupa untuk mengelakkan sekatan bersama. Anda boleh melaksanakannya seperti ini:
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();
			}
		}
	}
}
Seperti yang anda lihat dalam program ini, kami menduduki dua monitor, sambil menghapuskan kemungkinan penyekatan bersama. Sila ambil perhatian bahawa blok itu try- finallyperlu kerana kelas dalam pakej java.util.concurrent.lockstidak melepaskan monitor secara automatik, dan jika beberapa pengecualian berlaku semasa pelaksanaan tugas anda, monitor akan tersekat dalam keadaan terkunci. Bagaimana untuk mendiagnosis kebuntuan? JVM membolehkan anda mendiagnosis kebuntuan dengan memaparkannya dalam pembuangan benang. Lambakan tersebut termasuk maklumat tentang keadaan benang tersebut. Jika ia disekat, dump mengandungi maklumat tentang monitor yang sedang menunggu untuk dikeluarkan. Sebelum membuang benang, JVM melihat graf pemantau menunggu (sibuk), dan jika ia menjumpai kitaran, ia menambah maklumat kebuntuan, menunjukkan pemantau dan utas yang mengambil bahagian. Lambakan benang buntu kelihatan seperti ini:
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)
Lambakan di atas jelas menunjukkan bahawa dua utas yang berfungsi dengan pangkalan data telah menyekat satu sama lain. Untuk mendiagnosis kebuntuan menggunakan ciri JVM ini, adalah perlu untuk membuat panggilan ke operasi pembuangan benang di pelbagai tempat dalam program dan menguji aplikasi. Seterusnya, anda harus menganalisis log yang terhasil. Jika ia menunjukkan bahawa kebuntuan telah berlaku, maklumat daripada tempat pembuangan akan membantu untuk mengesan keadaan di mana ia berlaku. Secara umum, anda harus mengelakkan situasi seperti yang terdapat dalam contoh kebuntuan. Dalam kes sedemikian, kemungkinan besar aplikasi akan berfungsi dengan stabil. Tetapi jangan lupa tentang ujian dan semakan kod. Ini akan membantu mengenal pasti masalah jika ia berlaku. Dalam kes di mana anda sedang membangunkan sistem yang pemulihan medan kebuntuan adalah kritikal, anda boleh menggunakan kaedah yang diterangkan dalam bahagian "Bagaimana untuk mengelakkan kebuntuan?". Dalam kes ini, kaedah lockInterruptiblyantara muka Lockdaripada java.util.concurrent.locks. Ia membolehkan anda mengganggu benang yang telah menduduki monitor menggunakan kaedah ini (dan dengan itu membebaskan monitor).
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION