Eğitim Portalı/Java/Singleton Sınıf
Java01-java/39-singleton

Singleton Sınıf

Bazı nesnelerden uygulamada tek bir tane olmalıdır: bir yapılandırma yöneticisi, bir log sistemi, bir veritabanı bağlantı havuzu. Bunlardan birden çok oluşturmak hem kaynak israfı hem de tutarsızlık (farklı ayar kopya…

Singleton Sınıf

Bazı nesnelerden uygulamada tek bir tane olmalıdır: bir yapılandırma yöneticisi, bir log sistemi, bir veritabanı bağlantı havuzu. Bunlardan birden çok oluşturmak hem kaynak israfı hem de tutarsızlık (farklı ayar kopyaları) demektir. Singleton deseni, bir sınıfın tek örneğini garanti eder ve ona global, kontrollü bir erişim noktası sunar. Tasarım desenleri bölümünde kısaca gördük; burada tüm varyantlarını ve tuzaklarını derinlemesine ele alıyoruz.

Singleton'ın iki kuralı

  1. Dışarıdan new engellenir: Constructor private yapılır.
  2. Tek erişim noktası: Sınıf, kendi tek örneğini static bir metot/alanla sunar.
class Ayarlar {
    private static final Ayarlar TEK = new Ayarlar();
    private Ayarlar() {}                       // new engellendi
    public static Ayarlar getInstance() { return TEK; }
}

Eager vs Lazy

  • Eager (erken): Örnek, sınıf yüklenirken hemen oluşturulur. Basit ve thread-safe; ama hiç kullanılmasa bile oluşturulur.
  • Lazy (tembel): Örnek ilk getInstance() çağrısında oluşturulur. Kaynak tasarrufu sağlar ama çok thread'li ortamda dikkat ister.

Örnek 1 (./Ornek1.java) eager bir Ayarlar ve basit (tek-thread) lazy bir Logger gösterir.

Thread güvenliği: lazy singleton'ın tuzağı

Basit lazy singleton çok thread'de risklidir: iki thread aynı anda if (ornek == null) koşulunu geçip iki ayrı örnek yaratabilir. Çözümler:

Double-checked locking

volatile bir alan + iki kez null kontrolü (biri kilitsiz hızlı, biri kilit içinde):

private static volatile Baglanti ornek;
public static Baglanti getInstance() {
    if (ornek == null) {                  // hızlı yol (kilitsiz)
        synchronized (Baglanti.class) {
            if (ornek == null) ornek = new Baglanti();  // güvenli yaratım
        }
    }
    return ornek;
}

Holder idiom (en zarif)

İç static bir sınıf, JVM'in sınıf-yükleme garantisini kullanır: örnek, Holder ilk erişildiğinde thread-safe ve kilitsiz oluşur:

private static class Holder { static final Onbellek ORNEK = new Onbellek(); }
public static Onbellek getInstance() { return Holder.ORNEK; }

Örnek 2 (./Ornek2.java) 20 thread'le tek örnek garantisini test eder ve her iki yöntemi gösterir.

Enum singleton (önerilen)

Joshua Bloch'un Effective Java'da önerdiği en güvenli yol: tek elemanlı bir enum.

public enum VeriTabani {
    INSTANCE;
    public void sorgu(String sql) { ... }
}
// kullanım: VeriTabani.INSTANCE.sorgu(...)

Avantajları (Örnek 3, ./Ornek3.java):

  • Thread-safe + lazy, ekstra kod yok (JVM enum'ları güvenle yükler).
  • Serileştirmeye karşı güvenli: Normal singleton'lar deserialize edilince ikinci bir örnek oluşabilir; enum'da bu olmaz.
  • Reflection'a karşı güvenli: Enum constructor'ı reflection ile çağrılamaz, ikinci örnek yaratılamaz.

Tek kısıtı: bir sınıfı uzatamaz (enum zaten Enum'u uzatır). Çoğu durumda en iyi seçimdir.

Singleton'a eleştiriler ve modern bakış

Singleton güçlü ama dikkatli kullanılmalı:

  • Global durum yaratır; gizli bağımlılıklar ve test zorluğu doğurabilir.
  • Test edilebilirlik: Singleton'a sıkıca bağlı kod, testte sahteyle değiştirilemez.

Modern çözüm: dependency injection. Spring'de bir bean'i @Component/@Bean yaparsın; Spring onu zaten singleton kapsamında yönetir ve sana enjekte eder — sen elle singleton yazmazsın. Yani "tek örnek" ihtiyacını framework, gizli global durum yaratmadan, test edilebilir biçimde çözer.

Özet

Singleton'ın iki kuralını; eager/lazy ayrımını (Örnek 1); lazy'nin thread tuzağını ve çözümlerini — double-checked locking, holder idiom (Örnek 2) — ve en güvenli yol olan enum singleton'ı (Örnek 3) öğrendik; singleton'ın global-durum eleştirisini ve modern alternatifi (dependency injection) ele aldık. Sırada, metot çağrılarının hangi anda çözüldüğü: statik ve dinamik bağlama.

Kod Örnekleri(3)

Ornek1

çalıştırılabilir
Ornek1.java
1// Ornek1: Singleton — tek örnek garantisi (eager ve lazy).
2// Çalıştırma: java Ornek1.java
3public class Ornek1 {
4
5    public static void main(String[] args) {
6        // Nereden istersek aynı nesneyi alırız.
7        Ayarlar a1 = Ayarlar.getInstance();
8        Ayarlar a2 = Ayarlar.getInstance();
9        a1.ayarla("tema", "koyu");
10        System.out.println("a1 == a2 ? " + (a1 == a2) + "  (tek örnek)");
11        System.out.println("a2'den tema: " + a2.oku("tema"));
12
13        // Lazy singleton: ilk getInstance çağrısına kadar OLUŞTURULMAZ.
14        System.out.println("\nLazy singleton ilk erişim:");
15        Logger.getInstance().log("uygulama başladı");
16
17        System.out.println("""
18
19                --- Singleton (eager vs lazy) ---
20                Amaç: bir sınıfın TÜM uygulamada TEK örneği olsun (yapılandırma, log, bağlantı havuzu).
21                Anahtar: private constructor (dışarıdan 'new' engellenir) + tek statik erişim noktası.
22                EAGER : örnek sınıf yüklenince hemen oluşturulur (basit, thread-safe).
23                LAZY  : örnek ilk istendiğinde oluşturulur (kaynak tasarrufu; ama thread güvenliği konusu var -> Örnek 2).""");
24    }
25}
26
27// EAGER singleton: örnek sınıf yüklenirken oluşturulur. Basit ve thread-safe.
28class Ayarlar {
29    private static final Ayarlar TEK = new Ayarlar();
30    private final java.util.Map<String, String> map = new java.util.HashMap<>();
31    private Ayarlar() {}                          // dışarıdan new engellendi
32    public static Ayarlar getInstance() { return TEK; }
33    public void ayarla(String k, String v) { map.put(k, v); }
34    public String oku(String k) { return map.get(k); }
35}
36
37// LAZY singleton (tek thread için basit hali): ilk çağrıda oluşturulur.
38class Logger {
39    private static Logger ornek;                 // başta null
40    private Logger() { System.out.println("(Logger oluşturuldu)"); }
41    public static Logger getInstance() {
42        if (ornek == null) ornek = new Logger();  // ilk çağrıda yarat
43        return ornek;
44    }
45    public void log(String m) { System.out.println("[LOG] " + m); }
46}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek2

çalıştırılabilir
Ornek2.java
1// Ornek2: Thread-safe lazy singleton — double-checked locking ve holder idiom.
2// Çalıştırma: java Ornek2.java
3import java.util.concurrent.ConcurrentHashMap;
4import java.util.Map;
5import java.util.Set;
6import java.util.concurrent.CopyOnWriteArraySet;
7
8public class Ornek2 {
9
10    public static void main(String[] args) throws InterruptedException {
11        // Birden çok thread aynı anda getInstance çağırsa bile TEK örnek olmalı.
12        Set<Integer> kimlikler = new CopyOnWriteArraySet<>();
13        Runnable is = () -> kimlikler.add(System.identityHashCode(Baglanti.getInstance()));
14
15        Thread[] t = new Thread[20];
16        for (int i = 0; i < t.length; i++) { t[i] = new Thread(is); t[i].start(); }
17        for (Thread x : t) x.join();
18
19        System.out.println("20 thread'in gördüğü FARKLI örnek sayısı: " + kimlikler.size() + " (1 olmalı)");
20
21        // Holder idiom singleton da tek örnek verir:
22        System.out.println("Holder singleton aynı mı? "
23                + (Onbellek.getInstance() == Onbellek.getInstance()));
24        Onbellek.getInstance().koy("k", "v");
25        System.out.println("Holder'dan: " + Onbellek.getInstance().al("k"));
26
27        System.out.println("""
28
29                --- Thread-safe lazy singleton ---
30                Tek thread'li lazy (Örnek 1) çok thread'de RISKLIDIR: ikisi aynı anda 'null' görüp İKİ örnek yaratabilir.
31                Çözüm 1 — double-checked locking: volatile alan + iki kez null kontrolü + senkron blok.
32                Çözüm 2 — holder idiom (EN ZARİF): iç static sınıf, JVM'in sınıf yükleme garantisini kullanır;
33                          örnek ilk erişimde, thread-safe ve kilitsiz oluşur.""");
34    }
35}
36
37// Double-checked locking: volatile + çift kontrol.
38class Baglanti {
39    private static volatile Baglanti ornek;        // volatile: görünürlük garantisi
40    private Baglanti() {}
41    public static Baglanti getInstance() {
42        if (ornek == null) {                       // 1. kontrol (kilitsiz, hızlı)
43            synchronized (Baglanti.class) {
44                if (ornek == null) {               // 2. kontrol (kilit içinde)
45                    ornek = new Baglanti();
46                }
47            }
48        }
49        return ornek;
50    }
51}
52
53// Holder idiom: en temiz lazy + thread-safe yöntem.
54class Onbellek {
55    private final Map<String, String> map = new ConcurrentHashMap<>();
56    private Onbellek() {}
57    private static class Holder { static final Onbellek ORNEK = new Onbellek(); }
58    public static Onbellek getInstance() { return Holder.ORNEK; } // Holder ilk erişimde yüklenir
59    public void koy(String k, String v) { map.put(k, v); }
60    public String al(String k) { return map.get(k); }
61}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek3

çalıştırılabilir
Ornek3.java
1// Ornek3: Enum singleton — en güvenli ve en kısa singleton (önerilen).
2// Çalıştırma: java Ornek3.java
3public class Ornek3 {
4
5    public static void main(String[] args) {
6        // Enum sabiti tek örnektir; erişim her zaman aynı nesneyi verir.
7        VeriTabani.INSTANCE.baglan();
8        VeriTabani.INSTANCE.sorgu("SELECT * FROM urun");
9        System.out.println("Aynı örnek mi? " + (VeriTabani.INSTANCE == VeriTabani.INSTANCE));
10
11        System.out.println("""
12
13                --- Enum singleton (önerilen) ---
14                'public enum X { INSTANCE; ... }' en güvenli singleton'dır (Joshua Bloch önerisi).
15                Avantajları:
16                  - Thread-safe ve lazy (JVM enum'ları güvenle yükler), ekstra kod yok.
17                  - Serileştirmeye karşı GÜVENLİ: deserialize tek örneği bozmaz.
18                  - Reflection ile ikinci örnek YARATILAMAZ (enum constructor çağrılamaz).
19                Dezavantaj: bir sınıfı uzatamaz (enum zaten Enum'u uzatır). Çoğu durumda en iyi seçimdir.
20                Not: Spring kullanıyorsan bean'ler zaten singleton'dır; bu deseni elle yazmana gerek kalmaz.""");
21    }
22}
23
24enum VeriTabani {
25    INSTANCE;                                  // tek örnek — işte bu kadar
26
27    private int sorguSayisi = 0;
28    VeriTabani() { System.out.println("(VeriTabani başlatıldı)"); }
29
30    public void baglan() { System.out.println("veritabanına bağlanıldı"); }
31    public void sorgu(String sql) {
32        sorguSayisi++;
33        System.out.println("sorgu #" + sorguSayisi + ": " + sql);
34    }
35}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.