제이온

[Java] Senkronize Edilmiş Koleksiyonlar ve Eş Zamanlı Koleksiyonlar

  • Yazım Dili: Korece
  • Baz Ülke: Tüm Ülkelercountry-flag
  • BT

Oluşturulma: 2024-04-25

Oluşturulma: 2024-04-25 22:31

Senkronize Edilmiş Koleksiyonlar

Senkronize edilmiş koleksiyonlar, öncelikle aşağıdaki gibi sınıfları içerir.


  • Vector
  • Hashtable
  • Collections.synchronizedXXX


Bu sınıfların hepsi, içerideki değerleri yalnızca bir iş parçacığının kullanabileceği şekilde kontrol ederek ve aynı zamanda eşzamanlılığı sağlayarak, genel olarak ilan edilmiş yöntemlerde synchronized anahtar sözcüğünü kullanır.


Vector

```javascript public class Vector extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable { ... public synchronized boolean add(E e) { modCount++; add(e, elementData, elementCount); return true; } ... }

Vector sınıfında eleman ekleyen add() yöntemini incelediğimizde synchronized anahtar sözcüğünü görürüz. Yani, Vector içinde eleman ekleme işlemi gerçekleştiğinde eşzamanlılık sağlanır.

Hashtable

```javascript public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { ... public synchronized boolean contains(Object value) { if (value == null) { throw new NullPointerException(); }

}


Hashtable sınıfında aynı değerin olup olmadığını kontrol eden contains() yöntemini incelediğimizde, Vector sınıfında olduğu gibi synchronized anahtar sözcüğünün kullanıldığını görürüz.


Collections.synchronizedXXX

Collections.synchronizedList() yöntemi kullanılarak oluşturulan SynchronizedList sınıfını inceleyelim.


```javascript static class SynchronizedList extends SynchronizedCollection implements List {

}


SynchronizedList sınıfının yöntemlerini incelediğimizde hepsinde synchronized anahtar sözcüğünün kullanıldığını görürüz. Ancak eşzamanlılığı sağlamak için mutex aracılığıyla synchronized bloğu kullanılmıştır. Tüm yöntemler mutex objesini paylaşır, bu nedenle bir iş parçacığı synchronized bloğuna girdiği anda, diğer yöntemlerin synchronized bloğu da kilitlenir.


Senkronize Edilmiş Koleksiyonların Sorunları

Çoklu iş parçacığı ortamında senkronize edilmiş koleksiyonlar kullanılması gereken durumlar da olsa, mümkün olduğunca başka eşzamanlılık yöntemleri kullanmak daha iyidir. Bunun nedeni, esas olarak iki ana nedene dayanır.


Birkaç işlemi tek işlem gibi kullanma

Senkronize edilmiş koleksiyon sınıfları, çoklu iş parçacığı ortamında eşzamanlılığı sağlar. Ancak, birkaç işlemi tek işlem gibi kullanmak gerekiyorsa sorunlar ortaya çıkar. Bu senkronize edilmiş koleksiyonları kullanmaya rağmen doğru şekilde çalışmayabilir.


```javascript final List list = Collections.synchronizedList(new ArrayList()); final int nThreads = 2; ExecutorService es = Executors.newFixedThreadPool(nThreads);

for (int i = 0; i < nThreads; i++) { es.execute(new Runnable() {

}


Yukarıdaki kod çalıştırıldığında, Thread A remove(0) işlemini yaparken Thread B clear() işlemini yaparsa hata oluşur. Bu nedenle aşağıdaki gibi bir synchronized bloğu içinde gruplanmalıdır.


```javascript synchronized (list) { list.clear(); list.add("888"); list.remove(0); }


Performans Düşüşü

Paylaşılan nesneyi kullanmak isteyen tüm yöntemleri synchronized yöntemler yaparsanız veya yöntemlerin içinde aynı synchronized bloğu tanımlarsanız, bir iş parçacığı kilidi elde ettiği anda diğer iş parçacıkları tüm senkronize edilmiş yöntemleri kullanamaz ve engelleyici durumda olur. Bu tekrarlanan durum, performans düşüşüne neden olabilir.


Eşzamanlı Koleksiyonlar

java.util.concurrent paketinde sunulan paralel koleksiyon türleri aşağıdaki gibidir ve bunlardan yalnızca bazıları bu makalede ele alınacaktır.


  • CopyOnWriteArrayList
    • List sınıfının alt sınıfıdır ve nesne listesini yineleyerek sorgulama işlemlerinin performansını önceliklendiren bir paralel koleksiyondur.
  • ConcurrentMap
    • Paralel bir koleksiyondur ve arayüzü, eklenmek istenen öğenin daha önce yoksa yalnızca eklenmesini sağlayan put-if-absent, replace, conditional remove işlemleri gibi yöntemleri tanımlar.
  • ConcurrentHashMap
    • ConcurrentMap'in alt sınıfıdır ve HashMap'in yerine geçerek paralelliği sağlayan paralel bir koleksiyondur.
  • ConcurrentLinkedQueue
    • FIFO yöntemini kullanan bir Queue'dur ve paralelliği sağlayan bir paralel koleksiyondur. Kuyrukta çıkarılacak öğe yoksa, derhal döndürür ve başka bir işlemi yürütmeye gider.
  • LinkedBlockingQueue
    • ConcurrentLinkedQueue'a benzer. Ancak kuyruk boşsa kuyruktan öğe çıkarma işlemi, yeni öğe eklenene kadar bekler. Tersine, kuyruğa boyut verilmişse ve kuyruk belirtilen boyuta kadar doluysa, kuyruğa yeni öğe ekleme işlemi kuyrukta boş yer olana kadar bekler.
  • ConcurrentSkipListMap, ConcurrentSkipListSet
    • Sırasıyla SortedMap ve SortedSet sınıflarının paralelliğini artıran gelişmiş biçimleridir.


Daha önce kullanılan senkronize edilmiş koleksiyon sınıflarını paralel koleksiyonlarla değiştirmek bile, başka risk faktörleri olmadan genel performansı önemli ölçüde artırabilir.

Paralel koleksiyonlara karşıt olan senkronize edilmiş koleksiyonları karşılaştırarak ayrıntılı olarak inceleyelim.


CopyOnWriteArrayList

Senkronize edilmiş bir ArrayList oluşturmanın iki yolu vardır.


  • Collections.synchronizedList()
  • CopyOnWriteArrayList


Collections.synchronizedList() JDK 1.2 sürümüne eklenmiştir. Bu koleksiyon, tüm okuma ve yazma işlemleri için senkronize edilmiştir, bu nedenle esnek olmayan bir tasarım olarak kabul edilebilir. Bu nedenle, CopyOnWriteArrayList ortaya çıkmıştır.


Okuma İşlemleri

SynchronizedList, okuma ve yazma işlemleri sırasında kendi kendine kilitlenir. Ancak CopyOnWriteArrayList, tüm yazma işlemleri sırasında orijinal dizideki elemanları kopyalayarak yeni bir geçici dizi oluşturur ve bu geçici dizide yazma işlemini gerçekleştirdikten sonra orijinal diziyi günceller. Bunun sayesinde okuma işlemleri kilitlenmez, bu nedenle SynchronizedList'ten daha iyi performansa sahiptir.


```javascript public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

}


Yukarıda get() yöntemi yer almaktadır ve synchronized olmadığı için kilitlenmemektedir.


Yazma İşlemleri

CopyOnWriteArrayList, yazma işlemi gerçekleştirirken açıkça kilit kullanır. Sonuç olarak, her iki koleksiyon türü de bu işlemde kilitlenir. Bu durumda CopyOnWriteArrayList, nispeten maliyetli dizi kopyalama işlemini gerçekleştirir, bu nedenle önemli sayıda yazma işlemi gerçekleştirilirse performans sorunları ortaya çıkabilir.


```javascript public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {

}


Yukarıda add() yöntemi yer almaktadır ve synchronized bloğu aracılığıyla kilitlenir ve dizi kopyalama işlemi gerçekleştirilir.


Iterator

CopyOnWriteArrayList'te yineleyiciyi çıkarma anındaki koleksiyon verilerine göre yineleme yapılır ve yineleme sırasında koleksiyona veri eklenmesi veya silinmesi, yineleme döngüsüyle ilgili olmayan bir kopya üzerinde yansıtılır, bu nedenle eşzamanlı kullanımlarda sorun yaşanmaz.


CopyOnWriteArraySet

Senkronize edilmiş bir Set oluşturmanın iki yolu vardır.


  • Collections.synchronizedSet()
  • CopyOnWriteArraySet


Yöntem adından da anlaşılacağı gibi, CopyOnWriteArrayList ile çalışma yöntemi, veri yapısı özelliği hariç, neredeyse aynıdır.


Okuma İşlemleri

```javascript public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

}


Yukarıda contains() yöntemi yer almaktadır ve CopyOnWriteArraySet'in içeride CopyOnWriteArrayList'i tanımladığını ve CopyOnWriteArrayList'in yöntemlerini kullandığını görebiliriz.


```javascript public boolean contains(Object o) { return indexOf(o) >= 0; }

public int indexOf(Object o) { Object[] es = getArray(); return indexOfRange(o, es, 0, es.length); }

}


CopyOnWriteArrayList'in contains() yöntemini incelediğimizde, kilitlenmediğini görebiliriz.


Yazma İşlemleri

```javascript public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {

}


add() yöntemi de CopyOnWriteArrayList'in yöntemlerini kullanır.


```javascript public boolean addIfAbsent(E e) { Object[] snapshot = getArray(); return indexOfRange(e, snapshot, 0, snapshot.length) < 0 && addIfAbsent(e, snapshot); }

private boolean addIfAbsent(E e, Object[] snapshot) { synchronized (lock) { Object[] current = getArray(); int len = current.length; if (snapshot != current) { // Optimize for lost race to another addXXX operation int common = Math.min(snapshot.length, len); for (int i = 0; i < common; i++) if (current[i] != snapshot[i] && Objects.equals(e, current[i])) return false; if (indexOfRange(e, current, common, len) >= 0) return false; } Object[] newElements = Arrays.copyOf(current, len + 1); newElements[len] = e; setArray(newElements); return true; } }


addIfAbsent() yöntemini incelediğimizde, yazma işlemi gerçekleştirirken kilitlendiğini ve dizi kopyalama işleminin gerçekleştiğini görürüz. Bu nedenle, CopyOnWriteArraySet de CopyOnWriteArrayList gibi, çok sayıda yazma işlemi yapmaktan kaçınmak daha iyidir.


ConcurrentHashMap

Senkronize edilmiş bir HashMap oluşturmanın iki yolu vardır.


  • Collections.synchronizedMap(new HashMap<>())
  • ConcurrentHashMap


ConcurrentHashMap, HashMap ile aynı şekilde Hash tabanlı bir Map'tir. synchronizedMap'e göre daha etkili bir şekilde eşzamanlılığı sağlar.


Java 8'den önce, ReentrantLock'tan kalıtım alan Segment'leri kullanarak, bölgeleri ayırarak bölge bazlı kilitleme gerçekleştiriliyordu.


Java 8'den sonra, her bir tablo kovasını bağımsız olarak kilitleme yöntemi kullanılmaktadır. Boş bir kovaya düğüm eklenmesi durumunda, kilit yerine CAS algoritması kullanılır ve diğer değişiklikler, her bir kovadaki ilk düğümü temel alarak kısmi kilit (synchronized block) elde edilerek iş parçacığı çatışması en aza indirilir ve eşzamanlılık sağlanır.


ConcurrentHashMap'e yeni bir düğüm ekleyen putVal() yönteminin kodunu inceleyerek eşzamanlılığın nasıl sağlandığını görelim. Dikkat çekmek gerekirse, aşağıdaki örnek kod Java 11 tabanlıdır.


putVal() yöntemi, temel olarak aşağıdaki iki duruma (toplam dört bölümde şube) ayrılabilir.


  • Boş bir hash kovasına düğüm ekleme
  • Hash kovasında zaten düğüm varsa


```javascript final V putVal(K key, V value, boolean onlyIfAbsent) { if (key null || value null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; K fk; V fv; if (tab null || (n = tab.length) 0) tab = initTable(); // (1) else if ((f = tabAt(tab, i = (n - 1) & hash)) null) { // (2) if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) break; } else if ((fh = f.hash) MOVED) tab = helpTransfer(tab, f); else if (onlyIfAbsent // check first node without acquiring lock && fh hash && ((fk = f.key) key || (fk != null && key.equals(fk))) && (fv = f.val) != null) return fv; // (3) else { V oldVal = null; synchronized (f) { if (tabAt(tab, i) f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; // (4) if (e.hash hash && ((ek = e.key) key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; // (5) if ((e = e.next) null) { pred.next = new Node<K,V>(hash, key, value); break; } } } // (6) else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } else if (f instanceof ReservationNode) throw new IllegalStateException("Recursive update"); } } ... } } ... }


Boş bir hash kovasına düğüm ekleme

(1) Yeni bir düğüm eklemek için, ilgili kovanın değerini (tabAt()) alır ve boş olup olmadığını kontrol eder.


```javascript static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectAcquire(tab, ((long)i << ASHIFT) + ABASE); }


(2) Düğümde bulunan volatile değişkene erişerek, mevcut değerle (null) karşılaştırılır ve aynıysa yeni düğüm kaydedilir. Aynı değilse, for döngüsü tekrarlanır. Bu yöntem CAS algoritmasıdır.


```javascript static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v); }


CAS algoritması kullanılarak atomicity ve görünürlük sorunları çözülür ve eşzamanlılık sağlanır.


Hash kovasında zaten düğüm varsa

(3) Zaten düğüm varsa, synchronized block kullanılarak yalnızca bir iş parçacığının erişmesi sağlanır. Bu durumda, boş olmayan Node<K, V> tipindeki hash kovasına kilitlendiğinden, aynı kovaya erişen iş parçacıkları engelleyici durumda olur.

(4) Yeni düğümle değiştirilir.

(5) Hash çakışması meydana gelirse, Ayrı Bağlamaya eklenir.

(6) Hash çakışması meydana gelirse, ağaca eklenir.


Referanslar


Beklenen Görüşme Soruları ve Cevapları

Vector, HashTable ve Collections.SynchronziedXXX'in sorunları nelerdir?

Vector, HashTable ve SynchronziedXxx sınıfları synchronized yöntemler veya bloklar kullanır ve aynı kilidi paylaşır. Bu nedenle, koleksiyonlara bir iş parçacığı tarafından kilit elde edilirse, diğer iş parçacıkları tüm yöntemleri kullanamıyor ve engelleyici durumda kalıyor. Bu da uygulama performansında düşüşe neden olabilir.


SynchronizedList ve CopyOnArrayList arasındaki farklar nelerdir?

SynchronizedList, okuma ve yazma işlemleri sırasında kendi kendine kilitlenir. Ancak CopyOnArrayList, yazma işlemi sırasında ilgili bloğu kilitler ve orijinal dizideki elemanları kopyalayarak yeni bir geçici dizi oluşturur ve bu geçici dizide yazma işlemini gerçekleştirdikten sonra orijinal diziyi günceller. Bunun sayesinde okuma işlemleri kilitlenmez, bu nedenle SynchronizedList'ten daha iyi okuma performansına sahiptir. Ancak yazma işlemleri, nispeten maliyetli dizi kopyalama işlemini gerçekleştirdiği için SynchronizedList'ten daha düşük yazma performansına sahiptir.

Bu nedenle, değişiklik yapma işlemlerinden çok okuma işlemi yapılıyorsa, CopyOnArrayList kullanmak daha etkilidir.


SynchronizedMap ve ConcurrentHashMap arasındaki farklar nelerdir?

SynchronziedMap, okuma ve yazma işlemleri sırasında kendi kendine kilitlenir. Ancak ConcurrentHashMap, her bir tablo kovasını bağımsız olarak kilitleme yöntemi kullanır. Örneğin, boş bir kovaya düğüm eklenmesi durumunda kilit (Lock) yerine CAS algoritması kullanılır ve diğer değişiklikler, erişilen kovaya yalnızca kilitlenerek iş parçacığı çatışması en aza indirilir ve eşzamanlılık sağlanır.

Yorumlar0