Jawaban:
Ada dua kegunaan utama AtomicInteger
:
Sebagai penghitung atom ( incrementAndGet()
, dll) yang dapat digunakan oleh banyak utas secara bersamaan
Sebagai primitif yang mendukung instruksi compare-and-swap ( compareAndSet()
) untuk mengimplementasikan algoritma non-blocking.
Berikut adalah contoh generator nomor acak non-blocking dari Java Concurrency In Practice Brian Göetz :
public class AtomicPseudoRandom extends PseudoRandom {
private AtomicInteger seed;
AtomicPseudoRandom(int seed) {
this.seed = new AtomicInteger(seed);
}
public int nextInt(int n) {
while (true) {
int s = seed.get();
int nextSeed = calculateNext(s);
if (seed.compareAndSet(s, nextSeed)) {
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
...
}
Seperti yang Anda lihat, ini pada dasarnya bekerja dengan cara yang hampir sama incrementAndGet()
, tetapi melakukan perhitungan sewenang-wenang ( calculateNext()
) alih-alih kenaikan (dan memproses hasilnya sebelum kembali).
read
dan write that value + 1
, ini terdeteksi daripada menimpa pembaruan lama (menghindari masalah "pembaruan hilang"). Ini sebenarnya adalah kasus khusus compareAndSet
- jika nilai lama adalah 2
, kelas sebenarnya memanggil compareAndSet(2, 3)
- jadi jika utas lain telah mengubah nilai sementara itu, metode kenaikan secara efektif restart dari awal.
Contoh paling sederhana mutlak yang dapat saya pikirkan adalah membuat penambahan operasi atom.
Dengan int standar:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
Dengan AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Yang terakhir adalah cara yang sangat sederhana untuk melakukan efek mutasi sederhana (terutama penghitungan, atau pengindeksan unik), tanpa harus menggunakan sinkronisasi semua akses.
Logika bebas sinkronisasi yang lebih kompleks dapat digunakan dengan menggunakan compareAndSet()
jenis penguncian optimis - dapatkan nilai saat ini, hitung hasil berdasarkan ini, tetapkan hasil ini jika nilai masih input yang digunakan untuk melakukan perhitungan, lain mulai lagi - tetapi menghitung contoh sangat berguna, dan saya akan sering menggunakan AtomicIntegers
penghitungan dan generator unik VM-lebar jika ada beberapa thread terlibat, karena mereka sangat mudah untuk bekerja dengan saya hampir menganggap itu optimasi prematur untuk menggunakan polos ints
.
Meskipun Anda hampir selalu dapat mencapai jaminan sinkronisasi yang sama dengan ints
dan synchronized
deklarasi yang sesuai , keindahan dari AtomicInteger
thread-safety dibangun ke dalam objek aktual itu sendiri, daripada Anda perlu khawatir tentang kemungkinan interleavings, dan monitor yang dimiliki, dari setiap metode itu terjadi untuk mengakses int
nilai. Jauh lebih sulit untuk secara tidak sengaja melanggar keamanan threads saat menelepon getAndIncrement()
daripada ketika kembali i++
dan mengingat (atau tidak) untuk mendapatkan set monitor yang benar sebelumnya.
Jika Anda melihat metode yang dimiliki AtomicInteger, Anda akan melihat bahwa mereka cenderung sesuai dengan operasi umum pada int. Misalnya:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
adalah versi yang aman untuk ini:
static int i;
// Later, in a thread
int current = ++i;
Metode peta seperti ini:
++i
is i.incrementAndGet()
i++
is i.getAndIncrement()
--i
is i.decrementAndGet()
i--
is i.getAndDecrement()
i = x
is i.set(x)
x = i
isx = i.get()
Ada metode kenyamanan lain juga, seperti compareAndSet
atauaddAndGet
Penggunaan utama AtomicInteger
adalah ketika Anda berada dalam konteks multithreaded dan Anda perlu melakukan operasi aman thread pada integer tanpa menggunakan synchronized
. Penugasan dan pengambilan pada tipe primitif int
sudah bersifat atomik tetapi AtomicInteger
dilengkapi dengan banyak operasi yang bukan atomik int
.
Yang paling sederhana adalah getAndXXX
atau xXXAndGet
. Misalnya getAndIncrement()
adalah setara atom i++
yang bukan atom karena sebenarnya merupakan jalan pintas untuk tiga operasi: pengambilan, penambahan dan penugasan. compareAndSet
sangat berguna untuk mengimplementasikan semaphores, kunci, kait, dll.
Menggunakan AtomicInteger
lebih cepat dan lebih mudah dibaca daripada melakukan sinkronisasi menggunakan yang sama.
Tes sederhana:
public synchronized int incrementNotAtomic() {
return notAtomic++;
}
public void performTestNotAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
incrementNotAtomic();
}
System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}
public void performTestAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
atomic.getAndIncrement();
}
System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}
Pada PC saya dengan Java 1.6 tes atom berjalan dalam 3 detik sementara yang disinkronkan berjalan dalam sekitar 5,5 detik. Masalahnya di sini adalah bahwa operasi untuk menyinkronkan ( notAtomic++
) sangat singkat. Jadi biaya sinkronisasi sangat penting dibandingkan dengan operasi.
Di samping atomicity, AtomicInteger dapat digunakan sebagai versi yang bisa berubah Integer
misalnya dalam Map
nilai s.
AtomicInteger
sebagai kunci peta, karena menggunakan equals()
implementasi default , yang hampir pasti bukan semantik yang Anda harapkan jika digunakan dalam peta.
Sebagai contoh, saya memiliki perpustakaan yang menghasilkan instance dari beberapa kelas. Masing-masing instance harus memiliki ID integer unik, karena instance ini mewakili perintah yang dikirim ke server, dan setiap perintah harus memiliki ID unik. Karena beberapa utas diizinkan untuk mengirim perintah secara bersamaan, saya menggunakan AtomicInteger untuk menghasilkan ID tersebut. Pendekatan alternatif adalah dengan menggunakan semacam kunci dan integer biasa, tapi itu lebih lambat dan kurang elegan.
Seperti kata gabuzo, kadang-kadang saya menggunakan AtomicIntegers ketika saya ingin melewatkan int dengan referensi. Ini adalah kelas bawaan yang memiliki kode khusus arsitektur, jadi lebih mudah dan cenderung lebih dioptimalkan daripada MutableInteger yang bisa saya kodekan dengan cepat. Yang mengatakan, rasanya seperti penyalahgunaan kelas.
Di Java 8, kelas atom telah diperluas dengan dua fungsi menarik:
Keduanya menggunakan fungsi update untuk melakukan pembaruan nilai atom. Perbedaannya adalah bahwa yang pertama mengembalikan nilai lama dan yang kedua mengembalikan nilai baru. Fungsi pemutakhiran dapat diterapkan untuk melakukan operasi "bandingkan dan set" yang lebih kompleks daripada yang standar. Sebagai contoh ia dapat memeriksa bahwa penghitung atom tidak mencapai di bawah nol, biasanya itu akan memerlukan sinkronisasi, dan di sini kode bebas dari kunci:
public class Counter {
private final AtomicInteger number;
public Counter(int number) {
this.number = new AtomicInteger(number);
}
/** @return true if still can decrease */
public boolean dec() {
// updateAndGet(fn) executed atomically:
return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
}
}
Kode diambil dari Java Atomic Contoh .
Saya biasanya menggunakan AtomicInteger ketika saya harus memberikan Id ke objek yang dapat diakses atau dibuat dari beberapa utas, dan saya biasanya menggunakannya sebagai atribut statis pada kelas yang saya akses di konstruktor objek.
Anda dapat menerapkan kunci non-pemblokiran menggunakan compareAndSwap (CAS) pada bilangan bulat atom atau panjang. The "TL2" Software Transaksional Memori kertas menggambarkan ini:
Kami mengaitkan kunci-tulis versi khusus dengan setiap lokasi memori yang ditransaksikan. Dalam bentuknya yang paling sederhana, kunci-tulis versi adalah spinlock kata tunggal yang menggunakan operasi CAS untuk mendapatkan kunci dan toko untuk melepaskannya. Karena satu hanya membutuhkan bit tunggal untuk menunjukkan bahwa kunci diambil, kami menggunakan sisa kata kunci untuk menyimpan nomor versi.
Apa yang digambarkannya adalah pertama membaca bilangan bulat atom. Bagi ini menjadi bit kunci yang diabaikan dan nomor versi. Mencoba untuk CAS menulisnya sebagai bit kunci dibersihkan dengan nomor versi saat ini ke set bit kunci dan nomor versi berikutnya. Ulangi sampai Anda berhasil dan Anda adalah utas yang memiliki kunci. Buka kunci dengan mengatur nomor versi saat ini dengan kunci-bit dihapus. Makalah ini menjelaskan penggunaan nomor versi dalam kunci untuk mengoordinasikan bahwa utas memiliki rangkaian pembacaan yang konsisten ketika mereka menulis.
Artikel ini menjelaskan bahwa prosesor memiliki dukungan perangkat keras untuk membandingkan dan menukar operasi yang membuatnya sangat efisien. Ia juga mengklaim:
non-blocking counter berbasis CAS menggunakan variabel atom memiliki kinerja yang lebih baik daripada counter berbasis kunci dalam pertentangan rendah hingga sedang
Kuncinya adalah mereka memungkinkan akses bersamaan dan modifikasi dengan aman. Mereka umumnya digunakan sebagai penghitung di lingkungan multithreaded - sebelum pengenalan mereka ini harus menjadi kelas tertulis pengguna yang membungkus berbagai metode dalam blok yang disinkronkan.
Saya menggunakan AtomicInteger untuk memecahkan masalah Dining Philosopher.
Dalam solusi saya, contoh AtomicInteger digunakan untuk mewakili garpu, ada dua yang diperlukan per filsuf. Setiap filsuf diidentifikasi sebagai bilangan bulat, 1 sampai 5. Ketika garpu digunakan oleh seorang filsuf, AtomicInteger memegang nilai filsuf, 1 sampai 5, jika garpu tidak digunakan sehingga nilai AtomicInteger adalah -1 .
AtomicInteger kemudian memungkinkan untuk memeriksa apakah garpu bebas, nilai == - 1, dan mengaturnya ke pemilik garpu jika gratis, dalam satu operasi atom. Lihat kode di bawah ini.
AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){
if (Hungry) {
//if fork is free (==-1) then grab it by denoting who took it
if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
//at least one fork was not succesfully grabbed, release both and try again later
fork0.compareAndSet(p, -1);
fork1.compareAndSet(p, -1);
try {
synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork
lock.wait();//try again later, goes back up the loop
}
} catch (InterruptedException e) {}
} else {
//sucessfully grabbed both forks
transition(fork_l_free_and_fork_r_free);
}
}
}
Karena metode compareAndSet tidak memblokir, itu harus meningkatkan throughput, lebih banyak pekerjaan yang dilakukan. Seperti yang Anda ketahui, masalah Makan Filsuf digunakan ketika terkontrol diakses ke sumber daya diperlukan, yaitu garpu, diperlukan, seperti proses membutuhkan sumber daya untuk terus melakukan pekerjaan.
Contoh sederhana untuk fungsi compareAndSet ():
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val = new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(0, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
Dicetak adalah: nilai sebelumnya: 0 Nilai telah diperbarui dan itu adalah 6 Contoh sederhana lainnya:
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val
= new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(10, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
Dicetak adalah: Nilai sebelumnya: 0 Nilai tidak diperbarui