1. Niyə adi kolleksiyalar çoxsahəlilik üçün uyğun deyil
Gəlin, əsas tətbiqimizdə (məsələn, söhbət otağında) kolleksiyalarla necə işlədiyimizi xatırlayaq:
List<String> messages = new ArrayList<>();
messages.add("Salam!");
messages.add("Necəsən?");
Tək axınlı proqramda hər şey əladır. Amma bir neçə axın eyni anda eyni kolleksiyadan elementləri əlavə etsə, silsə və ya oxusa — məlumat yarışları (race conditions), uyğunsuz vəziyyət və müəmmalı xətalar dünyasına xoş gəlmisiniz.
Məsələn, bir axın element əlavə edir, digəri silir, üçüncüsü iterasiya edir — və qəfil ConcurrentModificationException alırıq, bəzən hətta ArrayIndexOutOfBoundsException və ya sadəcə “pozulmuş” kolleksiya ilə üzləşirik.
Klassik nümunə:
List<String> list = new ArrayList<>();
Runnable writer = () -> {
for (int i = 0; i < 1000; i++) {
list.add("msg-" + i);
}
};
Runnable reader = () -> {
for (String msg : list) {
// ...
}
};
// writer və reader-i müxtəlif axınlarda işə salırıq — xətalar alacaqsınız!
Nəticə: Adi kolleksiyalar (ArrayList, HashMap, HashSet və s.) thread-safe DEYİL. Onları əlavə sinxronizasiya olmadan (synchronized, kilidlər və s.) bir neçə axından istifadə etmək olmaz.
2. Java-da hansı thread-safe kolleksiyalar var
Java sizi taleyin ümidinə buraxmır. Çoxsahəli tapşırıqlar üçün java.util.concurrent paketində bir neçə axından təhlükəsiz istifadə oluna bilən zəngin kolleksiyalar toplusu var.
Əsas thread-safe kolleksiyalar:
| Kolleksiya | Harada istifadə olunur | Xüsusiyyətlər |
|---|---|---|
|
Map, keş, tez-tez giriş | Yüksək məhsuldarlıq, qlobal lock yoxdur |
|
List, nadir dəyişir, tez-tez oxunur | Sürətli oxuma, yavaş dəyişikliklər |
|
Set, nadir dəyişir, tez-tez oxunur | Copy-On-Write siyahısına bənzər |
|
Növbə, FIFO | Sürətli, bloklanmayan, tapşırıq növbələri |
|
Sıralama ilə Map (NavigableMap) | TreeMap-in thread-safe analoqu |
|
Sıralama ilə Set | TreeSet-in thread-safe analoqu |
|
Bloklamalı növbələr (axın hovuzları) | İnterfeys, çox sayda reallaşdırma |
Vacib! Köhnə, lakin tanış Collections.synchronizedList(list) və oxşarları java.util.concurrent-dəki müasir kolleksiyalarla tam eyni deyil. Ətraflı — bir az aşağıda.
3. ConcurrentHashMap: çoxsahəlilik dünyasında dostunuz
ConcurrentHashMap<K, V> — mahiyyətcə HashMap-ın çoxsahəlilik üçün gücləndirilmiş variantıdır. O, bir neçə axına eyni vaxtda bütün xəritəni tam kilidləmədən təhlükəsiz oxuma və yazma imkanı verir.
Adi HashMap-da, əgər girişi thread-safe etmək istəsəniz, bütün strukturu kilidləmək lazım gəlir — və bu da dərhal “dar keçid”ə çevrilir: bir axın işləyərkən, digərləri gözləyir.
ConcurrentHashMap bu problemi daha ağıllı həll edir. Erkən versiyalarda xəritə ayrıca kilidlərə malik seqmentlərə bölünürdü, yeni realizasiyalarda isə ayrı bucket səviyyəsində yüngül atomik əməliyyatlardan (CAS) istifadə olunur. Bunun sayəsində axınlar eyni verilənlərə toxunmadıqda paralel işləyə bilirlər.
ConcurrentHashMap istifadəsinə nümunə
import java.util.concurrent.ConcurrentHashMap;
public class ChatStats {
private final ConcurrentHashMap<String, Integer> userMessageCount = new ConcurrentHashMap<>();
public void increment(String user) {
// Dəyəri atomik artırırıq
userMessageCount.merge(user, 1, Integer::sum);
}
public int getCount(String user) {
return userMessageCount.getOrDefault(user, 0);
}
}
Burada vacib olanlar:
- Metodları müxtəlif axınlardan çağırmaq olar — hər şey düzgün olacaq.
- merge metodu atomikdir: bir neçə axın eyni vaxtda sayğacı artırsa belə, nəticə düzgün olacaq.
- Oxuma üçün əlavə sinxronizasiya lazım deyil.
ConcurrentHashMap nə ilə synchronizedMap-dan yaxşıdır?
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
synchronizedMap istifadə etdikdə, istənilən əməliyyat — oxuma, yazma və ya silmə — bütün xəritəni bloklayır. Bir axın verilənlərlə işləyərkən, digərləri növbəsini gözləməyə məcburdur.
ConcurrentHashMap çox daha zərif qurulub: eyni vaxtda bir neçə axına verilənləri oxumağa, hətta eyni xəritənin müxtəlif sahələrinə (bucket-lərə) toxunurlarsa dəyişməyə imkan verir. Nəticədə real çoxsahəli sistemlərdə məhsuldarlıq əhəmiyyətli dərəcədə yüksək olur — bəzən fərq onlarla dəfədir.
4. CopyOnWriteArrayList və CopyOnWriteArraySet
CopyOnWriteArrayList və CopyOnWriteArraySet — hər bir dəyişiklikdə (məsələn, add() və ya remove()) bütün massivdən yeni nüsxə yaradan xüsusi kolleksiyalardır. Bunun müqabilində onlardan oxuma sinxronizasiya olmadan və tamamilə təhlükəsiz baş verir.
Təsəvvür edin, bir ziyafət üçün qonaq siyahınız var. Kimsə gəldikdə və ya getdikdə, siyahını yenidən köçürür və hamıya təzə nüsxələr paylayırsınız. Bir az israfçılıq kimi görünür, amma heç kim “kim hazırda buradadır” məsələsində çaşmır.
Bu nə vaxt həqiqətən uyğundur
- Oxumalar tez-tez, dəyişikliklər isə nadir hallarda baş verir.
- Klassik hal — hadisə dinləyiciləri siyahısı: handler-lər nadir əlavə olunur, amma hadisələr daimi gəlir.
Nümunə: çat dinləyiciləri
import java.util.concurrent.CopyOnWriteArrayList;
public class ChatRoom {
private final CopyOnWriteArrayList<ChatListener> listeners = new CopyOnWriteArrayList<>();
public void addListener(ChatListener listener) {
listeners.add(listener);
}
public void removeListener(ChatListener listener) {
listeners.remove(listener);
}
public void sendMessage(String message) {
// Çoxsahəlilik üçün təhlükəsizdir, kimsə hazırda abunə olub/çıxsa belə
for (ChatListener listener : listeners) {
listener.onMessage(message);
}
}
}
Vacib xüsusiyyətlər:
- CopyOnWriteArrayList üzrə iterasiya heç vaxt ConcurrentModificationException atmayacaq.
- Dəyişikliklər (add/remove) vaxt və yaddaşa görə bahalıdır (bütün massiv köçürülür!).
- Böyük və tez-tez dəyişən kolleksiyalar üçün istifadə etmək məsləhət deyil.
5. Digər thread-safe kolleksiyalar
ConcurrentLinkedQueue
ConcurrentLinkedQueue — FIFO prinsipinə əsaslanan, bloklanmayan növbədir. O, bir neçə axına elementləri aydın kilidlərdən istifadə etmədən eyni anda təhlükəsiz əlavə etməyə və götürməyə imkan verir. Tez-tez axınlar arasında tapşırıqların ötürülməsi üçün istifadə olunur — sürətli və “tıxanmasız”.
import java.util.concurrent.ConcurrentLinkedQueue;
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.add("task1");
String task = queue.poll(); // növbə boşdursa null qaytaracaq
ConcurrentSkipListMap və ConcurrentSkipListSet
- TreeMap və TreeSet-in thread-safe analoqlarıdır.
- Elementlər həmişə sıralanır.
- Açarların ardıcıllığını qorumaq vacib olduqda istifadə edilir.
import java.util.concurrent.ConcurrentSkipListMap;
ConcurrentSkipListMap<Integer, String> sortedMap = new ConcurrentSkipListMap<>();
sortedMap.put(10, "a");
sortedMap.put(2, "b");
System.out.println(sortedMap.firstEntry()); // 2=b
BlockingQueue və onun reallaşdırmaları
- Bloklama əməliyyatlarını dəstəkləyən növbə interfeysi (yer açılanda/gələndə gözləmək).
- Reallaşdırmalar: ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue və s.
- Axın hovuzlarında, “producer–consumer” nümunəsi üçün istifadə olunur.
import java.util.concurrent.ArrayBlockingQueue;
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
blockingQueue.put("task"); // Növbə doludursa bloklayır
String t = blockingQueue.take(); // Növbə boşdursa bloklayır
6. Nümunələr: kolleksiyalarla təhlükəsiz əməliyyatlar
Nümunə 1: Mesajların sayılması üçün thread-safe Map
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> messageCount = new ConcurrentHashMap<>();
// Axın 1
messageCount.put("Anna", 1);
// Axın 2
messageCount.put("Anna", messageCount.getOrDefault("Anna", 0) + 1); // Atomik deyil!
// Düzgün (atomik):
messageCount.merge("Anna", 1, Integer::sum);
Nümunə 2: CopyOnWriteArrayList üzrə iterasiya
import java.util.concurrent.CopyOnWriteArrayList;
CopyOnWriteArrayList<String> users = new CopyOnWriteArrayList<>();
users.add("Anton");
users.add("Mariya");
for (String user : users) {
System.out.println(user);
users.remove(user); // ConcurrentModificationException atmayacaq!
}
System.out.println(users); // []
Nümunə 3: Axınlar arasında tapşırıq növbəsi
import java.util.concurrent.ConcurrentLinkedQueue;
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
// İstehsalçı axın
queue.add("task-1");
// İstehlakçı axın
String task = queue.poll(); // boşdursa null
7. Faydalı nüanslar
Thread-safe kolleksiyalardan nə zaman (və niyə) istifadə etməli
Thread-safe kolleksiyalardan istifadə aşağıdakı hallarda əsaslandırılmışdır:
- Eyni kolleksiya bir neçə axın arasında bölüşdürülür.
- Hər əməliyyatı əl ilə sinxronizasiya etmək istənilmir.
- Məlumat yarışlarının və konsistentlik səhvlərinin qarşısını almaq vacibdir.
Tipik ssenarilər:
- Çoxsahəli sistemdə keş (məsələn, istifadəçi sessiyalarını saxlamaq üçün ConcurrentHashMap).
- Axınlar arasında tapşırıq növbələri (ConcurrentLinkedQueue, BlockingQueue).
- Hadisə dinləyiciləri siyahıları (CopyOnWriteArrayList).
- Çoxsahəli məlumat emalı (məsələn, MapReduce tərzi).
Məhdudiyyətlər və gizli tələlər
- Bir neçə element üzərində əməliyyatlar atomik deyil. if (!map.containsKey(k)) map.put(k, v) kimi konstruksiya atomik deyil. putIfAbsent, computeIfAbsent, merge istifadə edin.
- CopyOnWriteArrayList tez-tez dəyişikliklərdə qeyri-səmərəlidir. Böyük ölçülərdə və tez-tez add/remove olduqda əlavə xərclər sürətlə artır.
- ConcurrentHashMap üzrə iterasiya “zəif”dir. Keçid zəif-konsistent görüntü verir: paralel dəyişikliklərin bir hissəsini görməyə bilərsiniz.
- Thread-safe kolleksiyalar bütün sinxronizasiya problemlərini həll etmir. Əgər məntiq eyni anda bir neçə kolleksiyaya/dəyişkənə toxunursa, xarici sinxronizasiya tələb olunur (synchronized, kilidlər, atomik siniflər).
8. Thread-safe kolleksiyalarla işləyərkən tipik səhvlər
Səhv №1: Thread-safe kolleksiyalardan möcüzə gözləmək. “Kolleksiya thread-safe-dirsə, nə istəsən edə bilərəm və sinxronizasiya barədə düşünməyə ehtiyac yoxdur”. Təəssüf ki, bir neçə əməliyyatdan ibarət ardıcıllıqlar (yoxlama + əlavə etmə) atomik deyil. Xüsusi metodlardan istifadə edin: putIfAbsent, compute, merge.
Səhv №2: Böyük və tez-tez dəyişən kolleksiyalar üçün CopyOnWriteArrayList istifadə etmək. Dinləyici siyahıları üçün uyğundur, amma 10 000+ element olduqda və tez-tez dəyişikliklərdə yaddaş və zaman xərcləri böyük olacaq.
Səhv №3: Adi kolleksiyalardan istifadə edərkən ConcurrentModificationException. ArrayList və ya HashMap üzrə iterasiya edirsiniz, eyni anda başqa axın kolleksiyanı dəyişir — ConcurrentModificationException alırsınız. Xüsusi kolleksiyalardan istifadə edin və ya girişi əl ilə bloklayın.
Səhv №4: Mürəkkəb əməliyyatların atomikliyini unutmaq. Əgər eyni anda bir neçə kolleksiyanı dəyişmək və ya əlaqəli əməliyyatlar silsiləsi icra etmək lazımdırsa — təkcə thread-safe kolleksiyalar yetərli deyil. Xarici sinxronizasiya və ya tranzaksiyalı məntiq tətbiq edin.
Səhv №5: ConcurrentHashMap üzrə iterasiya zamanı səhvlər. Iterasiya zəif-konsistentdir: iteratordan xəritənin “görüntüsü” kimi istifadə etmək olmaz. Konsistent görüntü üçün verilənləri ayrıca struktura köçürməkdən istifadə edin.
GO TO FULL VERSION