Elakkan kebuntuan penyegerakan

Dalam artikel saya yang terdahulu "Penguncian Berganda-ganda: Pandai, tetapi Pecah" ( JavaWorld,Februari 2001), saya menerangkan bagaimana beberapa teknik umum untuk mengelakkan penyegerakan sebenarnya tidak selamat, dan mengesyorkan strategi "Bila ragu, segerakkan." Secara umum, anda harus menyegerakkan setiap kali anda membaca pemboleh ubah yang mungkin sebelumnya ditulis oleh utas yang berbeza, atau setiap kali anda menulis pemboleh ubah yang mungkin kemudian dibaca oleh utas lain. Selain itu, sementara penyegerakan membawa penalti prestasi, hukuman yang berkaitan dengan penyegerakan yang tidak terkawal tidak sehebat yang disarankan oleh beberapa sumber, dan telah berkurang dengan berterusan dengan setiap pelaksanaan JVM berturut-turut. Jadi nampaknya sekarang ada alasan yang lebih sedikit daripada sebelumnya untuk mengelakkan penyegerakan. Walau bagaimanapun, risiko lain dikaitkan dengan penyegerakan yang berlebihan: kebuntuan.

Apakah kebuntuan?

Kami mengatakan bahawa sekumpulan proses atau utas menemui jalan buntu ketika setiap utas menunggu peristiwa yang hanya boleh menyebabkan proses lain dalam set. Cara lain untuk menggambarkan kebuntuan adalah dengan membina graf terarah yang bucunya adalah utas atau proses dan yang tepinya mewakili hubungan "sedang menunggu". Sekiranya grafik ini mengandungi kitaran, sistem akan menemui jalan buntu. Kecuali sistem dirancang untuk pulih dari kebuntuan, kebuntuan menyebabkan program atau sistem digantung.

Kebuntuan penyegerakan dalam program Java

Kebuntuan boleh berlaku di Java kerana synchronizedkata kunci menyebabkan utas pelaksana menyekat sementara menunggu kunci, atau monitor, yang terkait dengan objek yang ditentukan. Oleh kerana utas mungkin sudah memegang kunci yang berkaitan dengan objek lain, masing-masing dua utas menunggu yang lain melepaskan kunci; dalam kes seperti itu, mereka akhirnya akan menunggu selama-lamanya. Contoh berikut menunjukkan sekumpulan kaedah yang berpotensi untuk menemui jalan buntu. Kedua-dua kaedah memperoleh kunci pada dua objek kunci, cacheLockdan tableLock, sebelum mereka meneruskannya. Dalam contoh ini, objek yang bertindak sebagai kunci adalah pemboleh ubah global (statik), teknik biasa untuk mempermudah tingkah laku mengunci aplikasi dengan melakukan penguncian pada tahap butiran yang lebih kasar:

Penyenaraian 1. Potensi kebuntuan penyegerakan

cacheLock Objek statik awam = Objek baru (); tableLock Objek statik awam = Objek baru (); ... public void oneMethod () {synchronized (cacheLock) {synchronized (tableLock) {doSomething (); }}} awam membatalkan satu lagi Metode () {synchronized (tableLock) {synchronized (cacheLock) {doSomethingElse (); }}}

Sekarang, bayangkan bahawa thread A memanggil oneMethod()sementara thread B secara serentak memanggil anotherMethod(). Bayangkan lebih jauh bahawa benang A memperoleh kunci pada cacheLock, dan, pada masa yang sama, utas B memperoleh kunci pada tableLock. Sekarang utasnya buntu: kedua-dua utas tidak akan melepaskan kuncinya sehingga memperoleh kunci yang lain, tetapi kedua-dua utas tidak akan dapat memperoleh kunci yang lain sehingga benang yang lain melepaskannya. Apabila kebuntuan program Java, benang kebuntuan hanya menunggu selama-lamanya. Walaupun utas lain mungkin terus berjalan, anda akhirnya harus mematikan program itu, mulakan semula, dan berharap agar ia tidak menemui jalan buntu lagi.

Menguji kebuntuan sukar, kerana kebuntuan bergantung pada masa, beban, dan persekitaran, dan dengan demikian mungkin jarang terjadi atau hanya dalam keadaan tertentu. Kod boleh berpotensi untuk kebuntuan, seperti Penyenaraian 1, tetapi tidak menunjukkan kebuntuan sehingga beberapa kombinasi peristiwa rawak dan bukan rawak berlaku, seperti program yang mengalami tahap beban tertentu, berjalan pada konfigurasi perkakasan tertentu, atau terkena tertentu gabungan tindakan pengguna dan keadaan persekitaran. Kebuntuan menyerupai bom masa yang menunggu untuk meletup dalam kod kami; apabila mereka melakukannya, program kita hanya tergantung.

Urutan kunci yang tidak konsisten menyebabkan kebuntuan

Nasib baik, kami dapat mengenakan syarat yang agak mudah pada pemerolehan kunci yang dapat mencegah kebuntuan penyegerakan. Kaedah penyenaraian 1 berpotensi untuk menemui jalan buntu kerana setiap kaedah memperoleh dua kunci dalam urutan yang berbeza. Sekiranya Penyenaraian 1 ditulis supaya setiap kaedah memperoleh dua kunci dalam urutan yang sama, dua atau lebih utas yang menjalankan kaedah ini tidak dapat menemui jalan buntu, tanpa mengira waktu atau faktor luaran lain, kerana tidak ada utas yang dapat memperoleh kunci kedua tanpa memegang pertama. Sekiranya anda dapat menjamin bahawa kunci akan selalu diperoleh dalam urutan yang konsisten, maka program anda tidak akan menemui jalan buntu.

Kebuntuan tidak selalu begitu jelas

Setelah memahami pentingnya pesanan kunci, anda dapat mengenali masalah Penyenaraian 1 dengan mudah. Walau bagaimanapun, masalah yang serupa mungkin terbukti kurang jelas: mungkin kedua-dua kaedah itu berada dalam kelas yang berasingan, atau mungkin kunci yang terlibat diperoleh secara tersirat melalui memanggil kaedah yang diselaraskan dan bukannya secara eksplisit melalui blok yang disegerakkan. Pertimbangkan dua kelas yang bekerjasama ini, Modeldan View, dalam rangka kerja MVC (Model-View-Controller) yang dipermudahkan:

Penyenaraian 2. Kebuntuan penyegerakan yang berpotensi lebih halus

Model kelas awam {private View myView; public void updateModel yang disegerakkan (Objek someArg) {doSomething (someArg); myView.somethingChanged (); } Objek getSomething yang diselaraskan awam () {return someMethod (); }} pandangan kelas awam {Model peribadi underlyingModel; awam membatalkan sesuatu yang tidak betulChanged () {doSomething (); } pembaruan void yang disegerakkan awam () {Object o = myModel.getSomething (); }}

Penyenaraian 2 mempunyai dua objek yang bekerjasama yang mempunyai kaedah yang diselaraskan; setiap objek memanggil kaedah penyegerakan yang lain. Keadaan ini menyerupai Penyenaraian 1 - dua kaedah memperoleh kunci pada dua objek yang sama, tetapi dalam susunan yang berbeza. Walau bagaimanapun, urutan kunci yang tidak konsisten dalam contoh ini lebih kurang jelas daripada yang terdapat dalam Penyenaraian 1 kerana pemerolehan kunci adalah bahagian tersirat dari panggilan kaedah. Sekiranya satu utas memanggil Model.updateModel()sementara utas yang lain memanggil secara serentak View.updateView(), utas pertama dapat memperoleh Modelkunci dan menunggu Viewkunci, sementara yang lain memperoleh Viewkunci dan menunggu kunci selama-lamanya Model.

Anda dapat menguburkan potensi kebuntuan penyegerakan dengan lebih mendalam. Pertimbangkan contoh ini: Anda mempunyai kaedah untuk memindahkan dana dari satu akaun ke akaun lain. Anda ingin memperoleh kunci pada kedua-dua akaun sebelum melakukan pemindahan untuk memastikan bahawa pemindahan itu bersifat atom. Pertimbangkan pelaksanaan yang tidak berbahaya ini:

Penyenaraian 3. Kebuntuan penyegerakan yang berpotensi lebih halus

 public void transferMoney (Akaun fromAccount, Account toAccount, DollarAmount numberToTransfer) {synchronized (fromAccount) {synchronized (toAccount) {if (fromAccount.hasSufficientBalance (jumlahToTransfer) {fromAccount.debit (jumlahToTransfer)} keAccount} } 

Walaupun semua kaedah yang beroperasi pada dua atau lebih akaun menggunakan urutan yang sama, Penyenaraian 3 mengandungi masalah masalah kebuntuan yang sama seperti Penyenaraian 1 dan 2, tetapi dengan cara yang lebih halus. Pertimbangkan apa yang berlaku apabila thread A dijalankan:

 transferMoney (akaunOne, akaunTwo, jumlah); 

Walaupun pada masa yang sama, benang B melaksanakan:

 transferMoney (akaunTwo, accountOne, anotherAmount); 

Sekali lagi, kedua utas berusaha memperoleh dua kunci yang sama, tetapi dalam susunan yang berbeza; risiko kebuntuan masih ada, tetapi dalam bentuk yang kurang jelas.

Cara mengelakkan kebuntuan

Salah satu kaedah terbaik untuk mencegah potensi kebuntuan adalah dengan mengelakkan memperoleh lebih dari satu kunci pada satu masa, yang sering praktikal. Namun, jika itu tidak mungkin, anda memerlukan strategi yang memastikan anda memperoleh banyak kunci dalam susunan yang ditentukan dan konsisten.

Bergantung pada cara program anda menggunakan kunci, mungkin tidak rumit untuk memastikan bahawa anda menggunakan pesanan penguncian yang konsisten. Dalam beberapa program, seperti dalam Penyenaraian 1, semua kunci kritikal yang mungkin terlibat dalam beberapa penguncian diambil dari sekumpulan kecil objek kunci tunggal. Dalam kes itu, anda boleh menentukan urutan pemerolehan kunci pada set kunci dan memastikan bahawa anda selalu memperoleh kunci mengikut urutan tersebut. Setelah perintah kunci ditentukan, ia hanya perlu didokumentasikan dengan baik untuk mendorong penggunaan yang konsisten sepanjang program.

Kecilkan blok yang disegerakkan untuk mengelakkan penguncian berganda

Dalam Penyenaraian 2, masalahnya bertambah rumit kerana, akibat memanggil kaedah yang diselaraskan, kunci diperoleh secara tidak langsung. Anda biasanya dapat mengelakkan jenis kebuntuan berpotensi yang timbul dari kes seperti Penyenaraian 2 dengan mempersempit ruang lingkup penyegerakan kepada sekecil sekecil mungkin. Adakah Model.updateModel()perlu memegang Modelkunci semasa memanggilView.somethingChanged()? Selalunya tidak; keseluruhan kaedah mungkin diselaraskan sebagai jalan pintas, dan bukan kerana keseluruhan kaedah perlu diselaraskan. Walau bagaimanapun, jika anda mengganti kaedah yang diselaraskan dengan blok yang disegerakkan yang lebih kecil di dalam kaedah tersebut, anda mesti mendokumentasikan tingkah laku penguncian ini sebagai sebahagian daripada kaedah Javadoc. Pemanggil perlu tahu bahawa mereka boleh memanggil kaedah dengan selamat tanpa penyegerakan luaran. Pemanggil juga harus mengetahui tingkah laku penguncian kaedah supaya mereka dapat memastikan bahawa kunci diperoleh dalam urutan yang konsisten.

Teknik pesanan kunci yang lebih canggih

Dalam situasi lain, seperti contoh akaun bank Penyenaraian 3, menerapkan peraturan pesanan tetap bertambah rumit; anda perlu menentukan jumlah pesanan pada set objek yang layak untuk dikunci dan menggunakan pesanan ini untuk memilih urutan pemerolehan kunci. Kedengarannya tidak kemas, tetapi sebenarnya mudah. Penyenaraian 4 menggambarkan teknik itu; ia menggunakan nombor akaun berangka untuk mendorong pesanan pada Accountobjek. (Sekiranya objek yang perlu dikunci tidak memiliki harta identiti semula jadi seperti nombor akaun, anda boleh menggunakan Object.identityHashCode()kaedah untuk menghasilkannya.)

Penyenaraian 4. Gunakan pesanan untuk mendapatkan kunci dalam urutan tetap

public void transferMoney (Akaun fromAccount, Account toAccount, DollarAmount numberToTransfer) {Akaun firstLock, secondLock; jika (fromAccount.accountNumber () == toAccount.accountNumber ()) membuang Pengecualian baru ("Tidak dapat memindahkan dari akaun ke dirinya sendiri"); lain jika (fromAccount.accountNumber () <toAccount.accountNumber ()) {firstLock = fromAccount; secondLock = keAkta; } lain {firstLock = toAccount; secondLock = fromAkun; } disegerakkan (firstLock) {synchronized (secondLock) {if (fromAccount.hasSufficientBalance (jumlahToTransfer) {fromAccount.debit (numberToTransfer); toAccount.credit (jumlahToTransfer);}}}}

Sekarang urutan di mana akaun dinyatakan dalam panggilan transferMoney()tidak penting; kunci sentiasa diperoleh dalam urutan yang sama.

Bahagian yang paling penting: Dokumentasi

Elemen kritikal - tetapi sering diabaikan - strategi penguncian apa pun adalah dokumentasi. Sayangnya, walaupun dalam kes di mana banyak usaha diambil untuk merancang strategi penguncian, seringkali lebih sedikit usaha yang dikeluarkan untuk mendokumentasikannya. Sekiranya program anda menggunakan sekumpulan kunci tunggal, anda harus mendokumentasikan andaian pesanan kunci anda dengan sejelas mungkin supaya penyelenggara masa depan dapat memenuhi syarat pesanan kunci. Sekiranya kaedah mesti memperoleh kunci untuk menjalankan fungsinya atau mesti dipanggil dengan kunci tertentu yang dipegang, kaedah Javadoc harus memperhatikan fakta itu. Dengan cara itu, pemaju masa depan akan tahu bahawa memanggil kaedah tertentu mungkin memerlukan kunci.

Beberapa program atau perpustakaan kelas mendokumentasikan penggunaan penguncian mereka dengan secukupnya. Sekurang-kurangnya, setiap kaedah harus mendokumentasikan kunci yang diperolehnya dan sama ada pemanggil mesti memegang kunci untuk memanggil kaedah dengan selamat. Di samping itu, kelas harus mendokumentasikan sama ada atau tidak, atau dalam keadaan apa, ia selamat untuk benang.

Fokus pada mengunci tingkah laku pada masa reka bentuk

Kerana kebuntuan sering tidak jelas dan terjadi jarang dan tidak dapat diramalkan, mereka boleh menyebabkan masalah serius dalam program Java. Dengan memperhatikan tingkah laku penguncian program anda pada waktu reka bentuk dan menentukan peraturan untuk kapan dan bagaimana memperoleh banyak kunci, anda dapat mengurangkan kemungkinan kebuntuan. Ingatlah untuk mendokumentasikan peraturan pemerolehan kunci program anda dan penggunaan penyegerakannya dengan teliti; masa yang dihabiskan untuk mendokumentasikan andaian penguncian mudah akan terbayar dengan mengurangkan kemungkinan kebuntuan dan masalah lain yang serupa kemudian.

Brian Goetz adalah pembangun perisian profesional dengan pengalaman lebih dari 15 tahun. Dia adalah perunding utama di Quiotix, sebuah syarikat pembangunan perisian dan perundingan yang terletak di Los Altos, Calif.