Pengantar Variabel Atom di Jawa

1. Perkenalan

Sederhananya, keadaan bersama yang bisa berubah sangat mudah mengarah ke masalah saat konkurensi terlibat. Jika akses ke objek yang bisa berubah bersama tidak dikelola dengan benar, aplikasi dapat dengan cepat menjadi rentan terhadap beberapa kesalahan konkurensi yang sulit dideteksi.

Dalam artikel ini, kita akan meninjau kembali penggunaan kunci untuk menangani akses bersamaan, menjelajahi beberapa kerugian yang terkait dengan kunci, dan terakhir, memperkenalkan variabel atom sebagai alternatif.

2. Kunci

Mari kita lihat kelasnya:

public class Counter { int counter; public void increment() { counter++; } }

Dalam kasus lingkungan single-threaded, ini bekerja dengan sempurna; namun, segera setelah kami mengizinkan lebih dari satu utas untuk menulis, kami mulai mendapatkan hasil yang tidak konsisten.

Ini karena operasi penambahan sederhana ( penghitung ++ ), yang mungkin terlihat seperti operasi atomik, tetapi sebenarnya adalah kombinasi dari tiga operasi: mendapatkan nilai, menaikkan, dan menulis kembali nilai yang diperbarui.

Jika dua utas mencoba mendapatkan dan memperbarui nilai pada saat yang sama, ini dapat mengakibatkan pembaruan yang hilang.

Salah satu cara untuk mengelola akses ke suatu objek adalah dengan menggunakan kunci. Ini dapat dicapai dengan menggunakan kata kunci tersinkronisasi dalam tanda tangan metode kenaikan . Kata kunci tersinkronisasi memastikan bahwa hanya satu utas yang dapat masuk ke metode pada satu waktu (untuk mempelajari lebih lanjut tentang Penguncian dan Sinkronisasi, lihat - Panduan Kata Kunci Tersinkron di Java):

public class SafeCounterWithLock { private volatile int counter; public synchronized void increment() { counter++; } }

Selain itu, kami perlu menambahkan kata kunci yang mudah menguap untuk memastikan visibilitas referensi yang tepat di antara utas.

Menggunakan kunci memecahkan masalah. Namun, kinerjanya terpukul.

Ketika beberapa utas mencoba mendapatkan kunci, salah satunya menang, sementara utas lainnya diblokir atau ditangguhkan.

Proses menangguhkan dan melanjutkan utas sangat mahal dan memengaruhi efisiensi sistem secara keseluruhan.

Dalam program kecil, seperti penghitung , waktu yang dihabiskan dalam pengalihan konteks dapat menjadi lebih dari sekadar eksekusi kode yang sebenarnya, sehingga sangat mengurangi efisiensi secara keseluruhan.

3. Operasi Atom

Ada cabang penelitian yang berfokus pada pembuatan algoritma non-pemblokiran untuk lingkungan bersamaan. Algoritme ini mengeksploitasi instruksi mesin atom tingkat rendah seperti bandingkan-dan-tukar (CAS), untuk memastikan integritas data.

Operasi CAS tipikal bekerja pada tiga operan:

  1. Lokasi memori untuk mengoperasikan (M)
  2. Nilai yang diharapkan (A) dari variabel yang ada
  3. Nilai baru (B) yang perlu disetel

Operasi CAS memperbarui nilai secara atomis dalam M ke B, tetapi hanya jika nilai yang ada di M cocok dengan A, jika tidak, tidak ada tindakan yang diambil.

Dalam kedua kasus tersebut, nilai yang ada di M dikembalikan. Ini menggabungkan tiga langkah - mendapatkan nilai, membandingkan nilai, dan memperbarui nilai - menjadi satu operasi tingkat mesin.

Ketika beberapa utas mencoba memperbarui nilai yang sama melalui CAS, salah satunya menang dan memperbarui nilainya. Namun, tidak seperti dalam kasus kunci, tidak ada utas lain yang ditangguhkan ; sebaliknya, mereka hanya diberi tahu bahwa mereka tidak berhasil memperbarui nilainya. Utas kemudian dapat melanjutkan untuk melakukan pekerjaan lebih lanjut dan sakelar konteks sepenuhnya dihindari.

Konsekuensi lainnya adalah logika program inti menjadi lebih kompleks. Ini karena kita harus menangani skenario ketika operasi CAS tidak berhasil. Kami dapat mencobanya lagi dan lagi hingga berhasil, atau kami tidak dapat melakukan apa pun dan melanjutkan tergantung pada kasus penggunaan.

4. Variabel Atom di Jawa

Kelas variabel atom yang paling umum digunakan di Java adalah AtomicInteger, AtomicLong, AtomicBoolean, dan AtomicReference. Kelas-kelas ini masing-masing mewakili referensi int , long , boolean, dan objek yang dapat diperbarui secara atomik. Metode utama yang diekspos oleh kelas-kelas ini adalah:

  • get () - mendapatkan nilai dari memori, sehingga perubahan yang dibuat oleh utas lain terlihat; setara dengan membaca variabel volatil
  • set () - menulis nilai ke memori, sehingga perubahan tersebut terlihat ke thread lain; setara dengan menulis variabel volatil
  • lazySet () - akhirnya menulis nilai ke memori, mungkin disusun ulang dengan operasi memori yang relevan berikutnya. Salah satu kasus penggunaan adalah membatalkan referensi, demi pengumpulan sampah, yang tidak akan pernah bisa diakses lagi. Dalam hal ini, kinerja yang lebih baik dicapai dengan menunda penulisan volatil nol
  • bandingkanAndSet () - sama seperti yang dijelaskan di bagian 3, mengembalikan nilai true jika berhasil, jika tidak salah
  • weakCompareAndSet () - sama seperti yang dijelaskan di bagian 3, tetapi lebih lemah dalam artian, bahwa ia tidak membuat pengurutan yang terjadi sebelum. Ini berarti bahwa itu mungkin tidak selalu melihat pembaruan yang dilakukan pada variabel lain. Mulai Java 9, metode ini sudah tidak digunakan lagi di semua implementasi atomik yang mendukung weakCompareAndSetPlain () . Efek memori dari weakCompareAndSet () jelas tetapi namanya menyiratkan efek memori yang mudah menguap. Untuk menghindari kebingungan ini, mereka menghentikan metode ini dan menambahkan empat metode dengan efek memori berbeda seperti weakCompareAndSetPlain () atau weakCompareAndSetVolatile ()

Penghitung thread-safe yang diimplementasikan dengan AtomicInteger ditunjukkan pada contoh di bawah ini:

public class SafeCounterWithoutLock { private final AtomicInteger counter = new AtomicInteger(0); public int getValue() { return counter.get(); } public void increment() { while(true) { int existingValue = getValue(); int newValue = existingValue + 1; if(counter.compareAndSet(existingValue, newValue)) { return; } } } }

Seperti yang Anda lihat, kami mencoba kembali operasi bandingkanAndSet dan sekali lagi jika gagal, karena kami ingin menjamin bahwa panggilan ke metode increment selalu meningkatkan nilainya sebesar 1.

5. Kesimpulan

Dalam tutorial singkat ini, kami menjelaskan cara alternatif untuk menangani konkurensi di mana kerugian yang terkait dengan penguncian dapat dihindari. Kami juga melihat metode utama yang diekspos oleh kelas variabel atom di Java.

Seperti biasa, semua contoh tersedia di GitHub.

Untuk menjelajahi lebih banyak kelas yang secara internal menggunakan algoritme non-pemblokiran, lihat panduan untuk ConcurrentMap.