Menggunakan utas dengan koleksi, Bagian 1

Utas adalah bagian integral dari bahasa Java. Menggunakan utas, banyak algoritme, seperti sistem manajemen antrian, lebih mudah diakses daripada menggunakan teknik polling dan perulangan. Baru-baru ini, saat menulis kelas Java, saya merasa perlu menggunakan utas saat menghitung daftar, dan ini mengungkap beberapa masalah menarik yang terkait dengan koleksi yang sadar utas.

Ini Kedalaman Java Di kolom menjelaskan masalah yang saya menemukan dalam usaha saya untuk mengembangkan koleksi benang-aman. Koleksi disebut "thread-safe" jika dapat digunakan dengan aman oleh beberapa klien (thread) pada waktu yang sama. "Jadi apa masalahnya?" Anda bertanya. Masalahnya adalah, dalam penggunaan tipikal, sebuah program mengubah sebuah koleksi (disebut mutating ), dan membacanya (disebut enumerating ).

Beberapa orang tidak mendaftarkan pernyataan, "Platform Java multithread." Tentu, mereka mendengarnya, dan mereka menganggukkan kepala. Tetapi mereka tidak memahami bahwa, tidak seperti C atau C ++, di mana threading dibaut dari samping melalui OS, utas di Java adalah konstruksi bahasa dasar. Kesalahpahaman ini, atau pemahaman yang buruk, tentang sifat Java yang berulir secara inheren, pasti mengarah ke dua kelemahan umum dalam kode Java pemrogram: Mereka gagal untuk mendeklarasikan metode sebagai tersinkronisasi yang seharusnya (karena objek berada dalam keadaan yang tidak konsisten selama eksekusi metode) atau mereka mendeklarasikan metode sebagai tersinkronisasi untuk melindunginya, yang menyebabkan sistem lainnya tidak beroperasi secara efisien.

Saya menemukan masalah ini ketika saya menginginkan koleksi yang dapat digunakan beberapa utas tanpa perlu memblokir eksekusi utas lainnya. Tidak ada kelas koleksi dalam versi 1.1 JDK yang aman untuk thread. Secara khusus, tidak ada kelas koleksi yang memungkinkan Anda menghitung dengan satu utas saat bermutasi dengan utas lainnya.

Koleksi non-thread-safe

Masalah dasar saya adalah sebagai berikut: Dengan asumsi Anda memiliki kumpulan objek yang dipesan, rancang kelas Java sehingga utas dapat menghitung semua atau sebagian dari koleksi tanpa khawatir tentang pencacahan menjadi tidak valid karena utas lain yang mengubah koleksi. Sebagai contoh masalah, pertimbangkan Vectorkelas Java . Kelas ini tidak aman untuk thread dan menyebabkan banyak masalah bagi programmer Java baru saat mereka menggabungkannya dengan program multithread.

The Vectorkelas menyediakan fasilitas yang sangat berguna untuk programmer Java: yaitu, array dinamis berukuran benda. Dalam praktiknya, Anda dapat menggunakan fasilitas ini untuk menyimpan hasil di mana jumlah akhir dari objek yang akan Anda tangani tidak diketahui sampai Anda selesai dengan semuanya. Saya membuat contoh berikut untuk mendemonstrasikan konsep ini.

01 import java.util.Vector; 02 import java.util.Enumeration; 03 Demo kelas publik {04 public static void main (String args []) {05 Vector digits = new Vector (); 06 hasil int = 0; 07 08 if (args.length == 0) {09 System.out.println ("Penggunaan adalah java demo 12345"); 10 System.exit (1); 11} 12 13 untuk (int i = 0; i = '0') && (c <= '9')) 16 digits.addElement (new Integer (c - '0')); 17 lainnya 18 istirahat; 19} 20 System.out.println ("Ada" + digits.size () + "digit."); 21 untuk (Enumeration e = digits.elements (); e.hasMoreElements ();) {22 result = result * 10 + ((Integer) e.nextElement ()). IntValue (); 23} 24 System.out.println (args [0] + "=" + result); 25 System.exit (0); 26} 27}

Kelas sederhana di atas menggunakan Vectorobjek untuk mengumpulkan karakter digit dari sebuah string. Koleksi tersebut kemudian dihitung untuk menghitung nilai integer dari string tersebut. Tidak ada yang salah dengan kelas ini kecuali kelas ini tidak aman untuk thread. Jika utas lain kebetulan memegang referensi ke vektor digit , dan utas itu memasukkan karakter baru ke dalam vektor, hasil loop pada baris 21 hingga 23 di atas tidak akan dapat diprediksi. Jika penyisipan terjadi sebelum objek pencacahan melewati titik penyisipan, maka hasil thread computing akan memproses karakter baru tersebut. Jika penyisipan terjadi setelah pencacahan melewati titik penyisipan, loop tidak akan memproses karakter tersebut. Skenario terburuknya adalah bahwa loop mungkin melemparNoSuchElementException jika daftar internal disusupi.

Contoh ini hanya itu - contoh yang dibuat-buat. Ini menunjukkan masalah, tetapi apa peluang utas lain berjalan selama pencacahan singkat lima atau enam digit? Dalam contoh ini, risikonya rendah. Jumlah waktu yang berlalu saat satu utas memulai operasi berisiko, yang dalam contoh ini adalah pencacahan, dan kemudian menyelesaikan tugas disebut jendela kerentanan , atau jendela utas . Jendela khusus ini dikenal sebagai kondisi balapankarena satu utas "berlomba" untuk menyelesaikan tugasnya sebelum utas lain menggunakan sumber daya penting (daftar digit). Namun, ketika Anda mulai menggunakan koleksi untuk mewakili sekelompok beberapa ribu elemen, seperti dengan database, jendela kerentanan meningkat karena pencacahan utas akan menghabiskan lebih banyak waktu dalam putaran pencacahan, dan itu membuat peluang utas lain berjalan jauh lebih tinggi. Anda tentu tidak ingin utas lain mengubah daftar di bawah Anda! Yang Anda inginkan adalah jaminan bahwa Enumerationbenda yang Anda pegang itu valid.

Salah satu cara untuk melihat masalah ini adalah dengan memperhatikan bahwa Enumerationobjek terpisah dari Vectorobjek. Karena terpisah, mereka tidak dapat mempertahankan kendali satu sama lain setelah dibuat. Pengikatan longgar ini memberi kesan kepada saya bahwa mungkin jalur yang berguna untuk dijelajahi adalah pencacahan yang terikat lebih erat dengan koleksi yang menghasilkannya.

Membuat koleksi

Untuk membuat koleksi thread-safe saya, pertama-tama saya membutuhkan koleksi. Dalam kasus saya, koleksi yang diurutkan diperlukan, tetapi saya tidak repot-repot menggunakan rute pohon biner lengkap. Sebagai gantinya, saya membuat koleksi yang saya sebut SynchroList . Bulan ini, saya akan melihat elemen inti dari koleksi SynchroList dan menjelaskan cara menggunakannya. Bulan depan, di Bagian 2, saya akan mengambil koleksi dari kelas Java yang sederhana dan mudah dipahami ke kelas Java multithread yang kompleks. Tujuan saya adalah untuk menjaga desain dan implementasi koleksi yang berbeda dan dapat dipahami relatif terhadap teknik yang digunakan untuk membuatnya sadar-thread.

Saya menamai kelas saya SynchroList. Nama "SynchroList", tentu saja, berasal dari gabungan "sinkronisasi" dan "daftar". Koleksinya hanyalah daftar tertaut ganda seperti yang mungkin Anda temukan di buku teks perguruan tinggi mana pun tentang pemrograman, meskipun melalui penggunaan kelas batin bernama Link, keanggunan tertentu dapat dicapai. Kelas dalam Linkdidefinisikan sebagai berikut:

class Link {private Object data; Tautan pribadi nxt, prv; Tautan (Objek o, Tautan p, Tautan n) {nxt = n; prv = p; data = o; jika (n! = null) n.prv = ini; jika (p! = null) p.nxt = ini; } Objek getData () {data yang dikembalikan; } Tautkan next () {return nxt; } Tautkan berikutnya (Tautan baruBaru) {Tautan r = nxt; nxt = newNext; return r;} Link prev () {return prv; } Tautan sebelumnya (Tautan newPrev) {Tautan r = prv; prv = newPrev; return r;} public String toString () {return "Link (" + data + ")"; }}

Seperti yang Anda lihat pada kode di atas, sebuah Linkobjek merangkum perilaku penautan yang akan digunakan daftar untuk mengatur objeknya. Untuk mengimplementasikan perilaku daftar tertaut ganda, objek berisi referensi ke objek datanya, referensi ke tautan berikutnya dalam rantai, dan referensi ke tautan sebelumnya dalam rantai. Selanjutnya, metode nextdan prevkelebihan beban menyediakan sarana untuk memperbarui penunjuk objek. Ini diperlukan karena kelas induk perlu memasukkan dan menghapus tautan ke dalam daftar. Konstruktor tautan dirancang untuk membuat dan menyisipkan tautan pada saat yang bersamaan. Ini menghemat panggilan metode dalam implementasi daftar.

Kelas dalam lainnya digunakan dalam daftar - dalam hal ini, kelas pencacah bernama ListEnumerator. Kelas ini mengimplementasikan java.util.Enumerationantarmuka: mekanisme standar yang digunakan Java untuk mengulangi sekumpulan objek. Dengan meminta enumerator kami mengimplementasikan antarmuka ini, koleksi kami akan kompatibel dengan kelas Java lainnya yang menggunakan antarmuka ini untuk menghitung konten dari sebuah koleksi. Implementasi kelas ini ditunjukkan pada kode di bawah ini.

class LinkEnumerator mengimplementasikan Pencacahan {Private Link saat ini, sebelumnya; LinkEnumerator () {current = head; } public boolean hasMoreElements () {return (current! = null); } public Object nextElement () {Object result = null; Tautkan tmp; if (current! = null) {result = current.getData (); current = current.next (); } hasil pengembalian; }}

Dalam inkarnasinya yang sekarang, LinkEnumeratorkelasnya cukup mudah; ini akan menjadi lebih rumit saat kami memodifikasinya. Dalam inkarnasi ini, itu hanya berjalan melalui daftar untuk objek pemanggil sampai datang ke tautan terakhir dalam daftar tertaut internal. Dua metode yang diperlukan untuk mengimplementasikan java.util.Enumerationantarmuka adalah hasMoreElementsdan nextElement.

Tentu saja, salah satu alasan kami tidak menggunakan java.util.Vectorkelas adalah karena saya perlu mengurutkan nilai dalam koleksi. Kami punya pilihan: untuk membangun koleksi ini agar spesifik untuk jenis objek tertentu, sehingga menggunakan pengetahuan yang mendalam tentang tipe objek untuk mengurutkannya, atau untuk membuat solusi yang lebih umum berdasarkan antarmuka. Saya memilih metode terakhir dan mendefinisikan sebuah antarmuka bernama Comparatoruntuk merangkum metode yang diperlukan untuk mengurutkan objek. Antarmuka tersebut ditunjukkan di bawah ini.

antarmuka publik Pembanding {public boolean lessThan (Objek a, Objek b); public boolean GreaterThan (Objek a, Objek b); public boolean equalTo (Objek a, Objek b); void typeCheck (Objek a); }

Seperti yang Anda lihat pada kode di atas, Comparatorantarmuka cukup sederhana. Antarmuka memerlukan satu metode untuk masing-masing dari tiga operasi perbandingan dasar. Dengan menggunakan antarmuka ini, daftar dapat membandingkan objek yang ditambahkan atau dihapus dengan objek yang sudah ada dalam daftar. Metode terakhir,, typeCheckdigunakan untuk memastikan keamanan jenis koleksi. Saat Comparatorobjek digunakan, Comparatordapat digunakan untuk memastikan bahwa objek dalam koleksi semuanya berjenis sama. Nilai dari pemeriksaan jenis ini adalah bahwa hal itu menyelamatkan Anda dari melihat pengecualian transmisi objek jika objek dalam daftar bukan dari jenis yang Anda harapkan. Saya punya contoh nanti yang menggunakan a Comparator, tapi sebelum kita mendapatkan contoh, mari kita lihat SynchroListkelasnya secara langsung.

public class SynchroList {class Link {... ini ditunjukkan di atas ...} class LinkEnumerator mengimplementasikan Enumeration {... class enumerator ...} / * Sebuah objek untuk membandingkan elemen * / Comparator cmp; Tautkan kepala, ekor; Sinkronisasi publik () {} Daftar Sinkronisasi publik (Pembanding c) {cmp = c; } private void before (Object o, Link p) {new Link (o, p.prev (), p); } private void setelah (Objek o, Link p) {Link baru (o, p, p.next ()); } private void remove (Link p) {if (p.prev () == null) {head = p.next (); (p.next ()). prev (null); } lain jika (p.next () == null) {tail = p.prev (); (p.prev ()). next (null); } lain {p.prev (). next (p.next ()); p.next (). prev (p.prev ()); }} public void add (Object o) {// jika cmp adalah null, selalu tambahkan di bagian akhir daftar. if (cmp == null) {if (head == null) {head = new Link (o, null, null); ekor = kepala; } else {tail = new Link (o, tail,batal); } kembali; } cmp.typeCheck (o); if (head == null) {head = new Link (o, null, null); ekor = kepala; } lain jika (cmp.lessThan (o, head.getData ())) {head = new Link (o, null, head); } lain {Link l; untuk (l = head; l.next ()! = null; l = l.next ()) {if (cmp.lessThan (o, l.getData ())) {before (o, l); kembali; }} tail = Link baru (o, tail, null); } kembali; } public boolean delete (Objek o) {if (cmp == null) return false; cmp.typeCheck (o); untuk (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); kembali benar; } if (cmp.lessThan (o, l.getData ())) break; } return false; } publik disinkronkan elemen Pencacahan () {kembali baru LinkEnumerator (); } ukuran int publik () {hasil int = 0; untuk (Link l = head; l! = null; l = l.next ()) hasil ++; hasil pengembalian; }}if (head == null) {head = new Link (o, null, null); ekor = kepala; } lain jika (cmp.lessThan (o, head.getData ())) {head = new Link (o, null, head); } lain {Link l; untuk (l = head; l.next ()! = null; l = l.next ()) {if (cmp.lessThan (o, l.getData ())) {before (o, l); kembali; }} tail = Link baru (o, tail, null); } kembali; } public boolean delete (Object o) {if (cmp == null) return false; cmp.typeCheck (o); untuk (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); kembali benar; } if (cmp.lessThan (o, l.getData ())) break; } return false; } publik disinkronkan elemen Pencacahan () {kembali baru LinkEnumerator (); } ukuran int publik () {hasil int = 0; untuk (Link l = head; l! = null; l = l.next ()) hasil ++; hasil pengembalian; }}if (head == null) {head = new Link (o, null, null); ekor = kepala; } lain jika (cmp.lessThan (o, head.getData ())) {head = new Link (o, null, head); } lain {Link l; untuk (l = head; l.next ()! = null; l = l.next ()) {if (cmp.lessThan (o, l.getData ())) {before (o, l); kembali; }} tail = Link baru (o, tail, null); } kembali; } public boolean delete (Object o) {if (cmp == null) return false; cmp.typeCheck (o); untuk (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); kembali benar; } if (cmp.lessThan (o, l.getData ())) break; } return false; } publik disinkronkan elemen Pencacahan () {kembali baru LinkEnumerator (); } ukuran int publik () {hasil int = 0; untuk (Link l = head; l! = null; l = l.next ()) hasil ++; hasil pengembalian; }}} lain {Link l; untuk (l = head; l.next ()! = null; l = l.next ()) {if (cmp.lessThan (o, l.getData ())) {before (o, l); kembali; }} tail = Link baru (o, tail, null); } kembali; } public boolean delete (Object o) {if (cmp == null) return false; cmp.typeCheck (o); untuk (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); kembali benar; } if (cmp.lessThan (o, l.getData ())) break; } return false; } publik disinkronkan elemen Pencacahan () {kembali baru LinkEnumerator (); } ukuran int publik () {hasil int = 0; untuk (Link l = head; l! = null; l = l.next ()) hasil ++; hasil pengembalian; }}} lain {Link l; untuk (l = head; l.next ()! = null; l = l.next ()) {if (cmp.lessThan (o, l.getData ())) {before (o, l); kembali; }} tail = Link baru (o, tail, null); } kembali; } public boolean delete (Objek o) {if (cmp == null) return false; cmp.typeCheck (o); untuk (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); kembali benar; } if (cmp.lessThan (o, l.getData ())) break; } return false; } publik disinkronkan elemen Pencacahan () {kembali baru LinkEnumerator (); } ukuran int publik () {hasil int = 0; untuk (Link l = head; l! = null; l = l.next ()) hasil ++; hasil pengembalian; }}typeCheck (o); untuk (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); kembali benar; } if (cmp.lessThan (o, l.getData ())) break; } return false; } publik disinkronkan elemen Pencacahan () {kembali baru LinkEnumerator (); } ukuran int publik () {hasil int = 0; untuk (Link l = head; l! = null; l = l.next ()) hasil ++; hasil pengembalian; }}typeCheck (o); untuk (Link l = head; l! = null; l = l.next ()) {if (cmp.equalTo (o, l.getData ())) {remove (l); kembali benar; } if (cmp.lessThan (o, l.getData ())) break; } return false; } publik disinkronkan elemen Pencacahan () {kembali baru LinkEnumerator (); } ukuran int publik () {hasil int = 0; untuk (Link l = head; l! = null; l = l.next ()) hasil ++; hasil pengembalian; }}