Mengapa i ++ tidak atom?

97

Mengapa i++tidak atom di Jawa?

Untuk lebih memahami Java, saya mencoba menghitung seberapa sering loop di thread dijalankan.

Jadi saya menggunakan file

private static int total = 0;

di kelas utama.

Saya memiliki dua utas.

  • Thread 1: Cetakan System.out.println("Hello from Thread 1!");
  • Benang 2: Cetakan System.out.println("Hello from Thread 2!");

Dan saya menghitung garis yang dicetak oleh utas 1 dan utas 2. Tetapi garis utas 1 + garis utas 2 tidak cocok dengan jumlah total garis yang dicetak.

Ini kode saya:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Test {

    private static int total = 0;
    private static int countT1 = 0;
    private static int countT2 = 0;
    private boolean run = true;

    public Test() {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        newCachedThreadPool.execute(t1);
        newCachedThreadPool.execute(t2);
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        run = false;
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println((countT1 + countT2 + " == " + total));
    }

    private Runnable t1 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT1++;
                System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    private Runnable t2 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT2++;
                System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    public static void main(String[] args) {
        new Test();
    }
}
Andie2302
sumber
14
Mengapa Anda tidak mencobanya AtomicInteger?
Braj
3
JVM memiliki iincoperasi untuk menambah bilangan bulat, tetapi itu hanya berfungsi untuk variabel lokal, di mana konkurensi tidak menjadi perhatian. Untuk bidang, kompilator membuat perintah baca-ubah-tulis secara terpisah.
Silly Freak
14
Mengapa Anda berharap itu menjadi atom?
Hot Licks
2
@Silly Freak: meskipun ada iincinstruksi untuk bidang, memiliki instruksi tunggal tidak menjamin atomisitas, misalnya akses non- volatile longdan doublebidang tidak dijamin menjadi atom terlepas dari fakta bahwa itu dilakukan oleh instruksi bytecode tunggal.
Holger

Jawaban:

125

i++mungkin tidak bersifat atomik di Jawa karena atomisitas merupakan persyaratan khusus yang tidak terdapat dalam sebagian besar penggunaan i++. Persyaratan tersebut memiliki overhead yang signifikan: ada biaya yang besar dalam membuat operasi selisih atom; ini melibatkan sinkronisasi pada tingkat perangkat lunak dan perangkat keras yang tidak perlu dilakukan secara bertahap.

Anda bisa membuat argumen yang i++seharusnya dirancang dan didokumentasikan secara spesifik melakukan kenaikan atom, sehingga kenaikan non-atomik dilakukan dengan menggunakan i = i + 1. Namun, ini akan merusak "kompatibilitas budaya" antara Java, dan C dan C ++. Selain itu, ini akan menghilangkan notasi yang nyaman yang dianggap biasa oleh programmer yang akrab dengan bahasa C-like, memberinya makna khusus yang hanya berlaku dalam keadaan terbatas.

Kode dasar C atau C ++ for (i = 0; i < LIMIT; i++)akan diterjemahkan ke dalam Java sebagai for (i = 0; i < LIMIT; i = i + 1); karena tidak pantas menggunakan atom i++. Yang lebih buruk, pemrogram yang berasal dari C atau bahasa mirip C lainnya ke Java akan i++tetap menggunakannya , mengakibatkan penggunaan instruksi atomik yang tidak perlu.

Bahkan pada level set instruksi mesin, operasi tipe increment biasanya tidak atomic karena alasan kinerja. Dalam x86, instruksi khusus "kunci awalan" harus digunakan untuk membuat incinstruksi atom: untuk alasan yang sama seperti di atas. Jika incselalu atom, itu tidak akan pernah digunakan ketika inc non-atom diperlukan; programmer dan compiler akan menghasilkan kode yang memuat, menambahkan 1 dan menyimpan, karena itu akan jauh lebih cepat.

Dalam beberapa arsitektur set instruksi, tidak ada atom incatau mungkin tidak ada incsama sekali; untuk melakukan atomic inc di MIPS, Anda harus menulis loop perangkat lunak yang menggunakan lland sc: load-linked, dan store-conditional. Load-linked membaca kata tersebut, dan store-conditional menyimpan nilai baru jika kata tersebut tidak berubah, atau gagal (yang terdeteksi dan menyebabkan percobaan ulang).

Kaz
sumber
2
karena java tidak memiliki petunjuk, menaikkan variabel lokal secara inheren menyimpan benang, jadi dengan loop, sebagian besar masalahnya tidak akan terlalu buruk. poin Anda tentang kejutan yang paling tidak mengejutkan, tentu saja. juga, sebagaimana adanya, i = i + 1akan menjadi terjemahan untuk ++i, bukani++
Silly Freak
22
Kata pertama dari pertanyaan itu adalah "mengapa". Saat ini, ini adalah satu-satunya jawaban untuk menjawab masalah "mengapa". Jawaban yang lain benar-benar hanya menyatakan kembali pertanyaannya. Jadi +1.
Dawood ibn Kareem
3
Mungkin perlu dicatat bahwa jaminan atomicity tidak akan menyelesaikan masalah visibilitas untuk pembaruan non- volatilebidang. Jadi, kecuali Anda akan memperlakukan setiap bidang secara implisit volatilesetelah satu utas menggunakan ++operator di atasnya, jaminan atomisitas seperti itu tidak akan menyelesaikan masalah pembaruan serentak. Jadi mengapa berpotensi membuang-buang kinerja untuk sesuatu jika itu tidak menyelesaikan masalah.
Holger
1
@Davidace bukan maksudmu ++? ;)
Dan Hlavenka
36

i++ melibatkan dua operasi:

  1. membaca nilai saat ini dari i
  2. menambah nilai dan menetapkannya ke i

Saat dua utas bekerja i++pada variabel yang sama pada saat yang sama, keduanya mungkin mendapatkan nilai saat ini yang sama i, lalu menaikkan dan menyetelnya ke i+1, jadi Anda akan mendapatkan satu inkrementasi, bukan dua.

Contoh:

int i = 5;
Thread 1 : i++;
           // reads value 5
Thread 2 : i++;
           // reads value 5
Thread 1 : // increments i to 6
Thread 2 : // increments i to 6
           // i == 6 instead of 7
Eran
sumber
(Sekalipun i++ bersifat atom, itu tidak akan didefinisikan dengan baik / perilaku aman utas.)
pengguna2864740
15
+1, tetapi "1. A, 2. B dan C" terdengar seperti tiga operasi, bukan dua. :)
yshavit
3
Perhatikan bahwa meskipun operasi tersebut diimplementasikan dengan satu instruksi mesin yang meningkatkan lokasi penyimpanan, tidak ada jaminan bahwa itu akan aman untuk thread. Mesin masih perlu mengambil nilai, menaikkannya, dan menyimpannya kembali, ditambah lagi mungkin ada beberapa salinan cache dari lokasi penyimpanan itu.
Hot Licks
3
@Aquarelle - Jika dua prosesor menjalankan operasi yang sama pada lokasi penyimpanan yang sama secara bersamaan, dan tidak ada siaran "cadangan" di lokasi tersebut, maka hampir pasti mereka akan mengganggu dan menghasilkan hasil yang palsu. Ya, operasi ini mungkin saja "aman", tetapi membutuhkan upaya khusus, bahkan pada tingkat perangkat keras.
Hot Licks
6
Tapi saya pikir pertanyaannya adalah "Mengapa" dan bukan "Apa yang terjadi".
Sebastian Mach
11

Yang penting adalah JLS (Spesifikasi Bahasa Java) daripada bagaimana berbagai implementasi JVM mungkin atau mungkin tidak mengimplementasikan fitur bahasa tertentu. JLS mendefinisikan operator ++ postfix dalam klausul 15.14.2 yang mengatakan ia "nilai 1 ditambahkan ke nilai variabel dan jumlahnya disimpan kembali ke dalam variabel". Tidak ada yang menyebutkan atau mengisyaratkan multithreading atau atomicity. Untuk ini, JLS menyediakan volatile dan sinkronisasi . Selain itu, ada paket java.util.concurrent.atomic (lihat http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )

Jonathan Rosenne
sumber
5

Mengapa i ++ tidak atom di Java?

Mari kita pecahkan operasi increment menjadi beberapa pernyataan:

Benang 1 & 2:

  1. Ambil nilai total dari memori
  2. Tambahkan 1 ke nilai
  3. Tulis kembali ke memori

Jika tidak ada sinkronisasi, katakanlah Thread satu telah membaca nilai 3 dan menaikkannya menjadi 4, tetapi belum menuliskannya kembali. Pada titik ini, pengalih konteks terjadi. Utas dua membaca nilai 3, menambahnya dan sakelar konteks terjadi. Meskipun kedua utas telah menambah nilai total, itu masih akan menjadi 4 - kondisi balapan.

Aniket Thakur
sumber
2
Saya tidak mengerti bagaimana seharusnya ini menjadi jawaban atas pertanyaan itu. Sebuah bahasa dapat mendefinisikan fitur apa pun sebagai atom, baik itu increment atau unicorn. Anda hanya mencontohkan konsekuensi tidak atom.
Sebastian Mach
Ya, suatu bahasa dapat mendefinisikan fitur apa pun sebagai atom tetapi sejauh java dianggap operator kenaikan (yang merupakan pertanyaan yang diposting oleh OP) tidak atom dan jawaban saya menyatakan alasannya.
Aniket Thakur
1
(maaf untuk nada kasar saya di komentar pertama) Tapi kemudian, alasannya sepertinya "karena jika itu atom, maka tidak akan ada kondisi balapan". Yaitu, kedengarannya seperti kondisi balapan yang diinginkan.
Sebastian Mach
@phresnel overhead yang diperkenalkan untuk menjaga atom tambahan sangat besar dan jarang diinginkan, menjaga operasi tetap murah dan sebagai hasilnya non atomik diinginkan sebagian besar waktu.
josefx
4
@josefx: Perhatikan bahwa saya tidak mempertanyakan fakta, tetapi alasan dalam jawaban ini. Pada dasarnya dikatakan "i ++ tidak atomic di Jawa karena kondisi balapan yang dimilikinya" , yang seperti mengatakan "mobil tidak memiliki kantung udara karena tabrakan yang dapat terjadi" atau "Anda tidak mendapatkan pisau dengan pesanan currywurst Anda karena wurst mungkin perlu dipotong " . Jadi, saya rasa ini bukan jawaban. Pertanyaannya bukanlah "Apa yang dilakukan i ++?" atau "Apa konsekuensi dari i ++ tidak disinkronkan?" .
Sebastian Mach
5

i++ adalah pernyataan yang hanya melibatkan 3 operasi:

  1. Baca nilai saat ini
  2. Tulis nilai baru
  3. Simpan nilai baru

Ketiga operasi ini tidak dimaksudkan untuk dijalankan dalam satu langkah atau dengan kata lain i++bukan operasi gabungan . Akibatnya, segala macam hal bisa salah jika lebih dari satu utas terlibat dalam operasi tunggal tetapi non-gabungan.

Pertimbangkan skenario berikut ini:

Waktu 1 :

Thread A fetches i
Thread B fetches i

Waktu 2 :

Thread A overwrites i with a new value say -foo-
Thread B overwrites i with a new value say -bar-
Thread B stores -bar- in i

// At this time thread B seems to be more 'active'. Not only does it overwrite 
// its local copy of i but also makes it in time to store -bar- back to 
// 'main' memory (i)

Waktu 3 :

Thread A attempts to store -foo- in memory effectively overwriting the -bar- 
value (in i) which was just stored by thread B in Time 2.

Thread B has nothing to do here. Its work was done by Time 2. However it was 
all for nothing as -bar- was eventually overwritten by another thread.

Dan begitulah. Kondisi balapan.


Itu sebabnya i++tidak atom. Jika ya, semua ini tidak akan terjadi dan masing-masing fetch-update-storeakan terjadi secara atomik. Itulah tepatnya AtomicIntegeruntuk dan dalam kasus Anda itu mungkin cocok.

PS

Sebuah buku yang sangat bagus yang mencakup semua masalah itu dan beberapa di antaranya adalah ini: Java Concurrency in Practice

kstratis
sumber
1
Hmm. Sebuah bahasa dapat mendefinisikan fitur apa pun sebagai atom, baik itu increment atau unicorn. Anda hanya mencontohkan konsekuensi tidak atom.
Sebastian Mach
@resnel Persis. Tetapi saya juga menunjukkan bahwa ini bukan operasi tunggal yang dengan perluasan menyiratkan bahwa biaya komputasi untuk mengubah beberapa operasi semacam itu menjadi operasi atom jauh lebih mahal yang pada gilirannya -pihak- membenarkan mengapa i++tidak atomik.
kstratis
1
Sementara saya mengerti maksud Anda, jawaban Anda agak membingungkan untuk pembelajaran. Saya melihat contoh, dan kesimpulan yang mengatakan "karena situasi dalam contoh"; imho ini adalah alasan yang tidak lengkap :(
Sebastian Mach
1
@phresnel Mungkin bukan jawaban yang paling pedagogis tapi ini yang terbaik yang bisa saya tawarkan saat ini. Semoga bisa membantu orang dan tidak membingungkan mereka. Namun, terima kasih atas kritik. Saya akan mencoba lebih tepat dalam posting saya selanjutnya.
kstratis
2

Dalam JVM, kenaikan melibatkan membaca dan menulis, jadi ini bukan atom.

celeritas
sumber
2

Jika operasi ini i++akan menjadi atom Anda tidak akan memiliki kesempatan untuk membaca nilai darinya. Inilah yang ingin Anda lakukan dengan menggunakan i++(daripada menggunakan ++i).

Misalnya lihat kode berikut:

public static void main(final String[] args) {
    int i = 0;
    System.out.println(i++);
}

Dalam hal ini kami mengharapkan outputnya menjadi: 0 (karena kami memposting kenaikan, misalnya membaca pertama, kemudian memperbarui)

Ini adalah salah satu alasan mengapa operasi tidak bisa atomic, karena Anda perlu membaca nilai (dan melakukan sesuatu dengannya) dan kemudian memperbarui nilainya.

Alasan penting lainnya adalah melakukan sesuatu secara atomik biasanya membutuhkan lebih banyak waktu karena penguncian. Akan konyol jika semua operasi pada primitif membutuhkan waktu sedikit lebih lama untuk kasus yang jarang terjadi ketika orang ingin melakukan operasi atom. Itulah mengapa mereka telah menambahkan AtomicIntegerdan kelas atom lainnya ke bahasa tersebut.

Roy van Rijn
sumber
2
Ini menyesatkan. Anda harus memisahkan eksekusi dan mendapatkan hasilnya, jika tidak, Anda tidak bisa mendapatkan nilai dari operasi atom apa pun .
Sebastian Mach
Tidak, itulah mengapa AtomicInteger Java memiliki get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () dll.
Roy van Rijn
1
Dan bahasa Jawa bisa didefinisikan i++untuk diperluas ke i.getAndIncrement(). Perluasan seperti itu bukanlah hal baru. Misalnya, lambda di C ++ diperluas ke definisi kelas anonim di C ++.
Sebastian Mach
Mengingat atom, i++seseorang dapat dengan mudah membuat atom ++iatau sebaliknya. Yang satu setara dengan yang lainnya plus satu.
David Schwartz
2

Ada dua langkah:

  1. ambil saya dari memori
  2. setel i + 1 ke i

jadi ini bukan operasi atom. Ketika thread1 menjalankan i ++, dan thread2 menjalankan i ++, nilai akhir dari i mungkin adalah i + 1.

yanghaogn
sumber
-1

Concurrency ( Threadkelas dan semacamnya) adalah fitur tambahan di v1.0 Java . i++telah ditambahkan dalam versi beta sebelumnya, dan karena itu, kemungkinan besar masih dalam penerapan aslinya (kurang lebih).

Terserah programmer untuk menyinkronkan variabel. Lihat tutorial Oracle tentang ini .

Sunting: Untuk memperjelas, i ++ adalah prosedur yang terdefinisi dengan baik yang mendahului Java, dan karena itu perancang Java memutuskan untuk mempertahankan fungsionalitas asli dari prosedur itu.

Operator ++ didefinisikan dalam B (1969) yang mendahului java dan threading hanya oleh sedikit.

Kelelawar
sumber
-1 " utas
Silly Freak
Versi tidak terlalu penting karena faktanya itu masih diterapkan sebelum kelas Thread dan tidak diubah karenanya, tetapi saya telah mengedit jawaban saya untuk menyenangkan Anda.
TheBat
5
Yang penting adalah bahwa klaim Anda "itu masih diterapkan sebelum kelas Thread" tidak didukung oleh sumber. i++tidak menjadi atom adalah keputusan desain, bukan pengawasan dalam sistem yang berkembang.
Silly Freak
Lol itu lucu. i ++ didefinisikan jauh sebelum Threads, hanya karena ada bahasa yang sudah ada sebelum Java. Pencipta Java menggunakan bahasa-bahasa lain itu sebagai dasar daripada mendefinisikan ulang prosedur yang diterima dengan baik. Di mana saya pernah mengatakan itu adalah kekeliruan?
TheBat
@SillyFreak Berikut beberapa sumber yang menunjukkan berapa umur ++: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat