ThreadLocal, JDK'nin java.lang paketinde sık kullanılan bir yardımcı sınıftır,iş parçacığı bağlamı işlevini sağlar,en yaygın kullanım alanı muhtemelen oturum açıldıktan sonra kullanıcı bağlamı gibi önemli bilgileri ThreadLocal'da saklamaktır。
İş parçacığı bağlamı, karmaşık görünen pek çok sorunu da zarifçe çözebilir; örneğin spring-mybatis çerçevelerinin entegrasyonu sorunu:
Spring ekosistemine dayalı geliştirme yaparken genellikle işlem yönetimini Spring'e devreder, transactional anotasyonu aracılığıyla işlemlerin başlatılmasını ve işlem yalıtım düzeyi gibi bilgileri yapılandırırız. Veritabanını da doğrudan JDBC üzerinden değil, MyBatis çerçevesi aracılığıyla kullanırız. Spring ve MyBatis birbirinden tamamen bağımsız iki çerçevedir; ancak bunları kullanırken Spring'in açtığı veritabanı connection nesnesi sanki MyBatis tarafından doğrudan alınıp kullanılabiliyormuş gibi görünür. Neredeyse hiçbir şey yapmamız gerekmez; bir mybatis-spring-boot-stater JAR paketi eklemek yeterlidir.
Bu zarif tasarımın uygulanmasındaki kilit nokta ThreadLocal'dır. Genel çalışma mantığı, stater jar paketinin sonunda bizim için bir MybatisAutoConfiguration ve SPI aracılığıyla servisimizin bean kapsayıcısına enjekte edilir, şu sırada MybatisAutoConfiguration içinde yapılandırıldı mybatis Gerekli olan SqlSessionFactory, ayrıca Spring'in kendi SpringManagedTransactionFactory(uygulayan TransactionFactory,işlem fabrikası, mybatis'in sunduğu bir genişletme mekanizmasıdır; işlem yönetiminin başka bir bileşene devredilmesini sağlar)。
Genel yürütme akışı şöyledir: transactional anotasyonlu yöntem sonunda bir AOP vekili tarafından sarmalanır; vekil sınıf, ön bildirimde dataSource üzerinden bir connection alır ve sonunda bunu bir ThreadLocal içine koyar (aslında ThreadLocal içinde bir map tutulur; key, dataSource; value ise connection ile eşdeğer olan connectionHolder'dır); daha sonra mybatis SQL çalıştırırken önce şu yoldan TransactionFactory connection'ı alma, çünkü bu sırada enjekte edilen TransactionFactory Evet SpringManagedTransactionFactory,bu nedenle ilgili dataSource'un connection nesnesi ThreadLocal'dan alınır.
ThreadLocal'ın temel çalışma ilkesine kısa bir giriş:
ThreadLocalMap çakışmaları doğrusal sondalama yöntemiyle çözen bir karma tablosudur; her iş parçacığı için bir kopya oluşturulur ve her ThreadLocal nesnesinde bir threadLocalHashCode Özet anahtarını hesaplamak için kullanılır.
HashMap ile karşılaştıralım,ThreadLocalMap Çakışma çözümü için zincirleme yerine doğrusal sondalamanın seçilmesi aşağıdaki değerlendirmelere dayanıyor olabilir:
1、Bellek açısından, kullanım senaryoları farklı olduğu için ThreadLocalMap'teki veri miktarı genellikle HashMap'tekinden çok daha azdır; veri miktarı küçük olduğunda zincirleme yöntemi, işaretçiler nedeniyle çok daha fazla alan tüketir
2、Verimlilik; 1 temelinde, eleman sayısı az olduğunda tasarruf edilen alan, daha düşük yük faktörüne sahip bir karma tablo oluşturmak için kullanılabilir; böylece çakışmalar azalır ve ortalama arama hızı artar

Sorun
A İş parçacığı havuzuna dayalı görev senaryolarında bağlam aktarımı sorunu vardır. Özellikle Tomcat tabanlı web programlama modelinde, Tomcat sunucu ile istemci arasındaki istek-yanıt etkileşimlerini işlemek için iş parçacığı havuzu kullandığından, her web uygulaması iş parçacığı bağlamının doğru aktarılmasını elle yönetmek zorunda kalır.
B Verimlilik sorunu,uygulamada çok fazla ThreadLocal bulunması nedeniyle karma tablosu büyürse,arama hızı düşer
A sorunu için yaygın çözüm yöntemleri oldukça benzerdir; doğrudan sistemimizdeki kodu paylaşayım
Bu çözümün iki sorunu var:
1、İleride doğru biçimde aktarılması gereken yeni bir ThreadLocal eklenirse kodu değiştirip bu yeni ThreadLocal'ı wrap mantığına eklemek gerekir; atlanırsa hata oluşur
2、Bazı durumlarda görevi gönderen iş parçacığı görevi kendisi çalıştırabilir, örneğin:
İş parçacığı havuzu dolduğunda ve iş parçacığı havuzunun
RejectedExecutionHandlerkullanılanCallerRunsPolicyolduğunda, iş parçacığı havuzuna gönderilen görevler doğrudan gönderen iş parçacığında yürütülür; bu durumda sondaki temizleme mantığı, gönderen iş parçacığının bağlamının kaybolmasına yol açarBenzer şekilde, kullanarak
ForkJoinPool(Paralel yürütme dâhilStreamileCompletableFuture,altyapıdaForkJoinPool)durumunda, açılanForkJoinTaskgörevi gönderen iş parçacığında doğrudan çalıştırılır. Sondaki temizleme işlemi de görevi gönderen iş parçacığının bağlamını kaybetmesine yol açar
Çözüm:
Sorun 1:ThreadLocal sınıfı, uygulamada o anda toplam kaç ThreadLocal nesnesi bulunduğunu öğrenebileceğimiz bir API sunsaydı sorun kolayca çözülürdü; ne yazık ki sunmuyor. Bunu zorlayarak yapmak istersek yansıma yoluyla thread nesnesindeki inheritableThreadLocals(yani bir ThreadLocalMap)özelliğini kullanıp ardından karma tablosunu dolaşarak tüm entry öğelerini çıkardığınızda bütün threadlocal anahtar-değer çiftlerini elde edebilirsiniz. Bir başka yaklaşım ise JDK'nın ThreadLocal sınıfını bir katman daha sarmalayarak yukarıdaki işlevleri eklemektir。
Sorun 2:wrap mantığı geliştirilerek çözülebilir: İş parçacığı çalışmaya başladığında önce mevcut iş parçacığının bağlamı alınarak geçici olarak saklanır, ardından bu iş parçacığının bağlamı görevi gönderen iş parçacığının bağlamıyla değiştirilir. İş parçacığı sona ermeden önce bağlam temizlenmez; bunun yerine geçici olarak saklanan veriler kullanılarak iş parçacığının özgün bağlamı geri yüklenir.
transmittable-thread-local
Bu iş için zaten hazır bir çözüm var; çalışma ilkesi yukarıdaki iki yönden yola çıkıyor ve bu sürece bir de isim verilmiş CRR(capture/replay/restore) modu。
Üç kullanım biçimi vardır:
1 ### Niteleme Runnable ve Callable
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================
// Task中可以读取,值是"value-set-in-parent"
String value = context.get();2 İş parçacığı havuzunu dekore etme
ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();3 ### Kullanım Java Agent nitelemek için JDK iş parçacığı havuzu uygulama sınıfı
// ## 1. 框架上层逻辑,后续流程框架调用业务 ##
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("value-set-in-parent");
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// ## 3. 框架下层逻辑 ##
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();java agent yönteminin avantajı, uygulama koduna müdahale etmemesidir. Dezavantajına gelince, bence en büyük sorun, jvm başlatılırken parametre eklemeyi unutursanız bir aksaklık yaşanabilmesidir.
Kaynak kodun mantığına kısaca bakalım
B sorununa gelince, uygulama geliştirmede zaman verimliliğine karşı hassasiyet genellikle o kadar yüksek olmadığından bu konu çoğunlukla dikkate alınmaz. Ancak bazı alt düzey ara katman yazılımlarının geliştirilmesinde performans optimizasyonuna daha fazla önem verilebilir. Örneğin netty, kendi uygulamasını geliştirmeyi seçmiştir:FastThreadLocal JDK'nin ThreadLocal'ının yerini alır ve beraberinde uyumlu bir FastThreadLocalThread,şurada FastThreadLocalThread ve FastThreadLocal birlikte kullanıldığında O(1) arama zamanı karmaşıklığı her zaman garanti edilebilir. Çözümün temel ilkesi, zamandan kazanmak için daha fazla bellek kullanmaktır. Hash tablosundaki hash çakışmalarının sorgu verimliliğini düşürmesini önlemek için yeterince uzun bir dizi oluşturulur; her bir FastThreadLocal Oluşturulurken her birine benzersiz bir dizi indeksi atanır,bu, herkesin kendine ait bir yeri olması gibidir,sorgularken kendi index değeriyle doğrudan diziden almak yeterlidir。
Kaynak kodunu kısaca paylaşayım