Tasarım Kalıpları – V: Singleton (Tek Nesne) – II ve Double-Checked Locking
Tasarım kalıpları dizisinin bir önceki yazısında, ilk kalıbımız olarak Singleton’u ele almıştık. Aslında Singleton kalıbı başlangıç itibariyle kolay ve kısa olmasına rağmen, üzerinde yapılacak ufak bir oynamayla son derece karmaşık hale gelebilir. Bu yazıda da Singleton tasarım kalıbı üzerine yapılacak bu ufak değişikliğin ne olduğunu ve nasıl sonuçları olduğunu ele alalım.
Singleton kalıbımızın kodu şöyleydi:
package org.javaturk.dp.pattern.gof.creational.singleton; public class Singleton { private static Singleton singleton = new Singleton(); private static int count; private String name; private Singleton() { count++; name = "Singleton" + count; } public static Singleton getInstance() { return singleton; } public void printName() { System.out.println(name); } }
Yukarıdaki örnekteki problem, Singleton sınıfı yüklenirken “private static Singleton singleton = new Singleton();” satırının çalışması ve hiç kullanılmayacak olsa bile Singleton’un tek nesnesini yaratmasıdır. Özellikle bellek açısından ciddi kaynak tüketecek kadar büyük ve karmaşık olan nesnelerin oluşturulmasını, gerçekten erişilinceye kadar geciktirebiliriz. Bu şekilde, bir nesnenin oluşturulmasının istenildiği ana kadar geciktirilmesine, “sonradan yükleme” ya da “lazy loading” denir. Bunun tersi, yani yukarıdaki durum ise “önden yükleme” ya da “eager loading” olarak adlandırılır. Sonradan yüklemeyi kullandığımız durumda şöyle bir yapıya ulaşırız:
package org.javaturk.dp.pattern.gof.creational.singleton; public class LazySingleton { private static LazySingleton singleton; private static int count; private String name; private LazySingleton() { count++; name = "LazySingleton" + count; } public static LazySingleton getInstance() { if(singleton == null) singleton = new LazySingleton(); return singleton; } public void printName() { System.out.println(name); } }
Yukarıdaki sınıftaki singleton nesne, LazySingleton sınıfından üretilmektedir ve koddan da görüldüğü gibi tek nesnenin oluşturulması, “getInstance()” metodunun çağrısına kadar geciktirilmektedir. Bu yüzden tek nesne, bu metot çağrılmadıkça oluşmayacaktır, ilk defa metot çağrıldığında nesne oluşacak, sonraki çağrılarda ise bu meottaki “singleton == null” ifadesi false döneceğinden, tek nesne doğrudan metottan döndürülecektir.
Bu ikinci tip singleton yani LazySingleton, son derece şık duruyor ama çok ciddi bir yanlışı var. LazySingleton, bu haliyle birden fazla kanalın (multi-threaded) olduğu durumlarda tek nesne olmaktan çıkacak, birden fazla nesnesi oluşabilecektir. Yani, birden fazla kanal aynı anda “getInstance()” metodunu çağırırsa, bu kanalların hepsi aynı anda tek nesnenin null olduğunu görüp, birer nesne oluşturulmasına sebep olabilir. Bu durumu görmek için aşağıdaki örneğe bakalım:
package org.javaturk.dp.pattern.gof.creational.singleton; public class ThreadedLazySingleton { private static ThreadedLazySingleton singleton; private static int count; private String name; private ThreadedLazySingleton() { name = "ThreadedLazySingleton" + count; count++; } public static ThreadedLazySingleton getInstance() { if (singleton == null) { try { Thread.currentThread().sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } singleton = new ThreadedLazySingleton(); } return singleton; } public void printName() { System.out.println(name); } }
Birden fazla kanal oluşturan aşağıdaki istemciyi çalıştırırsak göreceğiz ki birden fazla ThreadLazySingleton nesnesi oluşacaktır. (Yukarıdaki örnekte “getInstance()” metodundaki “sleep()” metodu o anda çalışan kanalı 1 ms. uyutmak için konumştur ve tek amacı aynı anda birden fazla kanalın “singleton” nesnesini oluşturması ihtimalini arttırmak içindir.)
package org.javaturk.dp.pattern.gof.creational.singleton; public class ThreadedLazySingletonClient extends Thread { public static void main(String[] args) { for(int i = 0; i < 10; i++){ new ThreadedLazySingletonClient().start(); } } public void run(){ ThreadedLazySingleton ls = ThreadedLazySingleton.getInstance(); ls.printName(); } }
Bu durumu yani birden çok “tek nesne” oluşmasını önlemek için tabi olarak “getInstance()” metodunu synchronized yapmalıyız. Bu metodu “public static synchronzied ThreadedLazySingleton getInstance()” olarak tanımladığımızda, ThreadedLazySingletonClient istemcisi kaç kanal oluşturursa oluştursun, sadece ilk kanal tek nesnenin oluşmasına sebep olacak, diğerleri ancak ilk kanal”getInstance()” metot çağrısını bitirdikten sonra aynı metodu çağırabileceğinden, tek nesne üzerinde null kontrolü artık false dönecektir. Çünkü synchronized anahtar kelimesi, “getInstance()” metodunu thread-safe hale getirecektir.
synchronized anahtar kelimesi ile thread-safe hale getirilen ThreadedLazySingleton sınıfın problemi, “getInstance()” metodunun sürekli olarak synchronized anahtar kelimesinin getirdiği kontrollerden dolayı yavaş çalışmasıdır. Bildiğiniz gibi, Java’da bu anahtar kelimeyi sadece metotlara değil, metot içindeki bloklara da uygulayabiliriz. Yani bu metodu aşağıdaki gibi sadece bir bloğunu thread-safe hale getirerek kullanabiliriz. Thread-safe hale getirdiğimiz ThreadedLazySingleton sınıfın bir metodu olduğundan dışlayıcı kilit (mut-ex lock) olarak ThreadedLazySingleton sınıfın “class” nesnesini kullanmalıyız.
public static ThreadedLazySingleton getInstance() { synchronized(ThreadedLazySingleton.class){ if (singleton == null) { try { Thread.currentThread().sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } singleton = new ThreadedLazySingleton(); } } return singleton; }
Yukarıdaki kod ile aslında çok da fazla bir performans kazanımında bulunmadık çünkü if içindeki “singleton == null” kontrolü, bu metodun her çağrılışında, hem de synchronized olarak çalışıyor. Yani “singleton == null” kontrolünün thread-safe olarak çalışması gereken zaman aslında “singleton” tek nesnesinin oluşturulmasından önce olan zamandır. Bu nesne henüz oluşturulmadan, bütün “singleton == null” kontrolleri thread-safe olarak yapılmalı ama bir defa oluşturulduğunda yani “singleton = new ThreadedLazSingleton()” satırı çalıştığında, artık “singleton == null” kontrolünün synchronzied olarak olmasına gerek yoktur. Buradan şu sonuç çıkar: Eğer tek nesnenin oluşturulmasını sonradan yüklemeyle yapmak ama aynı zamanda performanslı olmasını istiyorsak, “singleton == null” kontrolünü, birisi çok kanallı, diğeri thread-safe halde iki defa yapmalıyız. Bu durumda kodumuz şu hale gelecektir:
package org.javaturk.dp.pattern.gof.creational.singleton; public class ThreadSafeLazySingleton { private static ThreadSafeLazySingleton singleton; private static int count; private String name; private ThreadSafeLazySingleton() { name = "ThreadSafeLazySingleton" + count; count++; } public static ThreadSafeLazySingleton getInstance() { if (singleton == null) { synchronized (ThreadSafeLazySingleton.class) { if (singleton == null) { try { Thread.currentThread().sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } singleton = new ThreadSafeLazySingleton(); } } } return singleton; } public void printName() { System.out.println(name); } }
Bu son durumda istemcimiz benzer şekilde şöyledir:
package org.javaturk.dp.pattern.gof.creational.singleton; public class ThreadSafeLazySingletonClient extends Thread{ public static void main(String[] args) { for(int i = 0; i < 10; i++){ new ThreadSafeLazySingletonClient().start(); } } public void run(){ ThreadSafeLazySingleton ls = ThreadSafeLazySingleton.getInstance(); ls.printName(); } }
Son durum olan ThreadSafeLazySingleton sınıfının “getInstance()” metodunda “singleton == null” kontrolünün iki defa yapılmasından dolayı bu yaklaşıma double- checked locking kalıbı denir.
Double-checked locking, bir kalıp ya da ters-kalıp (anti-pattern) olarak literatüre geçmiştir. Bir kalıptır çünkü çok kanallı hallerde daha etkin yazmamızı sağlar, ters-kalıptır çünkü kodumuzu karmaşıklaştırır ve… Ve’sini bir sonraki yazıda ele alalım 🙂
Toplam görüntülenme sayısı: 2481