Gereksinimler ve Ön Hazırlık
Bu eğitimdeki kod örneklerini çalıştırabilmek için sisteminizde aşağıdakilerin kurulu olması gerekmektedir:
- Java Development Kit (JDK) 17 veya üzeri: Güncel performans iyileştirmeleri ve dil özellikleri için en az JDK 17 önerilir.
- IDE: IntelliJ IDEA, Eclipse veya VS Code gibi Java destekli bir geliştirme ortamı.
- Maven veya Gradle: Proje bağımlılıklarını yönetmek için standart bir yapılandırma aracı.
Projenizde herhangi bir dış kütüphane kullanmanıza gerek yoktur; ConcurrentHashMap, Java'nın standart kütüphanesinin (JDK) bir parçasıdır.
ConcurrentHashMap Nedir ve Neden Kullanılır?
ConcurrentHashMap, eş zamanlı erişimlerde veri bütünlüğünü korumak için tasarlanmış, yüksek performanslı bir harita (map) veri yapısıdır. Geleneksel Collections.synchronizedMap yöntemine kıyasla, tüm haritayı kilitlemek yerine sadece ilgili "bucket" (kova) üzerinde kilitleme yaparak performansı artırır.
Önbellek (cache) sistemlerinde neden tercih edildiğini aşağıdaki tabloda özetleyelim:
| Özellik | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| Thread Güvenliği | Hayır | Evet (Tüm yapı) | Evet (Segment bazlı) |
| Performans | Yüksek | Düşük | Çok Yüksek |
| Null Anahtar/Değer | İzin verir | İzin vermez | İzin vermez |
Adım Adım Thread Güvenli Önbellek Tasarımı
Önbellek yapımızı oluştururken temel amacımız, veriyi bir kez hesaplayıp veya veritabanından çekip, sonraki isteklerde doğrudan bellekten hızlıca sunmaktır. İlk olarak, basit bir önbellek sınıfı iskeleti oluşturalım.
import java.util.concurrent.ConcurrentHashMap;
public class BasitOnbellek {
private final ConcurrentHashMap cache = new ConcurrentHashMap();
public V get(K key) {
return cache.get(key);
}
public void put(K key, V value) {
cache.put(key, value);
}
}
Yukarıdaki kod, temel bir depolama yapısı sunar. Ancak gerçek bir önbellek senaryosunda, verinin var olup olmadığını kontrol edip yoksa ekleme yapmamız gerekir. Burada computeIfAbsent metodu devreye girer.
computeIfAbsent Metodu ile Atomik İşlemler
computeIfAbsent, anahtar önbellekte yoksa belirtilen fonksiyonu çalıştırıp sonucu önbelleğe ekler. Bu işlem atomiktir; yani aynı anda iki farklı thread aynı anahtar için işlem yapmaya çalışsa bile, fonksiyon sadece bir kez çalıştırılır.
import java.util.function.Function;
public V getOrCompute(K key, Function mappingFunction) {
return cache.computeIfAbsent(key, mappingFunction);
}
Bu yöntem, veritabanına gereksiz sorgu atılmasını engelleyerek sistemin yükünü hafifletir. Özellikle yoğun trafik altında "thundering herd" (sürü etkisi) problemini çözmek için en etkili yoldur.
Önbellek İçin Zaman Aşımı (TTL) Yönetimi
Gerçek dünya uygulamalarında veriler sonsuza kadar önbellekte kalmamalıdır. Veri tutarlılığı için belirli bir süre sonra veriyi geçersiz kılmak (eviction) gerekir. ConcurrentHashMap doğrudan TTL (Time To Live) desteği sunmaz, bu yüzden bir sarmalayıcı (wrapper) nesne kullanmalıyız.
import java.time.Instant;
class CacheEntry {
private final V value;
private final long expiryTime;
public CacheEntry(V value, long ttlMillis) {
this.value = value;
this.expiryTime = System.currentTimeMillis() + ttlMillis;
}
public boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
public V getValue() { return value; }
}
Bu sınıf ile artık verinin süresinin dolup dolmadığını kontrol edebiliriz. Zaman aşımı yönetimi için periyodik bir temizleyici (cleaner) thread veya erişim anında kontrol mekanizması kurmanız gerekir.
Kritik Uyarı: Üretim ortamlarında (production) önbellek boyutu sınırsız olmamalıdır. Eğer önbellek boyutunu kontrol etmezseniz, bellek sızıntısı (memory leak) veOutOfMemoryErrorhatası ile karşılaşmanız kaçınılmazdır. Mutlaka bir "eviction policy" (örneğin LRU - Least Recently Used) uygulayın veyaCaffeinegibi profesyonel kütüphaneleri değerlendirin.
Thread Güvenli Önbellek Testi
Yazdığımız önbelleğin thread güvenli olduğunu doğrulamak için çok sayıda iş parçacığı ile aynı anda veri ekleme ve okuma testi yapmalıyız.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestOnbellek {
public static void main(String[] args) {
BasitOnbellek onbellek = new BasitOnbellek();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
onbellek.put("key", "deger");
System.out.println("Veri okundu: " + onbellek.get("key"));
});
}
executor.shutdown();
}
}
Bu test, 10 farklı iş parçacığının aynı anda 100 kez işlem yapmasını sağlar. ConcurrentHashMap sayesinde hiçbir veri kaybı veya çakışma yaşanmadan işlem tamamlanacaktır.
Sıkça Sorulan Sorular
ConcurrentHashMap tüm senaryolar için en iyi çözüm müdür?
Hayır. Eğer çok karmaşık TTL kuralları, bellek yönetimi ve istatistiksel veriler (isabet oranı vb.) gerekiyorsa, Caffeine veya Google Guava Cache gibi kütüphaneler daha uygundur. Ancak basit ve hızlı bir çözüm için en iyi seçenektir.
computeIfAbsent içindeki fonksiyon uzun sürerse ne olur?
computeIfAbsent çalışırken ilgili anahtar kilitli kalır. Bu nedenle, fonksiyon içerisinde uzun süren veritabanı veya ağ işlemleri yapıyorsanız, sistemin kilitlenmesine neden olabilirsiniz. Bu işlemleri asenkron hale getirmek veya dikkatli yönetmek gerekir.
Bellek yönetimini nasıl yapmalıyım?
ConcurrentHashMap kendiliğinden eleman silmez. remove metodunu manuel çağırmalı veya LinkedHashMap ile birleştirerek bir LRU (En az kullanılanı sil) mekanizması kurmalısınız.
ConcurrentHashMap null değerleri neden desteklemez?
get metodunun null döndürmesi, anahtarın haritada olup olmadığını veya değerin gerçekten null olup olmadığını belirsizleştirir. Bu belirsizliği önlemek için null değerlere izin verilmez.
Hangi Java sürümünde bu yapıyı kullanmalıyım?
Java 8 ve sonrası, ConcurrentHashMap için büyük performans iyileştirmeleri içerir. 2026 yılı itibarıyla en az Java 17 veya 21 kullanmanız, hem performans hem de güvenlik açısından en doğru yaklaşımdır.
Önbellek Stratejilerinde Performans Optimizasyonu ve İleri İpuçları
ConcurrentHashMap ile kurduğunuz önbellek yapısı, yüksek trafikli uygulamalarda oldukça başarılı sonuçlar verir. Ancak, sistemin ölçeklenebilirliğini artırmak ve darboğazları önlemek için bazı ileri düzey stratejileri göz önünde bulundurmalısınız. Özellikle computeIfAbsent kullanımı sırasında, hesaplama maliyeti yüksek olan işlemlerin tüm haritayı kilitlememesi için dikkatli olunmalıdır.
Performans İçin İleri Düzey İpuçları
- Geçici Nesne Oluşturumunu Azaltın: Önbelleğe alınan nesnelerin boyutu büyükse, Garbage Collector (GC) üzerindeki yükü azaltmak için nesne havuzları (object pooling) kullanmayı düşünebilirsiniz.
- Paralellik Seviyesi (Concurrency Level): Java 8 ve sonrasında
ConcurrentHashMap, dahili olarak paralellik seviyesini dinamik olarak yönetir. Bu yüzden manuel olarak paralellik seviyesi belirlemekten kaçının; JVM'in çalışma zamanı optimizasyonlarına güvenin. - Lambda İfadelerinde Hata Yönetimi:
computeIfAbsentiçerisindeki fonksiyon bloğu bir hata fırlatırsa, bu hata çağrıyı yapan thread'e yansır. Bu durumu yönetmek için mutlaka try-catch blokları ile sarmalama yapın.
// Hata yönetimi içeren güvenli computeIfAbsent kullanımı
public String getCachedData(String key) {
return cache.computeIfAbsent(key, k -> {
try {
return fetchDataFromDatabase(k);
} catch (Exception e) {
logger.error("Veri çekme hatası: " + k, e);
return "DEFAULT_VALUE"; // Hata durumunda varsayılan değer
}
});
}
Alternatif Kütüphaneler ve Ne Zaman Geçiş Yapılmalı?
ConcurrentHashMap temel bir ihtiyaç için mükemmeldir; ancak önbellek yönetimi karmaşıklaştığında (örneğin; otomatik tahliye politikaları, istatistik takibi, ağırlık tabanlı sınırlama), özel kütüphanelere geçiş yapmak daha sürdürülebilirdir. Aşağıdaki tablo, kendi çözümünüz ile profesyonel kütüphaneler arasındaki farkları özetler.
| Özellik | ConcurrentHashMap | Caffeine / Guava Cache |
|---|---|---|
| Otomatik Tahliye (Eviction) | Yok (Manuel gerekir) | Var (LRU, LFU, Zaman bazlı) |
| İstatistikler | Yok | Var (Hit/Miss oranı) |
| Karmaşıklık | Düşük | Orta |
| Bellek Yönetimi | Manuel | Soft/Weak Reference desteği |
Caffeine Kütüphanesi ile Geçiş Senaryosu
Eğer uygulamanızda "bellek dolduğunda en az kullanılanı sil" (LRU) gibi bir mantığa ihtiyaç duyuyorsanız, ConcurrentHashMap yerine Caffeine kullanmak kodunuzu sadeleştirir. Caffeine, yüksek performanslı bir ConcurrentHashMap türevi üzerine inşa edilmiştir ve thread güvenliğini optimize edilmiş bir şekilde sunar.
// Caffeine ile otomatik zaman aşımı ve boyut sınırlaması
Cache cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
// Kullanımı
String value = cache.get(key, k -> fetchDataFromDatabase(k));
Sonuç olarak, ConcurrentHashMap ile kendi önbelleğinizi yazmak, bağımlılıkları (dependency) azaltmak ve sistemin çalışma prensibini tam olarak anlamak için harika bir yoldur. Ancak, kurumsal ölçekte bir uygulama geliştiriyorsanız, Caffeine gibi kütüphanelerin sunduğu otomatik temizleme ve izleme özelliklerini değerlendirmek, uzun vadede bakım maliyetinizi ciddi oranda düşürecektir.
Sonuç
Java ile ConcurrentHashMap kullanarak thread güvenli bir önbellek oluşturmak, sistem performansını artırmak için güçlü bir yöntemdir. Bu makalede atomik işlemlerin nasıl yapılacağını, zaman aşımı yönetiminin mantığını ve thread güvenliğinin neden önemli olduğunu ele aldık.
Kod Güvenliği Sorumluluk Reddi: Bu rehberdeki kod örnekleri eğitim amaçlıdır. Ticari uygulamalarınızda kullanmadan önce, özellikle veri boyutu, bellek limitleri ve hata yönetimi (exception handling) konularında kapsamlı testler yapmalı ve güvenlik açıklarına karşı girdi doğrulaması (input validation) uygulamalısınız.
Bir sonraki adım olarak, önbellek boyutunu sınırlayan bir LinkedHashMap entegrasyonu yaparak kendi basit LRU önbelleğinizi geliştirmeyi deneyebilirsiniz.

Yorumlar (17)
Hocam eline koluna sağlık, gayet açıklayıcı ve temiz bir anlatım olmuş. Basit ama etkili bir yöntem.
Bu yöntemi kullanırken thread havuzlarında şişme olmaması için ne gibi önlemler almalı? shutdown hook olayını es mi geçtik?
Haklısın Merve, temiz bir kapatma süreci olmazsa memory leak kaçınılmaz oluyor, özellikle büyük maplerde.
Harika bir kaynak, yer imlerine ekledim. Java concurrancy öğrenen arkadaşlara kesinlikle öneririm.
ConcurrentHashMap'in internal yapısını (segment yapısı vs.) biraz daha açsaydın keşke, sadece kullanıp geçmek biraz yüzeysel kalmış.
Selin, haklısın. Bir sonraki yazımda Java 8 sonrası gelen CAS operasyonları ve internal yapıya derinlemesine gireceğim.
Abi ellerine sağlık, tamda aradığım konuydu. Projeye entegre ettim çalışıyo şuan.
Yazı güzelde başlık biraz iddialı olmuş. Sadece basit bir map değil de biraz daha kompleks senaryolardan bahsetseydin keşke.
Bence giriş seviyesi için gayet yeterli, her şeyi aynı yazıda anlatınca kafa karışıyor sonuçta.
Cok güzel anlatmışsın teşekkürler. Peki bu yöntem spring boot projelerinde @Cacheable ile kıyaslandığında performans olarak nasıl bi fark yaratır?
Fatma, Spring Cache soyutlama katmanı olduğu için kullanımı kolaydır ama ConcurrentHashMap daha düşük seviyeli ve daha az overhead'li bir çözümdür. İhtiyaca göre seçmek lazım.
Hocam güzel yazı olmuş ama kod bloklarında font boyutu biraz ufak kalmış mobilden okurken zorlandım biraz.
Geri bildirimin için teşekkürler Burak, bir sonraki yazıda CSS tarafında düzenleme yapacağım.
Ya ben bu yapıyı büyük verilerde denedim ama TTL yönetimi kısmında biraz takıldım. Cache'i temizlemek için ayrı bir thread mi kurmak lazım yoksa Guava gibi kütüphaneler mi daha mantıklı?
Büyük projelerde Caffeine veya Guava kullanmak daha garanti, kendi implementasyonunla uğraşırken memory leak'e sebep olabilirsin.
Eline sağlık Mert hocam, computeIfAbsent gerçekten hayat kurtarıyor. Senkronizasyon bloklarıyla uğraşmaktan kurtulduk resmen.
Kesinlikle katılıyorum Caner, kodun okunabilirliğini de ciddi oranda artırıyor.
Yorum Yaz