Buku "Java Concurrency in Practice"

gambarHalo, habrozhiteli! Streaming adalah bagian mendasar dari platform Java. Prosesor multi-inti adalah hal biasa, dan penggunaan konkurensi yang efektif menjadi penting untuk membuat aplikasi berkinerja tinggi. Mesin virtual Java yang disempurnakan, dukungan untuk kelas berkinerja tinggi, dan serangkaian blok bangunan yang kaya untuk tugas paralelisasi pada satu waktu merupakan terobosan dalam pengembangan aplikasi paralel. Dalam Java Concurrency in Practice, pencipta teknologi terobosan itu sendiri tidak hanya menjelaskan cara kerjanya, tetapi juga berbicara tentang pola desain. Sangat mudah untuk membuat program kompetitif yang tampaknya berhasil. Namun, pengembangan, pengujian, dan debugging dari program multi-threaded menimbulkan banyak masalah. Kode berhenti bekerja tepat saat yang paling penting: di bawah beban berat.Dalam “Java Concurrency in Practice” Anda akan menemukan teori dan metode spesifik untuk membuat aplikasi paralel yang andal, dapat diskalakan dan didukung. Para penulis tidak menawarkan daftar API dan mekanisme paralelisme, mereka memperkenalkan aturan desain, pola dan model yang tidak tergantung pada versi Java dan tetap relevan dan efektif selama bertahun-tahun.

Kutipan. Keamanan benang


Anda mungkin terkejut bahwa pemrograman kompetitif dikaitkan dengan utas atau kunci (1) tidak lebih dari teknik sipil dikaitkan dengan paku keling dan balok-I. Tentu saja, pembangunan jembatan membutuhkan penggunaan sejumlah besar paku keling dan balok-I, dan hal yang sama berlaku untuk pembangunan program kompetitif, yang membutuhkan penggunaan benang dan kunci yang benar. Tetapi ini hanyalah mekanisme - sarana untuk mencapai tujuan. Menulis kode aman adalah, pada dasarnya, mengendalikan akses ke negara, dan, khususnya, ke keadaan bisa berubah.

Secara umum, keadaan suatu objek adalah datanya yang disimpan dalam variabel keadaan, seperti misalnya dan bidang statis atau bidang dari objek dependen lainnya. Status hash HashMap sebagian disimpan di HashMap itu sendiri, tetapi juga di banyak objek Map.Entry. Keadaan objek mencakup data apa pun yang dapat memengaruhi perilakunya.

(1) lock block, «», , . blocking. lock «», « ». lock , , , «». — . , , , . — . . .

Beberapa utas dapat mengakses variabel bersama, termutasi - mengubah nilainya. Faktanya, kami berusaha melindungi data, bukan kode, dari akses kompetitif yang tidak terkendali.

Membuat objek thread-safe memerlukan sinkronisasi untuk mengoordinasikan akses ke keadaan bermutasi, kegagalan untuk memenuhi yang dapat menyebabkan korupsi data dan konsekuensi yang tidak diinginkan lainnya.

Setiap kali lebih dari satu utas mengakses variabel keadaan dan salah satu utas mungkin menulis kepadanya, semua utas harus mengoordinasikan akses mereka ke sana menggunakan sinkronisasi. Sinkronisasi di Jawa disediakan oleh kata kunci yang disinkronkan, yang memberikan penguncian eksklusif, serta variabel volatil dan atom dan kunci eksplisit.

Tahan godaan untuk berpikir bahwa ada situasi yang tidak memerlukan sinkronisasi. Program ini dapat bekerja dan lulus tes, tetapi tetap tidak berfungsi dan macet kapan saja.

Jika beberapa utas mengakses variabel yang sama dengan keadaan bermutasi tanpa sinkronisasi yang tepat, maka program Anda tidak berfungsi. Ada tiga cara untuk memperbaikinya:

  • Jangan bagikan variabel status di semua utas
  • membuat variabel status tidak dapat diubah;
  • gunakan sinkronisasi status setiap kali Anda mengakses variabel status.

Koreksi mungkin memerlukan perubahan desain yang signifikan, sehingga jauh lebih mudah untuk merancang thread-safe kelas segera daripada meningkatkannya nanti.

Apakah beberapa utas akan mengakses variabel ini atau itu sulit diketahui. Untungnya, solusi teknis berorientasi objek yang membantu menciptakan kelas yang terorganisir dengan baik dan mudah dirawat - seperti enkapsulasi dan penyembunyian data - juga membantu menciptakan kelas yang aman untuk thread. Semakin sedikit utas yang memiliki akses ke variabel tertentu, semakin mudah memastikan sinkronisasi dan mengatur kondisi di mana variabel ini dapat diakses. Bahasa Java tidak memaksa Anda untuk merangkum keadaan - sangat dapat diterima untuk menyimpan keadaan di bidang publik (bahkan bidang statis publik) atau menerbitkan tautan ke objek yang bersifat internal - tetapi semakin baik kondisi program Anda dienkapsulasi,semakin mudah untuk membuat utas program Anda aman dan membantu pengelola mempertahankannya.

Saat merancang kelas yang aman untuk benang, solusi teknis berorientasi objek yang baik: enkapsulasi, mutabilitas, dan spesifikasi invarian yang jelas akan menjadi asisten Anda.

Jika solusi teknis desain berorientasi objek yang baik menyimpang dari kebutuhan pengembang, Anda harus mengorbankan aturan desain yang baik demi kinerja atau kompatibilitas dengan kode legacy. Terkadang abstraksi dan enkapsulasi berbeda dengan kinerja - walaupun tidak sesering yang dipikirkan oleh banyak pengembang - tetapi praktik terbaiknya adalah membuat kode terlebih dahulu dan kemudian cepat. Coba gunakan optimasi hanya jika pengukuran produktivitas dan kebutuhan menunjukkan bahwa Anda harus melakukannya (2) .

(2)Dalam kode kompetitif, Anda harus mematuhi praktik ini lebih dari biasanya. Karena kesalahan kompetitif sangat sulit untuk direproduksi dan tidak mudah untuk di-debug, keuntungan dari kenaikan kinerja kecil pada beberapa cabang kode yang jarang digunakan bisa sangat diabaikan dibandingkan dengan risiko bahwa program akan macet dalam kondisi operasi.

Jika Anda memutuskan bahwa Anda perlu memecah enkapsulasi, maka tidak semuanya hilang. Program Anda masih dapat dibuat utas agar aman, tetapi prosesnya akan lebih rumit dan lebih mahal, dan hasilnya tidak dapat diandalkan. Bab 4 menjelaskan kondisi-kondisi di mana enkapsulasi variabel keadaan dapat dengan aman dimitigasi.

Sejauh ini, kami telah menggunakan istilah "kelas aman thread" dan "program aman thread" hampir secara bergantian. Apakah program utas aman sepenuhnya dibangun dari kelas utas aman? Opsional: program yang seluruhnya terdiri dari kelas-kelas aman thread mungkin tidak aman thread, dan program aman thread mungkin berisi kelas-kelas yang tidak aman thread. Masalah yang berkaitan dengan tata letak kelas thread-safe juga dibahas dalam Bab 4. Dalam kasus apa pun, konsep kelas thread-safe masuk akal hanya jika kelas merangkum keadaannya sendiri. Istilah "keamanan thread" dapat diterapkan pada kode, tetapi berbicara tentang status dan hanya dapat diterapkan pada array kode yang merangkum statusnya (dapat berupa objek atau keseluruhan program).

2.1. Apa itu keamanan utas?


Menentukan keamanan utas tidak mudah. Pencarian Google cepat memberi Anda banyak pilihan seperti ini:

... dapat dipanggil dari banyak utas program tanpa interaksi yang tidak diinginkan antar utas.

... dapat dipanggil oleh dua utas atau lebih secara bersamaan, tanpa memerlukan tindakan lain dari pemanggil.

Dengan definisi seperti itu, tidak mengherankan bahwa kami menemukan keselamatan thread membingungkan! Bagaimana membedakan kelas thread-safe dari kelas yang tidak aman? Apa yang kita maksud dengan kata "aman"?

Inti dari setiap definisi yang masuk akal tentang keselamatan benang adalah gagasan tentang kebenaran.

Ketepatan menyiratkan bahwa kelas sesuai dengan spesifikasinya. Spesifikasi mendefinisikan invarian yang membatasi keadaan objek dan kondisi akhir yang menggambarkan efek operasi. Bagaimana Anda tahu bahwa spesifikasi untuk kelas sudah benar? Tidak mungkin, tetapi ini tidak menghalangi kita untuk menggunakannya setelah kita meyakinkan diri kita sendiri bahwa kode itu berfungsi. Jadi mari kita asumsikan kebenaran single-threaded adalah sesuatu yang terlihat. Sekarang kita dapat mengasumsikan bahwa kelas thread-safe berperilaku dengan benar selama akses dari beberapa utas.

Kelas adalah thread aman jika berperilaku dengan benar selama akses dari beberapa utas, terlepas dari bagaimana utas ini dijadwalkan atau disatukan oleh lingkungan kerja, dan tanpa sinkronisasi tambahan atau koordinasi lainnya pada bagian dari kode panggilan.

Program multi-utas tidak dapat menjadi utas aman jika tidak benar bahkan dalam lingkungan utas tunggal (3) . Jika objek diimplementasikan dengan benar, maka tidak ada urutan operasi - mengakses metode publik dan membaca atau menulis ke bidang publik - harus melanggar invarian atau postkondisinya. Tidak ada rangkaian operasi yang dilakukan secara berurutan atau secara kompetitif pada instance dari kelas thread-safe yang dapat menyebabkan instance berada dalam keadaan tidak valid.

(3) Jika penggunaan istilah koreksi yang longgar mengganggu Anda di sini, maka Anda dapat menganggap kelas thread-safe sebagai kelas yang salah dalam lingkungan kompetitif, serta dalam lingkungan single-threaded.

Kelas thread-safe merangkum sinkronisasi yang diperlukan sendiri dan tidak memerlukan bantuan klien.

2.1.1. Contoh: servlet tanpa dukungan keadaan internal


Dalam Bab 1, kami telah membuat daftar struktur yang membuat utas dan memanggil komponen dari mereka yang Anda bertanggung jawab atas keselamatan utas. Sekarang kami bermaksud untuk mengembangkan layanan faktorisasi servlet dan secara bertahap memperluas fungsinya dengan tetap menjaga keamanan benang.

Listing 2.1 memperlihatkan servlet sederhana yang mendekompres suatu angka dari sebuah query, memfaktorkannya, dan membungkus hasilnya sebagai respons.

Daftar 2.1. Servlet tanpa dukungan negara internal

@ThreadSafe
public class StatelessFactorizer implements Servlet {
      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            encodeIntoResponse(resp, factors);
      }
}

Kelas StatelessFactorizer, seperti kebanyakan servlet, tidak memiliki keadaan internal: tidak mengandung bidang dan tidak merujuk ke bidang dari kelas lain. Keadaan untuk perhitungan tertentu hanya ada dalam variabel lokal yang disimpan di tumpukan aliran dan hanya tersedia untuk aliran yang menjalankan. Satu utas yang mengakses StatelessFactorizer tidak dapat memengaruhi hasil utas lain yang melakukan hal yang sama, karena utas ini tidak berbagi status.

Objek tanpa dukungan keadaan internal selalu aman untuk thread.

Fakta bahwa sebagian besar servlet dapat diimplementasikan tanpa dukungan internal negara secara signifikan mengurangi beban threading servlets sendiri. Dan hanya ketika servlets perlu mengingat sesuatu, persyaratan untuk keamanan utasnya meningkat.

2.2. Atomicity


Apa yang terjadi ketika item keadaan ditambahkan ke objek tanpa dukungan keadaan internal? Misalkan kita ingin menambahkan hit counter yang mengukur jumlah permintaan yang diproses. Anda bisa menambahkan bidang tipe panjang ke servlet dan menambahkannya dengan setiap permintaan, seperti yang ditunjukkan di UnsafeCountingFactorizer di Listing 2.2.

Listing 2.2. Sebuah servlet yang menghitung permintaan tanpa sinkronisasi yang diperlukan. Ini seharusnya tidak dilakukan.

gambar

@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
      private long count = 0;

      public long getCount() { return count; }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            ++count;
            encodeIntoResponse(resp, factors);
      }
}

Sayangnya, kelas UnsafeCountingFactorizer tidak aman untuk thread, bahkan jika itu berfungsi dengan baik di lingkungan single-threaded. Seperti UnsafeSequence, ia rentan terhadap pembaruan yang hilang. Meskipun penghitungan peningkatan operasi ++ memiliki sintaks yang kompak, itu bukan atomik, yaitu, tidak dapat dibagi, tetapi urutan dari tiga operasi: memberikan nilai saat ini, menambahkan satu ke nilai saat itu dan menuliskan nilai baru kembali. Dalam operasi "baca, ubah, tulis", status yang dihasilkan diturunkan dari yang sebelumnya.

Dalam gbr. 1.1 ditunjukkan apa yang dapat terjadi jika dua utas mencoba meningkatkan penghitung pada saat yang sama, tanpa sinkronisasi. Jika penghitungnya adalah 9, maka karena koordinasi waktu yang gagal, kedua utas akan melihat nilai 9, tambahkan satu padanya, dan tetapkan nilainya menjadi 10. Jadi penghitung hit akan mulai tertinggal satu per satu.

Anda mungkin berpikir bahwa memiliki hit counter yang sedikit tidak akurat dalam layanan web adalah kerugian yang dapat diterima, dan terkadang demikian. Tetapi jika penghitung digunakan untuk membuat urutan atau pengidentifikasi unik objek, maka mengembalikan nilai yang sama dari beberapa aktivasi dapat menyebabkan masalah integritas data yang serius. Kemungkinan munculnya hasil yang salah karena koordinasi waktu yang gagal muncul dalam kondisi balapan.

2.2.1. Kondisi balapan


Kelas UnsafeCountingFactorizer memiliki beberapa kondisi lomba (4) . Jenis kondisi lomba yang paling umum adalah situasi "periksa dan kemudian bertindak", di mana pengamatan yang berpotensi usang digunakan untuk memutuskan apa yang harus dilakukan selanjutnya.

(4) (data race). , . , , , , , . Java. , , . UnsafeCountingFactorizer . 16.

Kita sering menghadapi kondisi balapan di kehidupan nyata. Misalkan Anda berencana untuk bertemu seorang teman di siang hari di Starbucks Café di Universitetskiy Prospekt. Tetapi Anda akan menemukan bahwa ada dua Starbucks di University Avenue. Pada jam 12:10 Anda tidak melihat teman Anda di kafe A dan pergi ke kafe B, tetapi dia juga tidak ada di sana. Entah teman Anda terlambat, atau dia tiba di kafe A segera setelah Anda pergi, atau dia berada di kafe B, tetapi pergi mencari Anda dan sekarang sedang dalam perjalanan ke kafe A. Kami akan menerima yang terakhir, yaitu, skenario terburuk. Sekarang 12:15, dan kalian berdua bertanya-tanya apakah temanmu menepati janjinya. Apakah Anda akan kembali ke kafe lain? Berapa kali Anda akan bolak-balik? Jika Anda belum menyetujui protokol, Anda dapat menghabiskan sepanjang hari berjalan di sepanjang University Avenue dalam euforia berkafein.
Masalah dengan pendekatan "jalan-jalan dan lihat apakah dia ada di sana" adalah bahwa berjalan di sepanjang jalan antara dua kafe membutuhkan waktu beberapa menit, dan selama waktu ini keadaan sistem dapat berubah.

Contoh dengan Starbucks menggambarkan ketergantungan hasil pada waktu koordinasi yang relatif dari peristiwa (pada berapa lama Anda menunggu seorang teman saat di kafe, dll). Pengamatan bahwa dia tidak ada di kafe A menjadi berpotensi tidak valid: begitu Anda keluar dari pintu depan, ia dapat masuk melalui pintu belakang. Sebagian besar kondisi ras menyebabkan masalah seperti pengecualian yang tidak terduga, data yang ditimpa, dan korupsi file.

2.2.2. Contoh: kondisi lomba dalam inisialisasi malas


Trik umum menggunakan pendekatan "centang dan kemudian bertindak" adalah inisialisasi malas (LazyInitRace). Tujuannya adalah untuk menunda inisialisasi objek sampai diperlukan, dan untuk memastikan bahwa inisialisasi hanya dilakukan satu kali. Dalam Listing 2.3, metode getInstance memverifikasi bahwa ExpensiveObject diinisialisasi dan mengembalikan instance yang ada, atau, sebaliknya, membuat instance baru dan mengembalikannya setelah mempertahankan referensi padanya.

Listing 2.3. Kondisi lomba inisialisasi malas. Ini seharusnya tidak dilakukan.

gambar

@NotThreadSafe
public class LazyInitRace {
      private ExpensiveObject instance = null;

      public ExpensiveObject getInstance() {
            if (instance == null)
                instance = new ExpensiveObject();
            return instance;
      }
}

Kelas LazyInitRace berisi kondisi lomba. Misalkan utas A dan B mengeksekusi metode getInstance secara bersamaan. A melihat bahwa bidang instance adalah nol, dan membuat ExpensiveObject baru. Thread B juga memeriksa untuk melihat apakah bidang instance adalah nol yang sama. Kehadiran nol di lapangan pada saat ini tergantung pada koordinasi waktu, termasuk keanehan perencanaan dan jumlah waktu yang diperlukan untuk membuat turunan dari Proyek Mahal dan menetapkan nilai dalam bidang contoh. Jika bidang instance adalah nol ketika B memeriksanya, dua elemen kode yang memanggil metode getInstance bisa mendapatkan dua hasil yang berbeda, bahkan jika metode getInstance seharusnya selalu mengembalikan instance yang sama.

Penghitung hit di UnsafeCountingFactorizer juga berisi kondisi balapan. Pendekatan "baca, ubah, tulis" menyiratkan bahwa untuk menambah penghitung, aliran harus mengetahui nilai sebelumnya dan memastikan bahwa tidak ada orang lain yang mengubah atau menggunakan nilai ini selama proses pembaruan.

Seperti kebanyakan kesalahan kompetitif, kondisi balapan tidak selalu mengarah pada kegagalan: koordinasi sementara berhasil. Tetapi jika kelas LazyInitRace digunakan untuk instantiate registri seluruh aplikasi, maka ketika itu akan mengembalikan contoh yang berbeda dari beberapa aktivasi, pendaftaran akan hilang atau tindakan akan menerima representasi yang saling bertentangan dari set objek terdaftar. Atau jika kelas UnsafeSequence digunakan untuk menghasilkan pengidentifikasi entitas dalam struktur konservasi data, maka dua objek yang berbeda dapat memiliki pengidentifikasi yang sama, melanggar batasan identitas.

2.2.3. Tindakan majemuk


LazyInitRace dan UnsafeCountingFactorizer mengandung urutan operasi yang harus bersifat atomik. Tetapi untuk mencegah kondisi balapan, harus ada hambatan bagi utas lain untuk menggunakan variabel sementara satu utas memodifikasinya.

Operasi A dan B adalah atom jika, dari sudut pandang operasi yang menjalankan thread A, operasi B dilakukan seluruhnya oleh thread lain atau bahkan tidak dilakukan sebagian.

Atomicity dari operasi kenaikan di UnsafeSequence akan menghindari kondisi balapan yang ditunjukkan pada Gambar. 1.1. Operasi "periksa dan kemudian bertindak" dan "baca, ubah, tulis" harus selalu berupa atom. Mereka disebut tindakan gabungan - urutan operasi yang harus dilakukan secara atom agar tetap aman. Pada bagian selanjutnya, kami akan mempertimbangkan penguncian - sebuah mekanisme yang dibangun di Jawa yang menyediakan atomitas. Sementara itu, kami akan memperbaiki masalah dengan cara lain dengan menerapkan kelas aman-thread yang ada, seperti yang ditunjukkan dalam Penghitung faktor di Listing 2.4.

Listing 2.4. Permintaan penghitungan servlet menggunakan AtomicLong

@ThreadSafe
public class CountingFactorizer implements Servlet {
      private final AtomicLong count = new AtomicLong(0);

      public long getCount() { return count.get(); }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            count.incrementAndGet();
            encodeIntoResponse(resp, factors);
      }
}

Paket java.util.concurrent.atomic berisi variabel atom untuk mengelola status kelas. Mengganti tipe penghitung dari panjang ke AtomicLong, kami menjamin bahwa semua tindakan yang merujuk pada status penghitung adalah atomic1. Karena keadaan servlet adalah keadaan penghitung, dan penghitung itu aman dari benang, servlet kami menjadi aman dari benang.

Ketika elemen keadaan tunggal ditambahkan ke kelas yang tidak mendukung keadaan internal, kelas yang dihasilkan akan aman thread jika negara sepenuhnya dikendalikan oleh objek aman thread. Tetapi, seperti yang akan kita lihat di bagian berikutnya, transisi dari satu variabel keadaan ke variabel berikutnya tidak akan semudah transisi dari nol ke satu.

Jika nyaman, gunakan objek aman yang ada, seperti AtomicLong, untuk mengontrol keadaan kelas Anda. Kemungkinan status objek thread-safe yang ada dan transisinya ke status lain lebih mudah dipelihara dan diperiksa keamanannya daripada variabel status arbitrer.

»Informasi lebih lanjut tentang buku ini dapat ditemukan di situs web penerbit
» Isi
» Kutipan

Untuk Khabrozhiteley Diskon 25% untuk kupon - Jawa

Setelah pembayaran versi kertas buku, sebuah buku elektronik dikirim melalui email.

All Articles