Takip et

ConcurrentSkipListMap ile Java Concurrency Rehberi

Yüksek performanslı eşzamanlı Java uygulamalarında verileri hem thread-safe hem de sıralı tutmak zorlaşır. ConcurrentSkipListMap bu problemi zahmetsizce çözer.

ConcurrentSkipListMap ile Java Concurrency Rehberi

Yüksek performanslı eşzamanlı Java uygulamalarında verileri hem thread-safe hem de sıralı tutmak zorlaşır. ConcurrentSkipListMap bu problemi zahmetsizce çözer.

Java platformunda çok izlekli (multi-threaded) uygulamalar geliştirirken, veri yapılarını doğru seçmek mimarinin kaderini belirler. Genelde geliştiriciler anahtar-değer çiftlerini saklamak için doğrudan ConcurrentHashMap sınıfına başvururlar. Ancak, verilerin belirli bir sıraya göre dizilmesi ve bu sıranın eşzamanlı (concurrent) erişimler altında korunması gerektiğinde ConcurrentHashMap yetersiz kalır. İşte bu noktada, Java’nın java.util.concurrent paketinde yer alan ve Atlamalı Liste (SkipList) veri yapısına dayanan ConcurrentSkipListMap devreye girer.

Özellikle sistem tasarımı ve mülakatlarda sıkça karşılaşılan “Machine Coding” (Canlı Kodlama) aşamalarında, sıralı ve thread-safe bir harita (map) oluşturmak kritik bir beceridir. Örneğin; dinamik bir liderlik tablosu (leaderboard), yüksek frekanslı borsa emir defteri (order book) veya zaman serisi veri işleme motoru yazmanız istendiğinde bu yapıyı bilmek size büyük bir avantaj sağlar. Bu rehberde, ConcurrentSkipListMap yapısını temel mekanizmasından başlayarak ileri seviye kullanım senaryolarına kadar adım adım inceleyeceğiz.

Eşzamanlı Java Programlamada Neden Sıralı Veri Yapılarına İhtiyaç Duyarız?

Çok izlekli ortamlarda veri sıralamasını korumak geleneksel yöntemlerle oldukça maliyetlidir. Standart bir TreeMap sınıfı, elemanlarını kırmızı-siyah ağaç (Red-Black Tree) algoritması ile sıralı bir şekilde tutar. Fakat TreeMap thread-safe değildir. Eğer birden fazla thread aynı anda bir TreeMap üzerinde yazma veya okuma işlemi yaparsa, veri yapısının iç bütünlüğü bozulur ve uygulamada beklenmeyen hatalar meydana gelir.

Bu sorunu çözmek için akla gelen ilk yöntem, TreeMap yapısını Collections.synchronizedSortedMap() ile sarmalamaktır (wrap etmek). Ancak bu yaklaşım, her işlemde tüm haritayı tek bir kilit (lock) arkasına gizler. Sonuç olarak, uygulamanızda ciddi bir performans darboğazı (bottleneck) oluşur. Okuma yapan thread’ler bile yazma yapan bir thread’in kilit açmasını beklemek zorunda kalır. Eşzamanlılık (concurrency) seviyesi arttıkça sistemin yanıt verme süresi kabul edilemez düzeylere ulaşır.

İşte bu performans kaybını önlemek için kilitiz (lock-free) veya parçalı kilit mekanizmaları kullanan veri yapılarına ihtiyaç duyarız. Verilerin hem her an sıralı kalmasını hem de yüzlerce thread’in aynı anda minimum bekleme süresiyle (low latency) bu veriye erişmesini istediğimizde ConcurrentSkipListMap ideal bir çözüm olarak karşımıza çıkar.

SkipList (Atlamalı Liste) Veri Yapısı Nasıl Çalışır?

ConcurrentSkipListMap yapısını derinden kavramak için altta yatan Atlamalı Liste (SkipList) algoritmasını anlamak gerekir. SkipList, sıralı bir bağlı listenin (linked list) üzerine inşa edilmiş hiyerarşik bir veri yapısıdır. Standart bir bağlı listede bir elemanı aramak $O(n)$ zaman karmaşıklığı gerektirirken, SkipList bu süreyi $O(\log n)$ seviyesine indirir.

Mekanizmanın temelinde “katmanlar” (levels) yer alır. En alt katman (Level 0), tüm elemanları içeren standart sıralı bir bağlı listedir. Üst katmanlar ise alt katmanlardaki elemanların bazılarını atlayarak köprü görevi görür. Arama işlemine en üst katmandan başlanır. Aranılan değer mevcut düğümden büyükse sağa, küçükse bir alt katmana inilir. Bu sayede arama işlemi, dengeli bir arama ağacına (balanced search tree) benzer bir hızla gerçekleşir.

Aşağıdaki metinsel temsil, SkipList katman yapısının nasıl çalıştığını görselleştirmeye yardımcı olur:

Katman 3: [10] ————————-> [50] ————-> null
Katman 2: [10] ———> [30] ———> [50] ————-> null
Katman 1: [10] -> [20] -> [30] -> [40] -> [50] -> [60] —-> null
Katman 0: [10]->[15]->[20]->[25]->[30]->[35]->[40]->[45]->[50]->[55]->[60]->null

SkipList yapısının ağaç yapılarına kıyasla eşzamanlı programlamadaki en büyük avantajı, düğüm ekleme ve silme sırasında karmaşık ağaç döndürme (tree rotation) işlemlerine ihtiyaç duymamasıdır. Düğümlerin hangi katmanlara çıkacağı olasılıksal (probabilistic) bir para atışı mantığıyla belirlenir. Bu durum, veri yapısını kilitiz Compare-And-Swap (CAS) komutları ile güncellenebilir hale getirir. Yani, bir thread bir düğüm eklerken tüm haritayı kilitlemek zorunda kalmaz; sadece ilgili bağlantı noktalarında atomik güncellemeler yapar.

ConcurrentSkipListMap Nedir ve Ne Zaman Kullanılmalıdır?

ConcurrentSkipListMap, Java’nın ConcurrentNavigableMap arayüzünü (interface) uygulayan ve SkipList algoritmasını temel alan thread-safe bir harita sınıfıdır. Anahtarlar (keys) doğal sıralamalarına (Natural Ordering) göre veya oluşturma anında verilen bir Comparator nesnesine göre sıralanır.

Bu veri yapısının ana karakteristiği, kilitiz (lock-free) CAS algoritmaları sayesinde son derece yüksek okunabilirlik ve ölçeklenebilirlik sunmasıdır. Okuma işlemleri (get) asla kilit tutmaz ve yazma işlemleri (put/remove) ile çakışmadan son derece hızlı bir şekilde tamamlanır.

Aşağıdaki durumlar gerçekleştiğinde ConcurrentSkipListMap kullanımı tercih edilmelidir:

  • Verilerin sürekli olarak anahtar sırasına göre işlenmesi gerekiyorsa.
  • Aralık sorguları (range queries) veya alt harita (subMap, headMap, tailMap) işlemleri yoğun olarak yapılıyorsa.
  • Aynı anda çok sayıda thread haritaya okuma ve yazma yapıyorsa.
  • En küçük (firstEntry) veya en büyük (lastEntry) elemanlara anlık olarak erişilmesi gerekiyorsa.

Ancak, sadece anahtar-değer eşleşmesi gerekiyorsa ve sıralama önemsizse, ConcurrentHashMap kullanmak daha doğru bir tercihtir. Çünkü ConcurrentHashMap, $O(1)$ zaman karmaşıklığı ile bellek ve hız açısından daha verimlidir.

Sıfırdan Uygulama: ConcurrentSkipListMap Temel Metotları Nasıl Kullanılır?

ConcurrentSkipListMap kullanımını anlamak için temel metotları içeren pratik bir kod örneği inceleyelim. Kod içerisinde atomik ekleme, aralık alma ve uç değerleri çekme gibi kritik işlevler yer almaktadır.

import java.util.concurrent.ConcurrentSkipListMap;
import java.util.Map;

public class BasicSkipListExample {
    public static void main(String[] args) {
        // Doğal sıralamaya (küçükten büyüğe) sahip harita oluşturma
        ConcurrentSkipListMap<Integer, String> sensorData = new ConcurrentSkipListMap<>();

        // Atomik veri ekleme işlemleri
        sensorData.put(1001, "Sıcaklık: 22C");
        sensorData.put(1005, "Sıcaklık: 25C");
        sensorData.put(1003, "Sıcaklık: 24C");
        sensorData.putIfAbsent(1002, "Sıcaklık: 23C");

        // Verilerin sıralı basılması
        System.out.println("Tüm Sensör Verileri (Sıralı):");
        for (Map.Entry<Integer, String> entry : sensorData.entrySet()) {
            System.out.println("Zaman: " + entry.getKey() + " -> " + entry.getValue());
        }

        // Uç değerlere erişim (O(1) veya O(log n) karmaşıklık)
        System.out.println("\nEn Erken Veri: " + sensorData.firstEntry());
        System.out.println("En Son Veri: " + sensorData.lastEntry());

        // Aralık Sorgusu (Range Query): 1002 dahil, 1005 hariç
        System.out.println("\n1002 ile 1005 Arasındaki Veriler:");
        Map<Integer, String> subMap = sensorData.subMap(1002, true, 1005, false);
        subMap.forEach((key, value) -> System.out.println(key + " : " + value));
    }
}

Yukarıdaki örnekte görüldüğü üzere, elemanlar hangi sıra ile eklenirse eklensin harita onları otomatik olarak anahtara göre sıralar. subMap metodu ile alınan görünüm (view) ana haritaya canlı olarak bağlıdır. Yani alt haritada yapılan bir değişiklik ana haritada da güncellenir. Bu durum, veri kopyalama maliyetini ortadan kaldırır.

Gerçek Dünya Senaryosu: Yüksek Frekanslı Borsa Emir Defteri Yapımı

Machine coding mülakatlarında en çok sorulan sistem tasarımı örneklerinden biri, finansal piyasalardaki Borsa Emir Defteri (Order Book) altyapısıdır. Borsa İstanbul veya herhangi bir kripto para borsasında, alış (Bid) ve satış (Ask) emirlerinin anlık olarak eşleştirilmesi gerekir.

Bir emir defterinde kurallar nettir:

  • Alış Emirleri (Bids): En yüksek fiyatlı teklif en üstte yer almalıdır (Azalan sıralama).
  • Satış Emirleri (Asks): En düşük fiyatlı teklif en üstte yer almalıdır (Artan sıralama).
  • Eşzamanlılık: Yüzlerce tüccar (trader) aynı anda emir verebilir veya iptal edebilir. Sistem kilitlenmemelidir.

Aşağıdaki uygulamada, ConcurrentSkipListMap kullanarak eşzamanlı ve thread-safe bir Borsa Emir Defteri motoru inşa edeceğiz.

import java.util.Collections;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.Map;

public class BorsaEmirDefteri {

    // Alış teklifleri: En yüksek fiyat en önde olmalı (Ters sıralama)
    private final ConcurrentSkipListMap<Double, Integer> bidOrders = 
        new ConcurrentSkipListMap<>(Collections.reverseOrder());

    // Satış teklifleri: En düşük fiyat en önde olmalı (Doğal sıralama)
    private final ConcurrentSkipListMap<Double, Integer> askOrders = 
        new ConcurrentSkipListMap<>();

    // Alış emri ekleme veya güncelleme
    public void addBid(double price, int quantity) {
        bidOrders.merge(price, quantity, Integer::sum);
    }

    // Satış emri ekleme veya güncelleme
    public void addAsk(double price, int quantity) {
        askOrders.merge(price, quantity, Integer::sum);
    }

    // Piyasa eşleşmesi kontrolü
    public void matchOrders() {
        while (!bidOrders.isEmpty() && !askOrders.isEmpty()) {
            Map.Entry<Double, Integer> highestBid = bidOrders.firstEntry();
            Map.Entry<Double, Integer> lowestAsk = askOrders.firstEntry();

            // Eğer en yüksek alış fiyatı, en düşük satış fiyatına eşit veya büyükse eşleşme gerçekleşir
            if (highestBid.getKey() >= lowestAsk.getKey()) {
                int tradeQuantity = Math.min(highestBid.getValue(), lowestAsk.getValue());
                
                System.out.printf("EŞLEŞME GERÇEKLEŞTİ: %.2f TL fiyatından %d adet işlem yapıldı.%n", 
                    lowestAsk.getKey(), tradeQuantity);

                // Miktarları güncelle
                updateOrderMap(bidOrders, highestBid.getKey(), tradeQuantity);
                updateOrderMap(askOrders, lowestAsk.getKey(), tradeQuantity);
            } else {
                break; // Eşleşme yoksa döngüden çık
            }
        }
    }

    private void updateOrderMap(ConcurrentSkipListMap<Double, Integer> map, double price, int quantity) {
        map.compute(price, (k, v) -> {
            if (v == null) return null;
            int remaining = v - quantity;
            return remaining > 0 ? remaining : null; // Miktar 0 ise kaydı sil
        });
    }

    public static void main(String[] args) throws InterruptedException {
        BorsaEmirDefteri emirDefteri = new BorsaEmirDefteri();

        // Farklı thread'lerin piyasaya emir girdiğini simüle edelim
        Thread trader1 = new Thread(() -> {
            emirDefteri.addBid(315.50, 100);
            emirDefteri.addBid(316.00, 50);
        });

        Thread trader2 = new Thread(() -> {
            emirDefteri.addAsk(315.00, 75);
            emirDefteri.addAsk(317.00, 200);
        });

        trader1.start();
        trader2.start();

        trader1.join();
        trader2.join();

        // Emirleri eşleştir
        emirDefteri.matchOrders();
    }
}

Bu vaka analizinde, merge ve compute gibi atomik metotlar sayesinde thread’lerin birbirini engellemeden veri eklemesini sağladık. Aynı zamanda firstEntry() metodu ile piyasadaki en iyi fiyat seviyelerine anında eriştik. Yapılan bu mimari tasarım, yüksek frekanslı işlem (HFT) sistemlerinin temel prensiplerini yansıtır.

ConcurrentSkipListMap ve ConcurrentHashMap Arasındaki Farklar Nelerdir?

Uygulama mimarisini tasarlarken iki yapı arasında doğru seçimi yapmak performans açısından kritik rol oynar. Karar vermenizi kolaylaştırmak için aşağıdaki karşılaştırma tablosunu inceleyebilirsiniz:

Özellik ConcurrentHashMap ConcurrentSkipListMap
Veri Yapısı Hash Table + Node/Tree SkipList (Atlamalı Liste)
Sıralama (Ordering) Sırasız (Unordered) Sıralı (Sorted)
Arama Karmaşıklığı $O(1)$ ortalama $O(\log n)$
Ekleme / Silme $O(1)$ ortalama $O(\log n)$
Aralık Sorgusu (Range Search) Yavaş (Tüm haritayı tarar) Çok Hızlı (subMap, headMap)
Null Key / Value Desteği Desteklemez Desteklemez
Bellek Tüketimi (Memory Overhead) Düşük Daha Yüksek (Katman işaretçileri)

Tablodan da anlaşılacağı üzere, eğer anahtar sıralamasına ihtiyacınız yoksa ConcurrentHashMap her zaman daha hızlıdır. Ancak sıralama, en alt/en üst değer bulma ve aralık filtreleme zorunlu ise tercih kesinlikle ConcurrentSkipListMap olmalıdır.

Performans Optimize Etme Yöntemleri ve Püf Noktaları

ConcurrentSkipListMap kullanırken maksimum performans elde etmek ve yazılımınızın kaynaklarını verimli kullanmak için şu noktalara dikkat etmelisiniz:

İlk olarak, yineleyicilerin (Iterators) davranışını iyi anlamak gerekir. Bu sınıfın sunduğu iterator nesneleri “weakly consistent” (zayıf tutarlı) bir yapıya sahiptir. Yani bir thread haritayı gezerken (iterate ederken), başka bir thread haritaya yeni bir eleman eklerse ConcurrentModificationException hatası fırlatılmaz. Gezinti anındaki veriler işlenir, ancak yeni eklenen verilerin görülüp görülmeyeceği garanti edilmez. Bu durum uygulamanızın patlamasını önler fakat veri tutarlılığı tasarımında göz önünde bulundurulmalıdır.

İkinci olarak, size() metodunun maliyetine dikkat edilmelidir. Standart koleksiyonların aksine ConcurrentSkipListMap içinde eleman sayısı sabit bir değişkende tutulmaz. Çünkü anlık değişken tutmak eşzamanlı yazma performansını düşürür. Bu nedenle size() metodunu çağırdığınızda yapı tüm liste üzerinden bir sayım gerçekleştirir ve bu işlem $O(n)$ zaman alır. Haritanın boş olup olmadığını kontrol etmek için size() == 0 yerine mutlaka isEmpty() metodunu kullanmalısınız.

Son olarak, özel bir Comparator kullanıyorsanız, bu karşılaştırıcının Comparable arayüzü ile tam bir tutarlılık (consistent with equals) sergilediğinden emin olun. Eğer karşılaştırıcı iki farklı anahtar için 0 sonucu döndürürse, harita bu iki anahtarı aynı kabul edecek ve yeni veri eskisin üzerine yazılacaktır.

Sonuç ve Özet

Java’da eşzamanlı programlama yaparken verilerin sıralı tutulması gereken senaryolarda ConcurrentSkipListMap benzersiz bir çözümdür. Kilitiz SkipList mimarisi sayesinde, yüksek iş parçacıklı sistemlerde thread’lerin birbiriyle yarış durumuna (race condition) girmesini engeller ve $O(\log n)$ performans garantisi sunar.

Machine coding sınavlarında ve ölçeklenebilir arka plan (backend) sistemlerinde bu yapıyı tercih ederek, hem kod kalitenizi artırabilir hem de karmaşık kilitleme mekanizmalarıyla uğraşmadan thread-safe sistemler inşa edebilirsiniz.

Sıkça Sorulan Sorular (SSS)

1. ConcurrentSkipListMap null değer kabul eder mi?

Hayır, ConcurrentSkipListMap ne null anahtara (key) ne de null değere (value) izin verir. Null eklemeye çalıştığınızda uygulama NullPointerException fırlatır. Bu kural, eşzamanlı işlemlerde null dönen bir yanıtın “anahtar yok mu” yoksa “değer mi null” belirsizliğini çözmek için konulmuştur.

2. ConcurrentSkipListMap neden TreeMap’ten daha fazla bellek tüketir?

SkipList yapısı arama hızını $O(\log n)$ seviyesine çekebilmek için düğümleri birden fazla katmanda (level) kopyalar veya işaretçilerini (pointer) tutar. Bu ek katman bağlantıları, bellekte TreeMap nesnesine kıyasla bir miktar daha fazla yer kaplamasına neden olur.

3. size() metodu neden yavaştır?

Çok izlekli bir ortamda anlık eleman sayısını sabit tutmak (örneğin bir atomic counter ile) tüm yazma işlemlerinin o counter üzerinde kilitlenmesine sebep olur. Bu performans darboğazını engellemek için ConcurrentSkipListMap sayaç tutmaz. size() çağrıldığında o anki düğümleri baştan sona sayar. Bu yüzden boyut kontrolü için isEmpty() tercih edilmelidir.

4. ConcurrentSkipListSet ile ConcurrentSkipListMap arasındaki ilişki nedir?

Tıpkı TreeSet sınıfının arkasında bir TreeMap barındırması gibi, ConcurrentSkipListSet sınıfı da kendi içerisinde bir ConcurrentSkipListMap örneği (instance) kullanır. Set yapısı, map yapısının değer kısmına dummy (kukla) bir nesne koyarak sadece anahtar sıralamasını utilize eder.

#Java #Concurrency #MachineCoding #MultiThreading #JavaDeveloper

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Mobil sürümden çık