![translation](https://cdn.durumis.com/common/trans.png)
Bu, AI tarafından çevrilen bir gönderidir.
Dil Seç
Text summarized by durumis AI
- Vector, Hashtable, Collections.synchronizedXXX gibi senkronize edilmiş koleksiyonlar, içsel olarak synchronized anahtar sözcüğünü kullanarak eşzamanlılığı sağlar, ancak birden fazla işlemi bir araya getirerek kullanmak veya performans düşüşü sorunları ortaya çıkabilir.
- java.util.concurrent paketinde sağlanan eşzamanlı koleksiyonlar, CopyOnWriteArrayList, ConcurrentMap, ConcurrentHashMap gibi çeşitli sınıflar sunar ve senkronize edilmiş koleksiyonlardan daha yüksek performansa sahiptir.
- CopyOnWriteArrayList, ConcurrentHashMap gibi eşzamanlı koleksiyonlar, senkronize edilmiş koleksiyonlara göre daha yüksek okuma performansına sahiptir ve yazma işlemleri nispeten az olduğunda kullanılması etkilidir.
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
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
public class Hashtable extends Dictionary
implements Map, Cloneable, java.io.Serializable {
...
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry,?> tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
for (Entry,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
...
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.
static class SynchronizedList extends SynchronizedCollection
implements List {
final Object mutex;
...
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
...
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.
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() {
public void run() {
while(true) {
try {
list.clear();
list.add("888");
list.remove(0);
} catch(IndexOutOfBoundsException e) {
e.printStackTrace();
}
}
}
});
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.
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.
public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {
final transient Object lock = new Object();
private transient volatile Object[] array;
...
public E get(int index) {
return elementAt(getArray(), index);
}
...
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.
public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, java.io.Serializable {
final transient Object lock = new Object();
private transient volatile Object[] array;
public void add(int index, E element) {
synchronized (lock) {
...
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index, newElements, index + 1,
numMoved);
}
...
}
}
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
public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {
private final CopyOnWriteArrayList al;
public boolean contains(Object o) {
return al.contains(o);
}
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.
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
Object[] es = getArray();
return indexOfRange(o, es, 0, es.length);
}
private static int indexOfRange(Object o, Object[] es, int from, int to) {
if (o == null) {
for (int i = from; i < to; i++)
if (es[i] == null)
return i;
} else {
for (int i = from; i < to; i++)
if (o.equals(es[i]))
return i;
}
return -1;
CopyOnWriteArrayList'in contains() yöntemini incelediğimizde, kilitlenmediğini görebiliriz.
Yazma İşlemleri
public class CopyOnWriteArraySet extends AbstractSet implements java.io.Serializable {
private final CopyOnWriteArrayList al;
public boolean add(E e) {
return al.addIfAbsent(e);
}
add() yöntemi de CopyOnWriteArrayList'in yöntemlerini kullanır.
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
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[] tab = table;;) {
Node 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(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 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 pred = e;
// (5)
if ((e = e.next) == null) {
pred.next = new Node(hash, key, value);
break;
}
}
}
// (6)
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)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.
static final Node tabAt(Node[] tab, int i) {
return (Node)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.
static final boolean casTabAt(Node[] tab, int i, Node c, Node 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
(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.