Atomar operatsiyalar kelib chiqishiga sabablar
Keling, atomar operatsiyalar qanday ishlashini tushunishga yordam beradigan quyidagi misolni ko'rib chiqamiz:
public class Counter {
int count;
public void increment() {
count++;
}
}
Agar bizda bitta oqim bo'lsa, hammasi yaxshi ishlaydi, lekin biz ko'p oqimli ishlashni boshlagach, noto'g'ri natijalarni olishimiz mumkin, chunki inkrement operatsiyasi uchta operatsiyadan iborat: countning joriy qiymatini olish, uni 1 ga oshirish va yana countga qayd etish.
Ikki oqim bir vaqtning o'zida o'zgaruvchini oshirishni xohlasa, ehtimol siz ma'lumotlarni yo'qotasiz. Masalan, ikkala oqim ham 100 oladi, va natijada ikkala 101 yozadi, kutilgan 102 o'rniga.
Buni qanday hal qilish mumkin? Blokirovkalarni ishlatishimiz kerak. synchronized kalit so'zi ushbu muammoni hal qilishga yordam beradi, bu kalit so'zni ishlatish sizga bir vaqtning o'zida faqat bitta oqim metodga kirishiga kafolat beradi.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
public int getCount() { // blokirovkasiz o'qiymiz
return count;
}
}
synchronized inkrementni atomar qiladi: ikki oqim count++ amalini bir vaqtda bajara olmaydi. volatile esa bu yerda getCount() metodi tufayli kerak: u maydonni blokirovkasiz o'qiydi va volatile bo'lmasa, boshqa oqim eskirgan qiymatni ko'rishi mumkin edi. Agar getCount() metodi ham synchronized bo'lganida, volatile kerak bo'lmas edi.
Lekin kamchiliklar bor. Eng kattasi — ma'lumot uzatishda qiyinchiliklar. Ko'p oqimlar blokirovkaga olishga harakat qilayotganida va bir oqim yozishga imkoniyat olganida, boshqa oqimlar blokirovka qilinadi yoki to'xtatiladi, qo'yib yuborilgunga qadar.
Barcha bu jarayonlar, blokirovka, boshqa holatga o'tish — tizim ishlab chiqarish qobiliyati uchun juda qimmat.
Atomar operatsiyalar
Algoritmlar past darajadagi mashina ko'rsatmalaridan foydalanadi, masalan, taqqoslash va almashtirish (CAS, compare-and-swap, bu ma'lumotlar yaxlitligini ta'minlaydi va bu borada ko'plab tadqiqotlar mavjud).
Odatda CAS operatsiyasi uchta operandni ishlatadi:
- Ish uchun xotira joyi (M)
- O'zgarmasdagi mavjud kutilgan qiymat (A)
- O'rnatilishi kerak bo'lgan yangi qiymat (B)
CAS M ni B ga atomar tarzda yangilaydi, lekin faqat M ning qiymati A ga to'g'ri kelganda, aks holda hech qanday harakat qilinmaydi.
Dastlabki va ikkinchi holatda M qiymati qaytariladi. Bu uchta qadamni, ya'ni qiymat olish, qiymatni taqqoslash va uning yangilanishini birlashtiradi. Bu barcha operatsiyalar mashina darajasida bitta operatsiyaga aylanadi.
Ko'p oqimli dastur o'zgaruvchiga murojaat qilib, uni yangilashga harakat qilganda, CAS ishlatiladi va bir oqim uni olish va yangilashga qodir bo'ladi. Lekin blokirovkalardan farqli o'laroq, boshqa oqimlar faqat yangilay olmaganliklari haqida xatoliklarni oladi. Keyin ular o'z ishlarini davom ettiradi va bunday ish bilan o'tganda to'liq siljish yo'q.
Biroq, CAS operatsiyasi muvaffaqiyatli amalga oshmaganda, vaziyatni boshqarish murakkablashadi. Kodni shunday modellashtirishimiz kerakki, operatsiya muvaffaqiyatli amalga oshirilmaguncha davom etmasin.
Atomar turlari bilan tanishish
Siz oddiy int turidagi o'zgaruvchini sinxronlashtirish kerak bo'lgan vaziyatga duch keldingizmi?
Birinchi usul — biz ilgari ko'rib chiqqan volatile + synchronized ishlatish. Lekin hali ham maxsus Atomic* sinf styuardessalari mavjud.
CAS ishlatilganida, operatsiyalar birinchi usulga nisbatan tezroq ishlaydi. Bundan tashqari, qiymat qo'shish va inkrement va dekremeniya qilish uchun juda qulay usullar mavjud.
AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray — ushbu sinflarda operatsiyalar atomar. Quyida biz ular bilan ishlashni ko'rib chiqamiz.
AtomicInteger
AtomicInteger sinfi int qiymatlari bilan operatsiyalarni atomar tarzda o'qish va yozish imkonini beradi, va qo'shimcha ravishda keng qamrovli atomar operatsiyalar mavjud.
U get va set metodlarini o'z ichiga oladi, ular o'zgaruvchilarni o'qish va yozish sifatida ishlaydi.
"Oldin sodir bo'lgan (happens-before)" biz ilgari aytgan o'zgaruvchi bilan har qanday keyingi olishga taalluqli. Atomar compareAndSet metodi ham ushbu xotira muvofiqligini ta'minlaydigan xususiyatlarga ega.
Yangi qiymat qaytaradigan barcha operatsiyalar atomar tarzda amalga oshiriladi:
| int addAndGet (int delta) | Joriy qiymatga ma'lum qiymat qo'shiladi. |
| boolean compareAndSet (кутилиши int, yangilanish int) | Joriy qiymat kutilgan qiymatga to'g'ri kelganda, yangilanish qiymatini o'rnatadi. |
| int decrementAndGet () | Joriy qiymatni bir birlikka kamaytiradi. |
| int getAndAdd (int delta) | Joriy qiymatga berilgan qiymat qo'shiladi. |
| int getAndDecrement () | Joriy qiymatni bir birlikka kamaytiradi. |
| int getAndIncrement () | Joriy qiymatni bir birlikka oshiradi. |
| int getAndSet (int newValue) | Berilgan qiymatni o'rnatadi va eski qiymatni qaytaradi. |
| int incrementAndGet () | Joriy qiymatni bir birlikka oshiradi. |
| lazySet (int newValue) | Oxir-oxirda berilgan qiymatga o'rnatadi. |
| boolean weakCompareAndSet (кутилиши, yangilanish int) | Joriy qiymat kutilgan qiymatga to'g'ri kelganda, yangilanish qiymatini o'rnatadi. |
Misol:
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 ni chiqaradi
GO TO FULL VERSION