Bu yazıda Java dünyasında sıkça kullanıldığına veya bahsedildiğine şahit olduğumuz ThreadLocal sınıfını inceleyeceğiz. ThreadLocal sınıfı belirlediğimiz nesnelerin sadece aynı thread tarafından erişilebilir olmasını sağlar. Bu sayede thread safe olmayan nesneleri thread safe kullanmış oluruz. Bir ThreadLocal nesnesi içerisine yazdığımız nesne, ThreadLocal‘e yazan thread tarafından çalıştırılan tüm methodlar tarafından okunabilir olacaktır. Gelin bu güçlü aracı avantajları ve dezavantajları ile inceleyelim.

İlk olarak ThreadLocal teknik olarak nedir ona bakalım. Java’da her yaratılan Thread nesnesi içerisinde o Thread‘e ait ThreadLocal‘lerin tutulduğu bir map bulunmaktadır. İki farklı thread aynı ThreadLocal nesnesine eriştikleri zaman, ThreadLocal nesnesi değerini o an kendisine erişen thread’in içerisindeki Map‘den okur veya yazar. Bu sayede iki farklı Thread aynı ThreadLocal nesnesi üzerinden farklı değerlere ulaşırlar.

Nasıl Tanımlanmalı?

new anahtar kelimesi ile yarattığımız her bir yeni ThreadLocal nesnesi Thread içerisindeki Map’de farklı bir key olarak tutulmaktadır. Bu sebeple belirli bir amaç için kullanacağımız ThreadLocal nesnesinin sadece tek bir instance’ının olduğundan emin olmalıyız.

Bunun en kolay yolu da static final olarak tanımlamaktır. Bu sayede oluşturacağımız ThreadLocal nesnesinin aynı ClassLoader içerisinde sadece tek bir instance’ının olacağını garanti etmiş oluruz.

Nasıl Kullanılır?

Bu kısımda ThreadLocal üzerindeki method ve constructor’ları inceleyelim.

Boş ThreadLocal Oluşturma

İçinde herhangi bir değer taşımayan (null value) bir ThreadLocal, doğrudan boş constructor kullanılarak üretilebilir. Bu ThreadLocal nesnesine erişen her bir Thread get methodundan null değerini alacaktır.

Bu yazıdaki örneklerde çoğunlukla Java’da thread-safe olmayan SimpleDateFormat sınıfını ThreadLocal içerisinde kullanacağım.

private final static ThreadLocal<SimpleDateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<>();

ThreadLocal Değerini Atamak

Boş veya dolu fark etmeksizin bir ThreadLocal nesnesine set methodunu kullanarak yeni bir değer atayabilirsiniz. Atadığınız değer sadece o değeri atadığınız Thread için geçerli olacaktır. Diğer Thread’lerin atayacakları veya okuyacakları değerler tamamen mevcut Thread’inkinden bağımsız olacaktır.

@Test
void shouldSetValueToThreadLocal() {
    final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
    threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

ThreadLocal Değerini Okumak

Bir ThreadLocal üzerinde mevcut Thread için tutulmakta olan değeri almak için get methodu kullanılır. Mevcut Thread tarafından değer atanmamış veya başlangıç değeri bulunmayan bir ThreadLocal üzerindeki get methodunu çağırmak null değerini dönecektir.

@Test
void shouldSetAndGetThreadLocalValue() {
    final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();

    SimpleDateFormat currentValue = threadLocal.get();
    assertNull(currentValue);

    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    threadLocal.set(simpleDateFormat);

    currentValue = threadLocal.get();
    assertNotNull(currentValue);
    assertSame(simpleDateFormat, currentValue);
}

Başlangıç Değeri ile Oluşturma

ThreadLocal nesneleri bir başlangıç değerine sahip olabilir. Bu durumda set çağırmadan get methodunu ilk kez çağıran her bir Thread belirlenen initialValue değerini alırlar. Bu ilk değer her Thread için farklı olabileceği gibi aynı değer de verilebilir. Bu kısım tamamen kullanıcıya kalmış. Aşağıda başlangıç değeri ile bir ThreadLocal nesnesi oluşturmanın iki farklı yolunu görelim.

initialValue Methodunun Override Edilmesi

ThreadLocal sınıfı üzerinde bulunan initialValue methodunun görevi bir ThreadLocal nesnesi için ilk değerin üretilmesidir. Bu method ThreadLocal sınıfı içerisinde return null şeklinde yazılmıştır.

Aşağıdaki şekilde initialValue methodunun override edildiği ThreadLocal’den extend eden bir anonymous class yazarak kullanacağımız ThreadLocal için bir ilk değer belirleyebiliyoruz.

@Test
void shouldGetInitialValueWhenInitialValueMethodIsOverridden() {
    final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    final SimpleDateFormat currentValue = threadLocal.get();
    assertNotNull(currentValue);
    assertEquals("yyyy-MM-dd HH:mm:ss", currentValue.toPattern());
}
Static withInitial Methodu İle Başlangıç Değerinin Verilmesi

Java 8 ile birlikte gelen static withInitial methodunu kullanarak başlangıç değeri olan bir ThreadLocal nesnesi yaratabiliriz.

initialValue methodunu override ederken anonymous bir sınıf yaratmıştık. Bu yöntemde ise ThreadLocal içerisinde tanımlı ThreadLocal’den extend eden SuppliedThreadLocal isimli bir private inner class dönülüyor.

Bu method parametre olarak bir Supplier alıyor ve başlangıç değerine ihtiyacı olan her Thread tarafından bu Supplier çağırılıyor.

@Test
void shouldGetInitialValueWhenThreadLocalIsConstructedUsingWithInitialMethod() {
    final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(
            () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    final SimpleDateFormat currentValue = threadLocal.get();
    assertNotNull(currentValue);
    assertEquals("yyyy-MM-dd HH:mm:ss", currentValue.toPattern());
}

ThreadLocal Değerinin Temizlenmesi

Bir ThreadLocal’e bir Thread tarafından atanan değer yine aynı Thread tarafından temizlenebilir. Bu işlem remove methodu kullanılarak yapılır.

remove işlemi sonrası get methodu null dönecektir.

@Test
void shouldRemoveThreadLocalValue() {
    final ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();

    final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    threadLocal.set(simpleDateFormat);

    SimpleDateFormat currentValue = threadLocal.get();
    assertNotNull(currentValue);
    assertSame(simpleDateFormat, currentValue);

    threadLocal.remove();
    currentValue = threadLocal.get();
    assertNull(currentValue);
}

InheritableThreadLocal Sınıfı

Bu sınıf da bir ThreadLocal çeşidi olmakta olup, özelliği şudur: Bu türdeki ThreadLocal’in üzerindeki mevcut Thread’e ait değer, çocuk Thread’lere aktarılır. Çocuk Thread nedir? Mevcut Thread tarafından oluşturulmuş Thread’ler, mevcut thread’in çocuklarıdır.

Aşağıdaki örnekte oluşturulan yeni Thread’de parent Thread’in ThreadLocal değerinin aktarıldığını görebiliyoruz.

@Test
void inheritableThreadLocalShouldInheritValuesToChild() throws InterruptedException {
    final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    threadLocal.set("Demo Value");

    final AtomicReference<String> valueInThread = new AtomicReference<>();

    final Thread thread = new Thread(() -> {
        valueInThread.set(threadLocal.get());
    });

    thread.start();
    thread.join();

    assertEquals("Demo Value", valueInThread.get());
}

Kullanım Alanları

Bir değerin bir Thread içerisinde her yerden ulaşılabilir olmasını isteyebileceğimiz bir çok durumda ThreadLocal kullanılabilir. Gördüğüm birkaç örneği aşağıda paylaşıyorum. Sizler de bildiğiniz ThreadLocal kullanım alanlarını yorum olarak paylaşabilirseniz birlikte daha fazla şey öğrenebiliriz.

Kullanıcının Tanınması (Authentication)

Bir kullanıcının yaptığı işlem eğer sisteme girişinden çıkışına kadar hep aynı Thread içerisinde akışına devam ediyorsa, kullanıcıya ait giriş bilgileri ThreadLocal üzerinde tutulabilir.

Mesela bir HTTP server uygulamasında kullanıcının Cookie’sinde gönderdiği bir bilgi okunup bir cache server veya veritabanı üzerinden kullanıcı bilgisi çekiliyorsa, bu değer ThreadLocal üzerinde tutularak çağırılan tüm methodlar tarafından kullanıcı bilgisinin erişilebilir olması sağlanabilir.

Örnek olarak Spring Security bir takım filtreler kullanarak gelen HTTP isteğinin hangi kullanıcı tarafından yapıldığını anlar ve SecurityContextHolder içerisine kullanıcı verisini yazar. Bu sınıf varsayılan olarak ThreadLocal kullanmaktadır. İstek Controller veya servis katmanlarımıza geldiğinde istersek SecurityContextHolder üzerinden kullanıcı bilgisine static methodlar kullanarak ulaşabiliyoruz.

Thread-Safe Olmayan Sınıfların Singleton Gibi Kullanılması

Yeni nesnesinin oluşturulması pahalı olan sınıfları genellikle singleton yapmaya çalışıyoruz. Fakat kullandığımız sınıf thread-safe değilse orada çuvallayabiliriz. Birden fazla thread, thread-safe olmayan singleton bir nesneye eriştiğinde bu nesnenin state’inde bir takım istenmeyen sonuçlar ortaya çıkabilir.

Bu durumda oluşturması maliyetli nesnemizi uygulama genelinde tek instance olan bir singleton olarak tanımlamak yerine, her thread’de bir instance’ı olacak şekilde oluşturabiliriz.

Örnek olarak yukarıdaki örneklerde de kullandığım SimpleDateFormat sınıfını verebilirim. Bu sınıf thread-safe olmadığı için global değişken olarak kullanılamıyor. Onun yerine bu sınıfın ya her methodda yeni bir nesnesinin oluşturulması gerekiyor ya da ThreadLocal üzerinde tutulması gerekiyor.

Bir İşlemi Başından Sonuna Kadar Takip Edebilmek

Eğer bir işlem başladığında ThreadLocal’e bir değer girersek ve bittiğinde o değeri silersek, işlem boyunca aynı thread içerisinde olup biten tüm işlemlerde bu değeri görebiliriz.

Bu sayede bir method yapmakta olduğu işi hangi context içerisinde yaptığını bilmek isterse ThreadLocal’den alabilir.

Örnek olarak logging kütüphanelerindeki MDC yapısı verilebilir. MDC yapısını kullanarak bir işin başından sonuna kadar loglara aynı değerin yazılmasını sağlayabiliriz. Örneğin sisteme gelen her istek için unique bir TransactionId üretiyoruz ve bu istek sırasında yazılan tüm loglarda bu TransactionId değerini görüyoruz.

MDC ile log takibi yapmak ile ilgili daha önce yazmış olduğum bir yazı için tıklayın: Slf4j: MDC Kullanarak Log Takibini Kolaylaştırma

Dikkat!

Global değişken kullanmanın yazılımın tasarımı açısından kötü olduğu konusunda birçok yazılım geliştirici uzun zamandır hemfikir. ThreadLocal kavramı da global variable kavramına çok yakın olduğu için zorunlu kalınmadıkça kullanılmamalıdır. Mümkün olduğunda ThreadLocal kullanmak yerine methodlara ilgili değerler parametre geçilebilir, cache kütüphaneleri kullanılabilir veya çeşitli dependency injection yöntemleri kullanılabilir.

Yazılım geliştirirken fark ettiğim problemlerden birisi de ThreadLocal kullanılan sınıfların unit testini yazmanın dependency injection ile yazılmış sınıfların testini yazmaktan daha zor olduğudur.

Ayrıca ThreadLocal’lerin kullanıldıkları context’lere göre dikkat edilmesi gereken bazı hususlar ortaya çıkmaktadır.

Bunlardan en yaygınları memory leak’ler ve yanlış değerlerin yanlış Thread’lerde bulunması durumlarıdır.

Öncelikle memory leak oluşma durumuna bir bakalım.

ClassLoader Leak

Application Server dediğimiz tomcat, weblogic ve jboss gibi uygulamalara birden fazla farklı uygulamayı yükleyebiliyoruz. Application Server’lar kendileri de bir java process’i olduğu için bir Application Server’a yüklediğimiz tüm bu uygulamalar tek bir JVM içerisinde çalışıyorlar.

Application Server ise uygulamaların arasındaki ayrımı sağlayabilmek için hepsine ayrı ClassLoader nesnesi yaratır. Bu uygulamalar ve onların bağımlı olduğu kütüphaneler bu ClassLoader tarafından JVM’e yüklenir.

Belirli bir uygulamayı kapatıp açarken veya yeni bir versiyonunu canlıya alırken Application Server ilgili uygulamayı kapatır ve bağlı olduğu ClassLoader da Garbage Collector tarafından yok edilir.

Application Server’larda gelen HTTP istekleri işlemek için kullandığı bir Thread Pool bulunur. Bu ThreadPool’daki Thread’ler genellikle Application Server ayakta olduğu sürece yaşarlar. Eğer Application Server’a yüklü bir uygulama HTTP isteğini yorumlarken ThreadLocal’e bir şey yazarsa ve bu isteğe cevap verirken ThreadLocal’e yazdığı değerleri silmezse bu değer Application Server’a ait olan Thread’in üzerinde kalır.

Application Server üzerindeki bir uygulamayı kapatırken eğer Application Server’ın ThreadLocal’i üzerinde kapatılmakta olan uygulanın ClassLoader’ı tarafından JVM’e yüklenmiş bir sınıfa ait bir nesne kaldıysa, bu nesne tüm ClassLoader’ın Garbage Collector tarafından silinmesini engeller. Bir ClassLoader belki binlerce sınıfı memory’ye yükleyeceği için Garbage Collect olmaması çok büyük problemdir. Uygulama Application Server üzerinde tekrar ayağa kaldırılırken yepyeni bir ClassLoader oluşturulur ve yeniden tüm class’lar load edilir. Bu şekilde uygulamalar aç/kapa yaptıkça Application Server üzerinde ClassLoader’lar birikecektir ve nihayetinde java.lang.OutOfMemoryError: PermGen space hatası alınacaktır. Bu noktadan sonra artık Application Server’ın restart edilmesi gerekecektir.

Eğer Application Server Thread’lerinin üzerine bir ThreadLocal yazılıyorsa bunun iş biterken silindiğinden emin olunması gerekir.

Bu sebeple ThreadLocal yazıldıktan sonraki işlem try/finally bloğu ile yazılmalı. Ben aşağıdaki gibi yazıyorum genellikle:

public static final ThreadLocal<UserContext> USER_CONTEXT_THREAD_LOCAL = new ThreadLocal<>();

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
  throws IOException, ServletException {
  
  try {
    final UserContext userContext = new UserContext();
    // do some work to fill userContext...
    // other stuff...
    // things...
    USER_CONTEXT_THREAD_LOCAL.set(userContext);
    chain.doFilter(request, response);
  } finally {
    USER_CONTEXT_THREAD_LOCAL.remove();
  }
}

ThreadLocal Değerinin İstenmeyen Yerlere Gitmesi

Yukarıdaki ClassLoader leak konusuna çok benziyor. Eğer bir ThreadLocal set ediyorsak çok büyük ihtimalle aynı method içerisinde onu silme fırsatımız olacaktır.

Örneğin bir ExecutorService kullanıyorsak ve ona çalıştırması için verdiğimiz Runnable sınıfında ThreadLocal kullanımı yapıyorsak, aynı Runnable içerisinde bu ThreadLocal’i silmeliyiz. Yoksa ExecutorService aynı thread’leri kullanarak başka işleri de koşacağı için ThreadLocal’e set ettiğimiz değer alakasız başka işler tarafından erişilebilir olacaktır.

Aynı zamanda executorService’e submit ettiğimiz Runnable ayrı bir thread tarafından çalıştırılacağı için, executorService.submit() methodunu çağırdığımız thread’deki ThreadLocal’leri görmeyecektir.

Aşağıdaki kodda executorService’e bir task verilmeden önce oradaki iş parçacığı ile paylaşmak istediğimiz ThreadLocal değeri önce bir değişkene aktarılıyor. Daha sonra executorService’in içerisinde işi çalıştıran Thread’in ThreadLocal’ine tekrar set ediliyor. Daha önemlisi, executorService’deki işimiz bittiğinde finally bloğu içerisinde ThreadLocal’i tekrar temizliyoruz. Bu sayede ExecutorService tarafından yapılan diğer işlere fazladan bilgi vermemiş oluyoruz.

private void executeBatchTask() {
    final UserContext userContext = USER_CONTEXT_THREAD_LOCAL.get();
    
    executorService.submit(() -> {
        try {
            USER_CONTEXT_THREAD_LOCAL.set(userContext);
            // Do work in executor thread 
        } finally {
            USER_CONTEXT_THREAD_LOCAL.remove();
        }
    });
}

Bitiriyoruz

ThreadLocal hakkında söylenebilecek her şey bu kadar değildir elbette. Elimden geldiğince hakkında bildiklerimi anlatmaya çalıştım.

Her ne kadar hatalı kullandığımız bir ThreadLocal ile production sistemlerinde başımızı belaya sokabilecek olsak da, meşhur büyüğümüz Joshua Bloch array ile de bunu yapabilirsiniz demiş :)

Bu sebeple Joshua Bloch’un bu konudaki aşağıdaki meşhur alıntısıyla bitiriyorum:

“Can you cause unintended object retention with thread locals? Sure you can. But you can do this with arrays too. That doesn’t mean that thread locals (or arrays) are bad things. Merely that you have to use them with some care. The use of thread pools demands extreme care. Sloppy use of thread pools in combination with sloppy use of thread locals can cause unintended object retention, as has been noted in many places. But placing the blame on thread locals is unwarranted.” - Joshua Bloch

Kaynaklar