Atomar əməliyyatların yaranmasının səbəbləri
Gəl bu nümunəni nəzərdən keçirək, bu atomar əməliyyatların işini başa düşməyə kömək edəcək:
public class Counter {
int count;
public void increment() {
count++;
}
}
Bir tək axınımız varsa, hər şey əladır, amma əgər çoxaxınlılıq əlavə etsək, yanlış nəticələr alırıq, çünki inkrement əməliyyatı tək bir əməliyyat deyil, üç əməliyyatdır: cari dəyərin alınması count, üzərinə 1 əlavə edilməsi və yenidən count yazılması.
Və iki axın eyni vaxtda dəyişəni artırmaq istədikdə, çox güman ki, məlumatları itirəcəksən. Yəni hər iki axın 100 alır və nəticədə hər ikisi 101 yazacaq, gözlənilən 102 əvəzinə.
Bunu necə həll etmək olar? Bloklamalardan istifadə etmək lazımdır. synchronized açar sözü bu problemi həll etməyə kömək edir, bu sözdən istifadə etmək sənə zəmanət verir ki, tək bir axın metoda bir anda müraciət edəcək.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
Üstəlik volatile açar sözünü də əlavə etmək lazımdır ki, o, dəyişkənlərin axınlar arasında düzgün görünüşünü təmin etsin. Onun işini artıq yuxarıda araşdırdıq.
Lakin yenə də mənfi cəhətlər var. Ən böyük mənfi cəhət - bu, performansdır, çünki çox axınlar bloklama əldə etmək istəyərkən və təkcə biri yazma imkanı əldə etdikdə, qalan axınlar ya bloklanacaq, ya da axın azad edilənə qədər dayandırılacaq.
Bütün bu proseslər, bloklama, başqa bir statusa keçid - sistemin performansı üçün çox bahalıdır.
Atomar əməliyyatlar
Alqoritm aşağı səviyyəli maşın təlimatlarını istifadə edir, məsələn, müqayisə və dəyişmə (CAS, compare-and-swap, hansı ki, məlumatların bütövlüyünü təmin edir və onlar üzərində artıq çox sayda tədqiqatlar mövcuddur).
Tipik CAS əməliyyatı üç operandla işləyir:
- Əməliyyat üçün yaddaş yeri (M)
- Var olan gözlənilən dəyər (A) dəyişənin
- Quraşdırılması lazım olan yeni dəyər (B)
CAS atomik olaraq M-ni B-yə yeniləyir, lakin yalnız M dəyəri A ilə uyğun gəlirsə, əks halda heç bir əməliyyat həyata keçirilmir.
Birinci və ikinci halda isə M dəyəri geri qaytarılır. Bu, üç addımı birləşdirməyə imkan verir: dəyərin alınması, dəyərin müqayisəsi və onun yenilənməsi. Və bu hamısı bir maşın səviyyəsində bir əməliyyata çevrilir.
O anda çox axınlı tətbiq dəyişənə müraciət edir və onu yeniləməyə çalışır və CAS tətbiq edilir, axınlardan biri onu əldə edib yeniləyə biləcək. Lakin bloklamalardan fərqli olaraq, digər axınlar sadəcə dəyərin yenilənə bilməməsi barədə xəbərdarlıq alacaq. Sonra isə onlar işlərinə davam edəcəklər və bu tip işdə keçid tamamilə xaric edilmişdir.
Belə vəziyyətin nəticəsində, CAS əməliyyatı uğurla yerinə yetirilməyən halda, biz bu vəziyyəti idarə etmək məcburiyyətindəyik. Kodumuzu sadəcə olaraq elə modelləşdirəcəyik ki, əməliyyat uğurla başa çatana qədər irəliləməsin.
Atomik tiplərlə tanışlıq
Sən sadə int tipli dəyişəni sinxronizasiya etmək lazım olduğu vəziyyətə qarşılaşmısan?
Artıq müzakirə etdiyimiz birinci yol – volatile + synchronized istifadə etməkdir. Lakin hələ də xüsusi Atomic* sinifləri var.

Əgər CAS istifadə olunursa, əməliyyatlar birinci yoldan daha sürətli işləyir. Və əlavə olaraq bizdə dəyərin artırılması üçün xüsusi və çox rahat metodlar var və inkrement və dekrement əməliyyatları da var.
AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray — bu siniflərdə əməliyyatlar atomardır. Aşağıda onların işini araşdıracağıq.
AtomicInteger
AtomicInteger sinifi int dəyəri ilə əməliyyatlar təmin edir ki, bunlar atomar olaraq oxunsun və yazılsın, əlavə olaraq inkişaf etmiş atomar əməliyyatlar da var.
Onda get və set metodları var, hansılar ki, dəyişənlərin oxunması və yazılması kimidir.
Yəni “bundan öncə baş verir (happens-before)” hər hansı sonrakı eyni dəyişənin alınması ilə, əvvəllər danışdığımız o məsələdən. Atomik metod compareAndSet də bu yaddaş uyğunluğu xüsusiyyətlərinə malikdir.
Yeni dəyər qaytaran bütün əməliyyatlar atomar olaraq yerinə yetirilir:
int addAndGet (int delta) | Cari dəyərə müəyyən bir dəyər əlavə edir. |
boolean compareAndSet (gözlənilən int, yeniləmə int) | Cari dəyər gözlənilən dəyərə uyğun gəlsə, verilmiş yenilənmiş dəyəri təyin edir. |
int decrementAndGet () | Cari dəyəri bir vahid azaldır. |
int getAndAdd (int delta) | Verilmiş dəyəri cari dəyərə əlavə edir. |
int getAndDecrement () | Cari dəyəri bir vahid azaldır. |
int getAndIncrement () | Cari dəyəri bir vahid artırır. |
int getAndSet (int newValue) | Verilmiş dəyəri təyin edir və köhnə dəyəri qaytarır. |
int incrementAndGet () | Cari dəyəri bir vahid artırır. |
lazySet (int newValue) | Nəticədə verilmiş dəyərə təyin edilir. |
boolean weakCompareAndSet (gözlənilən, yeniləmə int) | Cari dəyər gözlənilən dəyərə uyğun gəlsə, verilmiş yenilənmiş dəyəri təyin edir. |
Nümunə:
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()); // 50 çıxaracaq
GO TO FULL VERSION