Kapan menggunakan AtomicReference di Java?

311

Kapan kita menggunakan AtomicReference?

Apakah diperlukan untuk membuat objek di semua program multithread?

Berikan contoh sederhana di mana AtomicReference harus digunakan.

Chintu
sumber

Jawaban:

215

Referensi atom harus digunakan dalam pengaturan di mana Anda perlu melakukan operasi atom sederhana (yaitu thread-safe , non-trivial) pada referensi, di mana sinkronisasi berbasis monitor tidak tepat. Misalkan Anda ingin memeriksa untuk melihat apakah bidang tertentu hanya jika keadaan objek tetap saat Anda terakhir memeriksa:

AtomicReference<Object> cache = new AtomicReference<Object>();

Object cachedValue = new Object();
cache.set(cachedValue);

//... time passes ...
Object cachedValueToUpdate = cache.get();
//... do some work to transform cachedValueToUpdate into a new version
Object newValue = someFunctionOfOld(cachedValueToUpdate);
boolean success = cache.compareAndSet(cachedValue,cachedValueToUpdate);

Karena semantik referensi atom, Anda dapat melakukan ini bahkan jika cacheobjek dibagi di antara utas, tanpa menggunakan synchronized. Secara umum, Anda lebih baik menggunakan sinkronisasi atau java.util.concurrentkerangka daripada telanjang Atomic*kecuali Anda tahu apa yang Anda lakukan.

Dua referensi pohon mati yang bagus yang akan memperkenalkan Anda pada topik ini:

Perhatikan bahwa (saya tidak tahu apakah ini selalu benar) tugas referensi (yaitu =) itu sendiri adalah atomik (memperbarui tipe 64-bit primitif suka longatau doublemungkin tidak menjadi atom; tetapi memperbarui referensi selalu atom, meskipun itu 64 bit ) tanpa secara eksplisit menggunakan Atomic*.
Lihat Spesifikasi Bahasa Jawa 3ed, Bagian 17.7 .

andersoj
sumber
43
Perbaiki saya jika saya salah, tetapi sepertinya kunci untuk memerlukan ini adalah karena Anda perlu melakukan "compareAndSet". Jika semua yang perlu saya lakukan adalah mengatur saya tidak akan memerlukan AtomicObject sama sekali karena pembaruan referensi sendiri menjadi atom?
sMoZely
Apakah aman untuk melakukan cache.compareAndSet (cachedValue, someFunctionOfOld (cachedValueToUpdate))? Yaitu inline perhitungan?
kaqqao
4
argumen fungsi @veggen di Jawa dievaluasi sebelum fungsi itu sendiri, jadi inlining tidak membuat perbedaan dalam kasus ini. Ya, aman.
Dmitry
29
@sMoZely Itu benar, tetapi jika Anda tidak menggunakan AtomicReferenceAnda harus menandai variabel volatilekarena sementara runtime menjamin bahwa penugasan referensi adalah atom, kompiler dapat melakukan optimasi dengan asumsi bahwa variabel tidak sedang dimodifikasi oleh utas lainnya.
kbolino
1
Catatan @BradCupit yang saya katakan "jika Anda tidak menggunakan AtomicReference"; jika Anda sedang menggunakannya, maka saran saya akan pergi ke arah yang berlawanan dan menandainya finalsehingga compiler dapat mengoptimalkannya.
kbolino
91

Referensi atom sangat ideal untuk digunakan ketika Anda perlu berbagi dan mengubah keadaan objek abadi antara beberapa utas. Itu adalah pernyataan yang sangat padat jadi saya akan memecahnya sedikit.

Pertama, objek abadi adalah objek yang secara efektif tidak berubah setelah konstruksi. Seringkali metode objek yang tidak dapat diubah mengembalikan instance baru dari kelas yang sama. Beberapa contoh termasuk kelas pembungkus Long dan Double, serta String, hanya untuk beberapa nama. (Berdasarkan Pemrograman Concurrency pada JVM objek abadi adalah bagian penting dari concurrency modern).

Selanjutnya, mengapa AtomicReference lebih baik daripada objek yang mudah menguap untuk berbagi nilai bersama itu. Contoh kode sederhana akan menunjukkan perbedaannya.

volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
  synchronized(lock){
    sharedValue=sharedValue+"something to add";
  }
}

Setiap kali Anda ingin memodifikasi string yang dirujuk oleh bidang volatil itu berdasarkan nilai saat ini, Anda harus terlebih dahulu mendapatkan kunci pada objek itu. Ini mencegah beberapa utas lainnya masuk saat itu dan mengubah nilai di tengah rangkaian string baru. Kemudian ketika utas Anda kembali, Anda akan merusak pekerjaan utas lainnya. Tapi jujur ​​kode itu akan berfungsi, itu terlihat bersih, dan itu akan membuat kebanyakan orang senang.

Sedikit masalah. Itu lambat. Terutama jika ada banyak pertengkaran Obyek kunci itu. Itu karena sebagian besar kunci memerlukan panggilan sistem OS, dan utas Anda akan diblokir dan secara konteks dialihkan dari CPU untuk memberi jalan bagi proses lain.

Opsi lainnya adalah menggunakan AtomicRefrence.

public static AtomicReference<String> shared = new AtomicReference<>();
String init="Inital Value";
shared.set(init);
//now we will modify that value
boolean success=false;
while(!success){
  String prevValue=shared.get();
  // do all the work you need to
  String newValue=shared.get()+"lets add something";
  // Compare and set
  success=shared.compareAndSet(prevValue,newValue);
}

Sekarang mengapa ini lebih baik? Jujur kode itu sedikit kurang bersih dari sebelumnya. Tetapi ada sesuatu yang sangat penting yang terjadi di bawah tenda di AtomicRefrence, yaitu membandingkan dan menukar. Ini adalah instruksi CPU tunggal, bukan panggilan OS, yang membuat saklar terjadi. Itu adalah instruksi tunggal pada CPU. Dan karena tidak ada kunci, tidak ada saklar konteks dalam kasus di mana kunci dijalankan yang menghemat lebih banyak waktu!

Tangkapannya adalah, untuk AtomicReferences, ini tidak menggunakan panggilan .equals (), melainkan perbandingan == untuk nilai yang diharapkan. Jadi pastikan yang diharapkan adalah objek aktual yang dikembalikan dari get in the loop.

Erik Helleren
sumber
14
Dua contoh Anda berperilaku berbeda. Anda harus mengulang workeduntuk mendapatkan semantik yang sama.
CurtainDog
5
Saya pikir Anda harus menginisialisasi nilai di dalam konstruktor AtomicReference, jika tidak, utas lain mungkin masih melihat nilai nol sebelum Anda memanggil shared.set. (Kecuali shared.set dijalankan dalam penginisialisasi statis.)
Henno Vermeulen
8
Dalam contoh kedua, Anda harus menggunakan Java 8 seperti: shared.updateAndGet ((x) -> (x + "mari kita tambahkan sesuatu")); ... yang akan berulang kali memanggil .compareAndSet hingga berfungsi. Itu sama dengan blok yang disinkronkan yang akan selalu berhasil. Anda perlu memastikan bahwa lambda yang Anda lewati bebas efek samping karena dapat dipanggil beberapa kali.
Tom Dibble
2
Tidak perlu membuat String sharedValue yang mudah menguap. Sinkronisasi (kunci) cukup baik untuk memastikan terjadinya sebelum hubungan.
Jai Pandit
2
"... ubah status objek abadi" tidak tepat di sini, sebagian karena secara literal Anda tidak dapat mengubah status objek abadi. Contoh tersebut menunjukkan perubahan referensi dari satu instance objek yang tidak dapat diubah ke yang berbeda. Saya menyadari bahwa itu luar biasa tetapi saya pikir ini layak untuk disoroti mengingat betapa membingungkannya logika Thread.
Mark Phillips
30

Berikut ini adalah kasus penggunaan untuk AtomicReference:

Pertimbangkan kelas ini yang bertindak sebagai rentang angka, dan menggunakan variabel AtmomicInteger individual untuk mempertahankan batas angka bawah dan atas.

public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0);
    private final AtomicInteger upper = new AtomicInteger(0);

    public void setLower(int i) {
        // Warning -- unsafe check-then-act
        if (i > upper.get())
            throw new IllegalArgumentException(
                    "can't set lower to " + i + " > upper");
        lower.set(i);
    }

    public void setUpper(int i) {
        // Warning -- unsafe check-then-act
        if (i < lower.get())
            throw new IllegalArgumentException(
                    "can't set upper to " + i + " < lower");
        upper.set(i);
    }

    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

Baik setLower dan setUpper adalah urutan centang lalu tindakan, tetapi keduanya tidak menggunakan penguncian yang memadai untuk membuatnya menjadi atom. Jika rentang nomor berlaku (0, 10), dan satu utas memanggil setLower (5) sedangkan utas lainnya memanggil setUpper (4), dengan beberapa waktu sial keduanya akan melewati pemeriksaan di setter dan kedua modifikasi akan diterapkan. Hasilnya adalah bahwa rentang sekarang memegang (5, 4) keadaan tidak valid. Jadi sementara AtomicIntegers yang mendasarinya adalah thread-safe, kelas komposit tidak. Ini dapat diperbaiki dengan menggunakan AtomicReference daripada menggunakan AtomicIntegers individu untuk batas atas dan bawah.

public class CasNumberRange {
    // Immutable
    private static class IntPair {
        final int lower;  // Invariant: lower <= upper
        final int upper;

        private IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values = 
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public void setLower(int lower) {
        while (true) {
            IntPair oldv = values.get();
            if (lower > oldv.upper)
                throw new IllegalArgumentException(
                    "Can't set lower to " + lower + " > upper");
            IntPair newv = new IntPair(lower, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setUpper(int upper) {
        while (true) {
            IntPair oldv = values.get();
            if (upper < oldv.lower)
                throw new IllegalArgumentException(
                    "Can't set upper to " + upper + " < lower");
            IntPair newv = new IntPair(oldv.lower, upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}
Binita Bharati
sumber
2
Artikel ini mirip dengan jawaban Anda, tetapi membahas hal-hal yang lebih rumit. Ini menarik! ibm.com/developerworks/java/library/j-jtp04186
LppEdd
20

Anda dapat menggunakan AtomicReference saat menerapkan kunci optimis. Anda memiliki objek yang dibagikan dan Anda ingin mengubahnya dari lebih dari 1 utas.

  1. Anda dapat membuat salinan dari objek yang dibagikan
  2. Ubah objek yang dibagikan
  3. Anda perlu memeriksa bahwa objek yang dibagikan masih sama seperti sebelumnya - jika ya, perbarui dengan referensi salinan yang dimodifikasi.

Seperti utas lain yang mungkin telah memodifikasinya dan / dapat memodifikasi antara 2 langkah ini. Anda perlu melakukannya dalam operasi atom. Di sinilah AtomicReference dapat membantu

HamoriZ
sumber
7

Berikut ini adalah use case yang sangat sederhana dan tidak ada hubungannya dengan keamanan thread.

Untuk berbagi objek antara permintaan lambda, AtomicReferenceini adalah opsi :

public void doSomethingUsingLambdas() {

    AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();

    soSomethingThatTakesALambda(() -> {
        yourObjectRef.set(youObject);
    });

    soSomethingElseThatTakesALambda(() -> {
        YourObject yourObject = yourObjectRef.get();
    });
}

Saya tidak mengatakan ini adalah desain yang baik atau apa pun (itu hanya contoh sepele), tetapi jika Anda memiliki kasing di mana Anda perlu berbagi objek antara permintaan lambda, AtomicReference adalah pilihan.

Bahkan Anda dapat menggunakan objek apa pun yang memegang referensi, bahkan Koleksi yang hanya memiliki satu item. Namun, AtomicReference sangat cocok.

Benny Bottema
sumber
6

Saya tidak akan banyak bicara. Rekan-rekan saya yang terhormat telah memberikan masukan berharga mereka.Kode berjalan yang lengkap pada akhir blog ini akan menghilangkan kebingungan. Ini tentang program pemesanan kursi film kecil dalam skenario multi-utas.

Beberapa fakta dasar yang penting adalah sebagai berikut. 1> Berbagai utas hanya dapat bersaing sebagai contoh dan variabel anggota statis di ruang tumpukan. 2> Volatile baca atau tulis sepenuhnya atom dan serial / terjadi sebelumnya dan hanya dilakukan dari memori. Dengan mengatakan ini, saya maksudkan bahwa setiap pembacaan akan mengikuti tulisan sebelumnya dalam memori. Dan setiap tulisan akan mengikuti pembacaan sebelumnya dari memori. Jadi utas apa pun yang bekerja dengan volatil akan selalu melihat nilai paling baru. AtomicReference menggunakan properti volatil ini.

Berikut adalah beberapa kode sumber AtomicReference. AtomicReference mengacu pada referensi objek. Referensi ini adalah variabel anggota yang mudah menguap dalam contoh AtomicReference seperti di bawah ini.

private volatile V value;

dapatkan () cukup kembalikan nilai terbaru dari variabel (seperti halnya volatil dengan cara "terjadi sebelum").

public final V get()

Berikut ini adalah metode paling penting dari AtomicReference.

public final boolean  compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

Metode compareAndSet (mengharapkan, memperbarui) memanggil metode compareAndSwapObject () dari kelas Java yang tidak aman. Metode panggilan tidak aman ini memanggil panggilan asli, yang memanggil satu instruksi ke prosesor. "harapkan" dan "perbarui" setiap referensi sebuah objek.

Jika dan hanya jika variabel anggota instance AtomicReference "nilai" merujuk ke objek yang sama disebut oleh "ekspektasi", "pembaruan" ditugaskan ke variabel instance ini sekarang, dan "benar" dikembalikan. Atau yang lain, false dikembalikan. Semuanya dilakukan secara atom. Tidak ada utas lain yang dapat mencegahnya. Karena ini adalah operasi prosesor tunggal (keajaiban arsitektur komputer modern), seringkali lebih cepat daripada menggunakan blok yang disinkronkan. Tapi ingat bahwa ketika beberapa variabel perlu diperbarui secara atom, AtomicReference tidak akan membantu.

Saya ingin menambahkan kode berjalan yang lengkap, yang dapat dijalankan di gerhana. Itu akan menghapus banyak kebingungan. Di sini 22 pengguna (utasku) mencoba memesan 20 kursi. Berikut ini adalah cuplikan kode diikuti oleh kode lengkap.

Cuplikan kode tempat 22 pengguna mencoba memesan 20 kursi.

for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }

Berikut ini adalah kode berjalan lengkap.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class Solution {

    static List<AtomicReference<Integer>> seats;// Movie seats numbered as per
                                                // list index

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        seats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }
        for (Thread t : ths) {
            t.join();
        }
        for (AtomicReference<Integer> seat : seats) {
            System.out.print(" " + seat.get());
        }
    }

    /**
     * id is the id of the user
     * 
     * @author sankbane
     *
     */
    static class MyTh extends Thread {// each thread is a user
        static AtomicInteger full = new AtomicInteger(0);
        List<AtomicReference<Integer>> l;//seats
        int id;//id of the users
        int seats;

        public MyTh(List<AtomicReference<Integer>> list, int userId) {
            l = list;
            this.id = userId;
            seats = list.size();
        }

        @Override
        public void run() {
            boolean reserved = false;
            try {
                while (!reserved && full.get() < seats) {
                    Thread.sleep(50);
                    int r = ThreadLocalRandom.current().nextInt(0, seats);// excludes
                                                                            // seats
                                                                            //
                    AtomicReference<Integer> el = l.get(r);
                    reserved = el.compareAndSet(null, id);// null means no user
                                                            // has reserved this
                                                            // seat
                    if (reserved)
                        full.getAndIncrement();
                }
                if (!reserved && full.get() == seats)
                    System.out.println("user " + id + " did not get a seat");
            } catch (InterruptedException ie) {
                // log it
            }
        }
    }

}    
sankar banerjee
sumber
5

Kapan kita menggunakan AtomicReference?

AtomicReference adalah cara fleksibel untuk memperbarui nilai variabel secara atomis tanpa menggunakan sinkronisasi.

AtomicReference mendukung pemrograman thread-safe bebas kunci pada variabel tunggal.

Ada beberapa cara untuk mencapai keselamatan Thread dengan API bersamaan tingkat tinggi . Variabel atom adalah salah satu dari beberapa opsi.

Lock objek mendukung idiom pengunci yang menyederhanakan banyak aplikasi bersamaan.

Executorsmendefinisikan API tingkat tinggi untuk meluncurkan dan mengelola utas. Implementasi pelaksana yang disediakan oleh java.util.concurrent menyediakan manajemen kumpulan thread yang cocok untuk aplikasi skala besar.

Koleksi bersamaan membuatnya lebih mudah untuk mengelola koleksi data yang besar, dan dapat sangat mengurangi kebutuhan sinkronisasi.

Variabel atom memiliki fitur yang meminimalkan sinkronisasi dan membantu menghindari kesalahan konsistensi memori.

Berikan contoh sederhana di mana AtomicReference harus digunakan.

Kode contoh dengan AtomicReference:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

Apakah diperlukan untuk membuat objek di semua program multithread?

Anda tidak harus menggunakan AtomicReferencesemua program multi-utas.

Jika Anda ingin menjaga satu variabel, gunakan AtomicReference. Jika Anda ingin menjaga blok kode, gunakan konstruksi lain seperti Lock/ synchronizeddll.

Ravindra babu
sumber
-1

Contoh sederhana lainnya adalah melakukan modifikasi safe-thread pada objek sesi.

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    } 
}

Sumber: http://www.ibm.com/developerworks/library/j-jtp09238/index.html

Dherik
sumber