JavaRush /Java Blog /Random-ID /Kebuntuan di Java dan metode untuk mengatasinya
articles
Level 15

Kebuntuan di Java dan metode untuk mengatasinya

Dipublikasikan di grup Random-ID
Saat mengembangkan aplikasi multi-thread, sering kali muncul dilema: yang lebih penting adalah keandalan atau kinerja aplikasi. Misalnya, kami menggunakan sinkronisasi untuk keamanan thread, dan jika urutan sinkronisasi salah, kami dapat menyebabkan kebuntuan. Kami juga menggunakan kumpulan thread dan semaphore untuk membatasi konsumsi sumber daya, dan kesalahan dalam desain ini dapat menyebabkan kebuntuan karena kurangnya sumber daya. Pada artikel kali ini kita akan membahas tentang cara menghindari kebuntuan, serta masalah lain pada kinerja aplikasi. Kami juga akan melihat bagaimana sebuah aplikasi dapat ditulis sedemikian rupa agar dapat pulih jika terjadi kebuntuan. Kebuntuan di Java dan metode untuk mengatasinya - 1Kebuntuan adalah situasi di mana dua atau lebih proses yang menggunakan beberapa sumber daya mencoba untuk memperoleh beberapa sumber daya lain yang ditempati oleh proses lain dan tidak ada proses yang dapat menggunakan sumber daya yang mereka butuhkan dan, dengan demikian, melepaskan sumber daya yang ditempati. Definisi ini terlalu umum dan oleh karena itu sulit untuk dipahami, untuk pemahaman yang lebih baik, kita akan melihat jenis-jenis kebuntuan dengan menggunakan contoh.

Urutan Sinkronisasi Saling Mengunci

Pertimbangkan tugas berikut: Anda perlu menulis metode yang melakukan transaksi untuk mentransfer sejumlah uang dari satu rekening ke rekening lainnya. Solusinya mungkin terlihat 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, kode ini disinkronkan secara normal; kami memiliki operasi atom untuk memeriksa dan mengubah status akun sumber dan mengubah akun tujuan. Namun, dengan strategi sinkronisasi ini, situasi kebuntuan mungkin terjadi. Mari kita lihat contoh bagaimana hal ini terjadi. Anda perlu melakukan dua transaksi: transfer uang x dari rekening A ke rekening B, dan transfer uang y dari rekening B ke rekening A. Seringkali situasi ini tidak menyebabkan kebuntuan, namun, dalam keadaan yang tidak menguntungkan, transaksi 1 akan menempati monitor akun A, transaksi 2 akan menempati monitor akun B. Hasilnya adalah kebuntuan: transaksi 1 menunggu transaksi 2 melepaskan monitor akun B, tetapi transaksi 2 harus mengakses monitor A, yang ditempati oleh transaksi 1. Salah satu masalah besar dengan kebuntuan adalah bahwa kebuntuan tersebut tidak mudah ditemukan dalam pengujian. Bahkan dalam situasi yang dijelaskan dalam contoh, utas mungkin tidak diblokir, artinya, situasi ini tidak akan terus-menerus direproduksi, yang secara signifikan mempersulit diagnosis. Secara umum, masalah non-determinisme yang dijelaskan adalah tipikal untuk multithreading (walaupun hal ini tidak membuatnya lebih mudah). Oleh karena itu, tinjauan kode memainkan peran penting dalam meningkatkan kualitas aplikasi multi-thread, karena memungkinkan Anda mengidentifikasi kesalahan yang sulit direproduksi selama pengujian. Tentu saja, ini tidak berarti bahwa aplikasi tersebut tidak perlu diuji; kita hanya tidak boleh melupakan peninjauan kode. Apa yang harus saya lakukan agar kode ini tidak menyebabkan kebuntuan? Pemblokiran ini disebabkan oleh fakta bahwa sinkronisasi akun dapat terjadi dalam urutan yang berbeda. Oleh karena itu, jika Anda memasukkan beberapa urutan pada akun (ini adalah aturan yang memungkinkan Anda mengatakan bahwa akun A lebih kecil dari akun B), maka masalahnya akan teratasi. Bagaimana cara melakukannya? Pertama, jika akun memiliki semacam pengidentifikasi unik (misalnya, nomor akun), numerik, huruf kecil, atau lainnya dengan konsep keteraturan alami (string dapat dibandingkan dalam urutan leksikografis, maka kita dapat menganggap diri kita beruntung, dan kita akan selalu Kita dapat menempati monitor akun yang lebih kecil terlebih dahulu, lalu 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)}
			}
		}
	}
}
Opsi kedua, jika kita tidak memiliki pengenal seperti itu, kita harus membuatnya sendiri. Sebagai perkiraan pertama, kita dapat membandingkan objek dengan kode hash. Kemungkinan besar keduanya akan berbeda. Namun bagaimana jika ternyata keduanya sama? Kemudian Anda harus menambahkan objek lain untuk sinkronisasi. Ini mungkin terlihat sedikit canggih, tapi apa yang bisa Anda lakukan? Selain itu, objek ketiga akan jarang digunakan. Hasilnya akan terlihat 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 antar objek

Kondisi pemblokiran yang dijelaskan mewakili kasus kebuntuan yang paling mudah untuk didiagnosis. Seringkali dalam aplikasi multi-thread, objek yang berbeda mencoba mengakses blok tersinkronisasi yang sama. Hal ini dapat menyebabkan kebuntuan. Perhatikan contoh berikut: aplikasi operator penerbangan. Pesawat memberi tahu pengontrol ketika mereka telah tiba di tujuan dan meminta izin untuk mendarat. Pengontrol menyimpan semua informasi tentang pesawat yang terbang ke arahnya dan dapat memplot posisinya di 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 bahwa ada bug dalam kode ini yang dapat menyebabkan kebuntuan lebih sulit dibandingkan dengan yang sebelumnya. Pada pandangan pertama, tidak ada sinkronisasi ulang, tetapi tidak demikian. Anda mungkin telah memperhatikan bahwa setLocationkelas Planedan metode getMapkelas Dispatcherdisinkronkan dan memanggil metode tersinkronisasi dari kelas lain di dalamnya. Ini umumnya merupakan praktik yang buruk. Bagaimana hal ini dapat diperbaiki akan dibahas di bagian selanjutnya. Alhasil, jika pesawat sampai di lokasi, saat ada yang memutuskan untuk mengambil kartu tersebut, bisa terjadi kebuntuan. Artinya, metode getMapdan akan dipanggil, setLocationyang akan menempati monitor instance Dispatcherdan Planemasing-masing. Metode tersebut kemudian akan getMapmemanggil plane.getLocation(khususnya pada instance Planeyang sedang sibuk) yang akan menunggu hingga monitor menjadi bebas untuk setiap instance Plane. Pada saat yang sama, metode ini setLocationakan dipanggil dispatcher.requestLanding, sementara monitor instance Dispatchertetap sibuk menggambar peta. Hasilnya adalah kebuntuan.

Panggilan terbuka

Untuk menghindari situasi seperti yang dijelaskan di bagian sebelumnya, disarankan untuk menggunakan panggilan publik ke metode objek lain. Artinya, memanggil metode objek lain di luar blok yang disinkronkan. Jika metode ditulis ulang menggunakan prinsip panggilan terbuka setLocation, getMapkemungkinan kebuntuan akan dihilangkan. Misalnya, tampilannya akan 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 daya

Kebuntuan juga dapat terjadi ketika mencoba mengakses beberapa sumber daya yang hanya dapat digunakan oleh satu thread dalam satu waktu. Contohnya adalah kumpulan koneksi database. Jika beberapa thread perlu mengakses dua koneksi pada saat yang sama dan mereka mengaksesnya dalam urutan yang berbeda, hal ini dapat menyebabkan kebuntuan. Pada dasarnya, penguncian semacam ini tidak berbeda dengan penguncian perintah sinkronisasi, hanya saja hal ini terjadi bukan saat mencoba mengeksekusi kode tertentu, tetapi saat mencoba mengakses sumber daya.

Bagaimana cara menghindari kebuntuan?

Tentu saja, jika kode ditulis tanpa kesalahan (contohnya kita lihat di bagian sebelumnya), maka tidak akan ada kebuntuan di dalamnya. Tapi siapa yang bisa menjamin bahwa kodenya ditulis tanpa kesalahan? Tentu saja, pengujian membantu mengidentifikasi sebagian besar kesalahan, tetapi seperti yang telah kita lihat sebelumnya, kesalahan dalam kode multi-utas tidak mudah untuk didiagnosis dan bahkan setelah pengujian Anda tidak dapat memastikan bahwa tidak ada situasi kebuntuan. Bisakah kita melindungi diri kita sendiri dari pemblokiran? Jawabannya iya. Teknik serupa digunakan dalam mesin basis data, yang sering kali perlu pulih dari kebuntuan (terkait dengan mekanisme transaksi dalam basis data). Antarmuka Lockdan implementasinya yang tersedia dalam paket java.util.concurrent.locksmemungkinkan Anda untuk mencoba menempati monitor yang terkait dengan instance kelas ini menggunakan metode ini tryLock(mengembalikan nilai true jika memungkinkan untuk menempati monitor). Misalkan kita mempunyai sepasang objek yang mengimplementasikan sebuah antarmuka Lockdan kita perlu menempati monitor mereka sedemikian rupa untuk menghindari saling memblokir. Anda dapat menerapkannya 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 di program ini, kami menempati dua monitor, sekaligus menghilangkan kemungkinan saling memblokir. Harap dicatat bahwa blok ini try- finallydiperlukan karena kelas-kelas dalam paket java.util.concurrent.lockstidak secara otomatis melepaskan monitor, dan jika beberapa pengecualian terjadi selama pelaksanaan tugas Anda, monitor akan terjebak dalam keadaan terkunci. Bagaimana cara mendiagnosis kebuntuan? JVM memungkinkan Anda mendiagnosis kebuntuan dengan menampilkannya di thread dump. Dump tersebut mencakup informasi tentang kondisi thread tersebut. Jika diblokir, dump berisi informasi tentang monitor yang sedang menunggu thread untuk dilepaskan. Sebelum membuang thread, JVM melihat grafik monitor yang menunggu (sibuk), dan jika menemukan siklus, JVM menambahkan informasi kebuntuan, yang menunjukkan monitor dan thread yang berpartisipasi. Kumpulan thread yang menemui jalan buntu terlihat 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)
Dump di atas dengan jelas menunjukkan bahwa dua thread yang bekerja dengan database telah saling memblokir. Untuk mendiagnosis kebuntuan menggunakan fitur JVM ini, perlu melakukan panggilan ke operasi thread dump di berbagai tempat dalam program dan menguji aplikasi. Selanjutnya, Anda harus menganalisis log yang dihasilkan. Jika mereka menunjukkan bahwa kebuntuan telah terjadi, informasi dari dump akan membantu mendeteksi kondisi terjadinya kebuntuan. Secara umum, Anda harus menghindari situasi seperti pada contoh kebuntuan. Dalam kasus seperti itu, kemungkinan besar aplikasi akan bekerja dengan stabil. Tapi jangan lupa tentang pengujian dan peninjauan kode. Ini akan membantu mengidentifikasi masalah jika memang terjadi. Jika Anda sedang mengembangkan sistem yang memerlukan pemulihan bidang kebuntuan, Anda dapat menggunakan metode yang dijelaskan di bagian “Bagaimana cara menghindari kebuntuan?”. Dalam hal ini, metode lockInterruptiblyantarmuka Lockdari java.util.concurrent.locks. Ini memungkinkan Anda untuk menginterupsi thread yang menempati monitor menggunakan metode ini (dan dengan demikian membebaskan monitor).
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION