Atomik işlemlerin ortaya çıkması için ön koşullar
Atomik işlemlerin nasıl çalıştığını anlamanıza yardımcı olması için bu örneğe bir göz atalım:
public class Counter {
int count;
public void increment() {
count++;
}
}
Bir iş parçacığımız olduğunda, her şey harika çalışıyor, ancak çoklu iş parçacığı eklersek yanlış sonuçlar alıyoruz ve bunun nedeni, artırma işleminin bir işlem değil, üç işlem olması: geçerli değeri alma isteğisaymak, ardından 1 artırın ve tekrar yazınsaymak.
Ve iki iş parçacığı bir değişkeni artırmak istediğinde, büyük ihtimalle veri kaybedersiniz. Yani, her iki iş parçacığı da 100 alır, sonuç olarak her ikisi de beklenen 102 değeri yerine 101 yazar.
Ve nasıl çözülür? Kilit kullanmanız gerekiyor. synchronized anahtar sözcüğü bu sorunu çözmeye yardımcı olur, bunu kullanmak size her seferinde bir iş parçacığının yönteme erişeceğinin garantisini verir.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
Ayrıca, başlıklar arasında referansların doğru şekilde görünmesini sağlayan volatile anahtar kelimesini eklemeniz gerekir. Çalışmalarını yukarıda inceledik.
Ama yine de olumsuzluklar var. En büyüğü performanstır, bu noktada birçok iş parçacığı bir kilit elde etmeye çalıştığında ve biri yazma fırsatı yakaladığında, iş parçacığı serbest bırakılana kadar iş parçacıklarının geri kalanı ya engellenir ya da askıya alınır.
Tüm bu işlemler, engelleme, başka bir duruma geçiş, sistem performansı için çok pahalıdır.
atomik işlemler
Algoritma, karşılaştır ve değiştir (CAS, karşılaştır ve değiştir, veri bütünlüğünü sağlayan ve bunlar hakkında halihazırda büyük miktarda araştırma bulunan) gibi düşük seviyeli makine komutlarını kullanır.
Tipik bir CAS işlemi üç işlenen üzerinde çalışır:
- İş için hafıza alanı (M)
- Bir değişkenin mevcut beklenen değeri (A)
- Ayarlanacak yeni değer (B)
CAS, M'yi B'ye atomik olarak günceller, ancak yalnızca M'nin değeri A ile aynıysa, aksi takdirde herhangi bir işlem yapılmaz.
Birinci ve ikinci durumda, M'nin değeri döndürülür.Bu, değeri alma, değeri karşılaştırma ve güncelleme olmak üzere üç adımı birleştirmenizi sağlar. Ve hepsi makine düzeyinde tek bir işleme dönüşür.
Çok iş parçacıklı bir uygulama bir değişkene eriştiğinde ve onu güncellemeye çalıştığında ve CAS uygulandığında, iş parçacıklarından biri onu alır ve güncelleyebilir. Ancak kilitlerden farklı olarak, diğer ileti dizileri, değeri güncelleyememe konusunda basitçe hatalar alacaktır. Daha sonra daha fazla çalışmaya geçecekler ve bu tür işlerde geçiş tamamen hariç tutulmuştur.
Bu durumda, CAS operasyonunun başarılı bir şekilde çalışmadığı durumu halletmemiz gerektiğinden mantık daha da zorlaşıyor. Kodu, işlem başarılı olana kadar ilerlemeyecek şekilde modelleyeceğiz.
Atom Türlerine Giriş
int türündeki en basit değişken için senkronizasyon ayarlamanız gereken bir durumla karşılaştınız mı ?
Halihazırda ele aldığımız ilk yol, volatile + synchronized kullanmaktır . Ancak özel Atom* sınıfları da vardır.
CAS kullanırsak, işlemler ilk yönteme göre daha hızlı çalışır. Ayrıca değer ekleme, artırma ve eksiltme işlemleri için özel ve çok uygun metotlarımız mevcuttur.
AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray , işlemlerin atomik olduğu sınıflardır. Aşağıda onlarla çalışmayı analiz edeceğiz.
atomikTamsayı
AtomicInteger sınıfı, genişletilmiş atomik işlemler sağlamanın yanı sıra atomik olarak okunabilen ve yazılabilen bir int değeri üzerinde işlemler sağlar .
Değişkenleri okuma ve yazma gibi çalışan get ve set yöntemleri vardır .
Yani, daha önce bahsettiğimiz aynı değişkenin herhangi bir müteakip alımından "önce-olur". Atomik CompareAndSet yöntemi de bu bellek tutarlılığı özelliklerine sahiptir.
Yeni bir değer döndüren tüm işlemler atomik olarak gerçekleştirilir:
int addAndGet (int delta) | Geçerli değere belirli bir değer ekler. |
boolean CompareAndSet(beklenen int, güncelleme int) | Geçerli değer beklenen değerle eşleşiyorsa, değeri verilen güncellenmiş değere ayarlar. |
int azaltmaAndGet() | Geçerli değeri bir azaltır. |
int getAndAdd(int delta) | Verilen değeri mevcut değere ekler. |
int getAndDecrement() | Geçerli değeri bir azaltır. |
int getAndIncrement() | Geçerli değeri bir artırır. |
int getAndSet(int yeniDeğer) | Verilen değeri ayarlar ve eski değeri döndürür. |
int artışAndGet() | Geçerli değeri bir artırır. |
tembelKüme(int yeniDeğer) | Sonunda verilen değere ayarlayın. |
boolean zayıfCompareAndSet(beklenen, güncelleme int) | Geçerli değer beklenen değerle eşleşiyorsa, değeri verilen güncellenmiş değere ayarlar. |
Örnek:
ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
System.out.println(atomicInteger.get()); // prints 50
GO TO FULL VERSION