İ ++ neden atomik değildir?


97

i++Java'da neden atomik değil?

Java'da biraz daha derine inmek için iş parçacıklarındaki döngünün ne sıklıkla yürütüldüğünü saymaya çalıştım.

Ben de kullandım

private static int total = 0;

ana sınıfta.

İki iş parçam var.

  • Konu 1: Baskılar System.out.println("Hello from Thread 1!");
  • Konu 2: Baskılar System.out.println("Hello from Thread 2!");

Ve iplik 1 ve iplik 2 tarafından yazdırılan satırları sayıyorum. Ancak iplik 1'in satırları + iplik 2'nin satırları, yazdırılan toplam satır sayısıyla eşleşmiyor.

İşte kodum:

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();
    }
}

14
Neden birlikte denemiyorsun AtomicInteger?
Braj


3
JVM, iinctam sayıları artırmak için bir işleme sahiptir , ancak bu yalnızca eşzamanlılığın bir sorun olmadığı yerel değişkenler için çalışır. Alanlar için derleyici, okuma-değiştirme-yazma komutlarını ayrı ayrı oluşturur.
Silly Freak

14
Neden atomik olmasını bekliyorsunuz?
Sıcak Licks

2
@Silly Freak: iincalanlar için bir talimat olsa bile , tek bir talimata sahip olmak atomikliği garanti etmez, örneğin tek bir bayt kodu talimatı tarafından gerçekleştirildiği gerçeğine bakılmaksızın atomik olma garantisi olmayan volatile longve doublealan erişiminin garantisi yoktur.
Holger

Yanıtlar:


125

i++muhtemelen Java'da atomik değildir çünkü atomiklik, kullanımlarının çoğunda bulunmayan özel bir gerekliliktir i++. Bu gerekliliğin önemli bir ek yükü vardır: artırma işlemini atomik yapmanın büyük bir maliyeti vardır; sıradan bir artışta bulunması gerekmeyen hem yazılım hem de donanım seviyelerinde senkronizasyonu içerir.

i++Atomik olmayan bir artış kullanılarak gerçekleştirilecek şekilde, özellikle bir atomik artış gerçekleştirecek şekilde tasarlanmış ve belgelenmiş olması gereken argümanı yapabilirsiniz i = i + 1. Ancak bu, Java ile C ve C ++ arasındaki "kültürel uyumluluğu" bozacaktır. Ayrıca, C benzeri dillere aşina olan programcıların hafife aldığı uygun bir notasyonu ortadan kaldırarak, ona yalnızca sınırlı durumlarda geçerli olan özel bir anlam verir.

Temel C veya C ++ kodu for (i = 0; i < LIMIT; i++), Java'ya şu şekilde çevrilir for (i = 0; i < LIMIT; i = i + 1); çünkü atomik kullanmak uygunsuz olurdu i++. Daha da kötüsü, C veya diğer C benzeri dillerden Java'ya gelen programcılar i++yine de kullanır ve bu da atomik talimatların gereksiz kullanımına neden olur.

Makine komut seti seviyesinde bile, bir artış tipi işlem genellikle performans nedenlerinden dolayı atomik değildir. X86'da, incyönergeyi atomik yapmak için özel bir talimat "kilit öneki" kullanılmalıdır : yukarıdaki ile aynı nedenlerle. Her inczaman atomik olsaydı, atomik olmayan bir inc gerekli olduğunda asla kullanılmazdı; programcılar ve derleyiciler yükleyen, 1 ekleyen ve depolayan kod üretir, çünkü çok daha hızlı olur.

Bazı komut seti mimarilerinde atom yoktur incveya belki hiç yoktur inc; MIPS üzerinde atomik bir araştırma yapmak için, lland sc: load-linked ve store-conditional kullanan bir yazılım döngüsü yazmanız gerekir . Yük bağlantılı kelimeyi okur ve depo koşullu, kelime değişmediyse yeni değeri saklar veya aksi takdirde başarısız olur (bu algılanır ve yeniden denemeye neden olur).


2
Java'nın hiçbir işaretçisi olmadığı için, yerel değişkenlerin artırılması, doğası gereği iş parçacığı tasarrufudur, bu nedenle döngülerle sorun çoğunlukla o kadar da kötü olmaz. En az sürprizle ilgili fikriniz elbette geçerli. ayrıca, olduğu gibi, i = i + 1bunun bir çevirisi ++ideğili++
Silly Freak

22
Sorunun ilk kelimesi "neden" dir. Şu an itibariyle, "neden" konusunu ele almanın tek cevabı bu. Diğer cevaplar gerçekten soruyu yeniden ifade ediyor. Yani +1.
Dawood ibn Kareem

3
Bir atomiklik garantisinin, alan olmayanların güncellemeleri için görünürlük sorununu çözmeyeceğini belirtmekte fayda var volatile. Bu nedenle, her alanı volatilebir iş parçacığı üzerinde ++işleci kullandığında örtük olarak ele almadığınız sürece, böyle bir atomiklik garantisi eşzamanlı güncelleme sorunlarını çözmeyecektir. Öyleyse, problemi çözmüyorsa neden potansiyel olarak bir şey için performansı boşa harcamak?
Holger

1
@DavidWallace değil mi ++? ;)
Dan Hlavenka

36

i++ iki işlemi içerir:

  1. şu anki değerini oku i
  2. değeri artırın ve atayın i

İki iş parçacığı i++aynı anda aynı değişken üzerinde performans gösterdiğinde, ikisi de aynı akım değerini alabilir ive sonra artırabilir ve olarak ayarlayabilir i+1, böylece iki yerine tek bir artış elde edersiniz.

Misal :

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

(Bile i++ oldu atom, bu iyi tanımlanmış olmaz / evreli davranışlar.)
user2864740

15
+1, ancak "1. A, 2. B ve C" kulağa iki değil, üç işlem gibi geliyor. :)
yshavit

3
İşlem, yerinde bir depolama konumunu artıran tek bir makine talimatı ile uygulanmış olsa bile, iş parçacığı açısından güvenli olacağının hiçbir garantisi yoktur. Makinenin yine de değeri alması, artırması ve geri kaydetmesi gerekir, ayrıca bu depolama yerinin birden çok önbellek kopyası olabilir.
Sıcak Licks

3
@Aquarelle - Eğer iki işlemci aynı işlemi aynı depolama konumunda aynı anda yürütürse ve konumda "yedek" yayın yoksa, neredeyse kesinlikle müdahale edecek ve sahte sonuçlar üretecektir. Evet, bu işlemin "güvenli" olması mümkündür, ancak donanım düzeyinde bile özel çaba gerektirir.
Hot Licks

6
Ama bence soru "Ne oluyor" değil "Neden" idi.
Sebastian Mach

11

Önemli olan, JVM'nin çeşitli uygulamalarının dilin belirli bir özelliğini nasıl uygulayıp uygulamadığından ziyade JLS'dir (Java Dil Belirtimi). JLS, 15.14.2 maddesinde "1 değeri değişkenin değerine eklenir ve toplam değişkene geri depolanır" diyen ++ sonek operatörünü tanımlar. Hiçbir yerde multithreading veya atomisiteden bahsetmez veya ima etmez. Bunlar için JLS uçucu ve eşzamanlı sağlar . Ek olarak, java.util.concurrent.atomic paketi vardır (bkz. Http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )


5

Java'da i ++ neden atomik değildir?

Arttırma işlemini birden çok ifadeye ayıralım:

Konu 1 ve 2:

  1. Hafızadan toplamın değerini al
  2. Değere 1 ekleyin
  3. Hafızaya geri yaz

Senkronizasyon yoksa, diyelim ki Thread one 3 değerini okudu ve 4'e yükseltti, ancak geri yazmadı. Bu noktada bağlam değişikliği gerçekleşir. İkinci iş parçacığı 3 değerini okur, artırır ve bağlam anahtarı gerçekleşir. Her iki iş parçacığı da toplam değeri artırmış olsa da, yine de 4 yarış koşulu olacaktır.


2
Bunun soruya nasıl bir cevap olması gerektiğini anlamıyorum. Bir dil, herhangi bir özelliği atomik olarak tanımlayabilir, ister artımlı ister tek boynuzlu olsun. Sadece atomik olmamanın bir sonucunu örneklendiriyorsunuz.
Sebastian Mach

Evet, bir dil herhangi bir özelliği atomik olarak tanımlayabilir, ancak java artım operatörü olarak kabul edildiğinde (OP tarafından gönderilen soru) atomik değildir ve cevabım nedenleri belirtir.
Aniket Thakur

1
(İlk yorumdaki sert tonum için özür dilerim) Ama o zaman neden "çünkü atomik olursa, yarış koşulları olmazdı" gibi görünüyor. Yani, bir yarış durumu isteniyormuş gibi geliyor.
Sebastian Mach

@phresnel atomik bir artışı korumak için eklenen ek yük çok büyüktür ve nadiren istenir, bu da operasyonu ucuz tutar ve sonuç olarak atomik olmayan çoğu zaman arzu edilir.
josefx

4
@josefx: Gerçekleri değil, bu cevabın gerekçesini sorguladığımı unutmayın. Temelde "i ++ sahip olduğu yarış koşulları nedeniyle Java'da atomik değildir" diyor , bu "meydana gelebilecek çarpışmalardan dolayı bir arabada hava yastığı yok" veya "currywurst siparişinizle bıçak almazsınız çünkü wurst'un kesilmesi gerekebilir " . Dolayısıyla bunun bir cevap olduğunu düşünmüyorum. Soru "i ++ ne yapar?" Değildi. veya "i ++ 'nın senkronize edilmemesinin sonucu nedir?" .
Sebastian Mach

5

i++ basitçe 3 işlemi içeren bir ifadedir:

  1. Mevcut değeri oku
  2. Yeni değer yazın
  3. Yeni değeri saklayın

Bu üç işlemin tek bir adımda yürütülmesi amaçlanmamıştır veya başka bir deyişle i++, bileşik bir işlem değildir . Sonuç olarak, bileşik olmayan tek bir işlemde birden fazla iş parçacığı yer aldığında her türlü şey ters gidebilir.

Aşağıdaki senaryoyu düşünün:

Zaman 1 :

Thread A fetches i
Thread B fetches i

2. Zaman :

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)

Zaman 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.

İşte buyur. Bir yarış durumu.


Bu yüzden i++atomik değil. Öyle olsaydı, bunların hiçbiri olmazdı ve her biri fetch-update-storeatomik olarak olurdu. Tam olarak AtomicIntegerbunun için ve sizin durumunuzda muhtemelen tam olarak uyacaktır.

PS

Tüm bu konuları kapsayan mükemmel bir kitap ve sonra bazıları şudur: Uygulamada Java Eş Zamanlılığı


1
Hmm. Bir dil, herhangi bir özelliği atomik olarak tanımlayabilir, ister artımlı ister tek boynuzlu olsun. Sadece atomik olmamanın bir sonucunu örneklendiriyorsunuz.
Sebastian Mach

@phresnel Kesinlikle. Ama aynı zamanda bunun tek bir işlem olmadığına da işaret ediyorum, uzantı olarak bu tür birden çok işlemi atomik olanlara dönüştürmenin hesaplama maliyetinin çok daha pahalı olduğunu ve dolayısıyla da-kısmen- neden i++atomik olmadığını haklı çıkarıyor .
kstratis

1
Ne demek istediğini anlarken, cevabın öğrenme konusunda biraz kafa karıştırıcı. Bir örnek ve "örnekteki durum nedeniyle" diyen bir sonuç görüyorum; imho bu eksik bir muhakemedir :(
Sebastian Mach

1
@phresnel Belki en pedagojik cevap değil ama şu anda sunabileceğim en iyisi. Umarım insanlara yardımcı olur ve kafalarını karıştırmaz. Ancak eleştirileriniz için teşekkürler. Gelecekteki gönderilerimde daha kesin olmaya çalışacağım.
kstratis

2

JVM'de artış, okuma ve yazma içerir, bu nedenle atomik değildir.


2

İşlem i++atomik olsaydı, ondan değeri okuma şansınız olmazdı. Bu tam olarak kullanmak istediğiniz şeydir i++(kullanmak yerine ++i).

Örneğin aşağıdaki koda bakın:

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

Bu durumda çıktının şu olmasını bekleriz: 0 (çünkü artış gönderiyoruz, örneğin önce oku, sonra güncelle)

Bu, işlemin atomik olamamasının nedenlerinden biridir, çünkü değeri okumanız (ve onunla bir şeyler yapmanız) ve ardından değeri güncellemeniz gerekir.

Diğer önemli neden ise, atomik olarak bir şeyler yapmanın genellikle kilitlenme nedeniyle daha fazla zaman almasıdır. İnsanların atomik operasyonlar yaptırmak istediği ender durumlarda, ilkeller üzerindeki tüm işlemlerin biraz daha uzun sürmesi aptalca olurdu. Onlar ekledik nedeni budur AtomicIntegerve diğer dile atomik sınıflar.


2
Bu yanıltıcıdır. Yürütmeyi ayırıp sonucu almalısınız, aksi takdirde herhangi bir atomik işlemden değer alamazsınız .
Sebastian Mach

Hayır değil, bu yüzden Java'nın AtomicInteger'ında get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decmentAndGet () vb. Var.
Roy van Rijn

1
Ve Java dilinin i++genişletilmesi için tanımlanabilirdi i.getAndIncrement(). Böyle bir genişleme yeni değil. Örneğin, C ++ 'daki lambdalar, C ++' da anonim sınıf tanımlarına genişletilir.
Sebastian Mach

Bir atom verildiğinde i++, önemsiz bir şekilde bir atom yaratabilir ++iveya bunun tersi de geçerlidir. Biri diğerine eşit artı bir.
David Schwartz

2

İki adım vardır:

  1. hafızadan getir
  2. i + 1'i i'ye ayarla

bu yüzden atomik işlem değil. Evre1 i ++ 'yı çalıştırdığında ve evre2 i ++' yı çalıştırdığında, i'nin son değeri i + 1 olabilir.


-1

Eşzamanlılık ( Threadsınıf ve benzeri) Java'nın v1.0 sürümüne eklenen bir özelliktir . i++ondan önce betaya eklenmişti ve bu nedenle (aşağı yukarı) orijinal uygulamasında büyük olasılıkla hala daha fazla.

Değişkenleri senkronize etmek programcıya kalmıştır. Oracle'ın bu konudaki eğitimine göz atın .

Düzenleme: Açıklamak gerekirse, i ++ Java'dan önce gelen iyi tanımlanmış bir prosedürdür ve bu nedenle Java tasarımcıları bu prosedürün orijinal işlevselliğini korumaya karar verdiler.

++ operatörü, java ve iş parçacığı oluşturmadan biraz önce gelen B (1969) 'da tanımlanmıştır.


-1 "public class Thread ... Şu tarihten beri: JDK1.0
Silly Freak

Versiyon, Thread sınıfından önce hala uygulandığı ve bu yüzden değiştirilmediği gerçeği kadar önemli değil, ancak cevabımı sizi memnun etmek için düzenledim.
TheBat

5
Önemli olan, "Thread sınıfından önce hala uygulanıyor" iddianızın kaynaklar tarafından desteklenmemesidir. i++atomik olmamak bir tasarım kararıdır, büyüyen bir sistemde bir gözetim değil.
Aptal Freak

Lol bu şirin. i ++, Threads'ten çok önce tanımlanmıştı, çünkü Java'dan önce var olan diller vardı. Java'nın yaratıcıları, kabul gören bir prosedürü yeniden tanımlamak yerine bu diğer dilleri temel olarak kullandılar. Bunun bir gözetim olduğunu nerede söylemiştim?
TheBat

@SillyFreak İşte ++ 'nın ne kadar eski olduğunu gösteren bazı kaynaklar: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.