ThreadLocal فئة أدوات شائعة الاستخدام ضمن حزمة java.lang في JDK، وتوفر وظيفة سياق الخيط. ولعل أشهر استخداماتها تخزين معلومات أساسية مثل سياق المستخدم بعد تسجيل الدخول.
يمكن لسياق الخيط أيضًا حل كثير من المشكلات التي تبدو معقدة بطريقة أنيقة، مثل:مشكلة تكامل إطارَي spring-mybatis:
عند التطوير اعتمادًا على منظومة spring، نعهد عادةً بإدارة المعاملات إلى spring، ونضبط تفعيل المعاملة ومستوى عزلها وغير ذلك من المعلومات باستخدام التعليمة التوضيحية transactional. كما أننا لا نتعامل مع قاعدة البيانات مباشرةً عبر jdbc، بل نصل إليها من خلال إطار mybatis. إن spring وmybatis إطاران مستقلان تمامًا، لكن عند استخدامهما يبدو أن mybatis يستطيع الحصول مباشرةً على اتصال قاعدة البيانات connection الذي أنشأه spring واستخدامه، ولا نحتاج عمليًا إلى فعل شيء سوى إضافة حزمة jar المسماة mybatis-spring-boot-stater.
يكمن مفتاح تنفيذ هذا التصميم الأنيق في ThreadLocal، ومبدؤه العام هو أن حزمة stater jar ستُدرج لنا في النهاية MybatisAutoConfiguration ويُحقن عبر SPI في حاوية bean الخاصة بخدمتنا، في MybatisAutoConfiguration تم إعداده في mybatis المطلوب SqlSessionFactory، مع حقن ما يخص spring نفسه من SpringManagedTransactionFactory(تُنفذ TransactionFactory، ومصنع المعاملات آلية توسعة يوفرها mybatis، تتيح تفويض إدارة المعاملات إلى جهة أخرى).
مسار التنفيذ العام هو أن الدالة الموسومة بالتعليق التوضيحي transactional تخضع في النهاية لوكيل AOP، وتحصل فئة الوكيل ضمن الإشعار السابق للتنفيذ على connection من dataSource، ثم تضعه في ThreadLocal (في الواقع، ما يوضع في ThreadLocal هو map، مفتاحه dataSource وقيمته connectionHolder الذي يعادل connection)، وعندما ينفذ mybatis لاحقًا أوامر sql، فإنه يبدأ بالمرور عبر TransactionFactory الحصول على connection، لأن ما حُقن في هذه اللحظة هو TransactionFactory نعم SpringManagedTransactionFactory، لذا يُستخرج connection الموافق لـ dataSource من ThreadLocal.
مقدمة مبسطة عن آلية عمل ThreadLocal:
ThreadLocalMap هو جدول تجزئة يستخدم الاستكشاف الخطي لحل التصادمات، وينشئ كل خيط تنفيذ نسخة منه، ويحتوي كل كائن ThreadLocal على واحد threadLocalHashCode يُستخدم لحساب مفتاح التجزئة.
بالمقارنة مع HashMap،ThreadLocalMap قد يكون اختيار الاستكشاف الخطي بدلًا من التسلسل بالسلاسل لمعالجة التصادمات قائمًا على الاعتبارات التالية:
1، اعتبارات الذاكرة، إذ إن اختلاف سيناريوهات الاستخدام يجعل حجم البيانات في ThreadLocalMap عادةً أصغر بكثير منه في HashMap، وعندما يكون حجم البيانات صغيرًا، تستهلك طريقة السلاسل المنفصلة مساحة إضافية كبيرة للمؤشرات
2، الكفاءة، استنادًا إلى 1، يمكن عند التعامل مع أعداد صغيرة استخدام المساحة المُوفَّرة لإنشاء جدول تجزئة بمعامل تحميل أصغر، مما يقلل التصادمات ويحسّن متوسط سرعة البحث

المشكلة
A في سيناريوهات المهام التي تعتمد على مجمّعات الخيوط، توجد مشكلة نقل السياق، ولا سيما في نموذج برمجة الويب القائم على Tomcat. فبما أن Tomcat نفسه يستخدم مجمّع خيوط لمعالجة تفاعلات الطلب والاستجابة بين الخادم والعميل، يضطر كل تطبيق ويب إلى معالجة مسألة النقل الصحيح لسياق الخيط يدويًا.
B مشكلة الكفاءة، إذا زاد عدد كائنات ThreadLocal في التطبيق وتضخم جدول التجزئة، فستنخفض سرعة البحث
بالنسبة إلى المشكلة A، فإن الحلول الشائعة متقاربة إلى حد كبير؛ لذا سأورد مباشرة الشيفرة المستخدمة في نظامنا
ينطوي هذا الحل على مشكلتين:
1、إذا أُضيف لاحقًا ThreadLocal جديد يحتاج إلى النقل بصورة صحيحة، فلا بد من تعديل الشيفرة وإضافته إلى منطق wrap؛ وأي إغفال سيؤدي إلى خلل
2、في بعض الحالات قد ينفّذ المهمةَ خيطُ التنفيذ الذي قدّمها بنفسه، مثل:
عندما يمتلئ مجمع الخيوط وتكون خاصية مجمع الخيوط المسماة
RejectedExecutionHandlerالمستخدَم هوCallerRunsPolicy، تُنفذ المهمة المقدمة إلى مجمع الخيوط مباشرةً في الخيط الذي قدمها، وعندها يؤدي منطق التنظيف في النهاية إلى فقدان سياق ذلك الخيطوبالمثل، باستخدام
ForkJoinPool(بما يشمل التنفيذ المتوازيStreamوCompletableFuture، ويستخدم في الطبقة الأساسيةForkJoinPool)من الحالات، فإن ما يُوسّع هوForkJoinTaskسيُنفَّذ مباشرةً في خيط تقديم المهمة. وستؤدي عملية التنظيف النهائية أيضًا إلى فقدان سياق خيط التقديم
الحل:
المشكلة 1:لو كانت الفئة ThreadLocal تدعم واجهة API تتيح معرفة العدد الإجمالي لكائنات ThreadLocal الموجودة حاليًا في التطبيق، لكان الحل سهلًا، لكنها للأسف لا تدعم ذلك. وإذا أردنا فرض هذا الحل، فيمكن استخدام الانعكاس للوصول إلى ما داخل كائن thread من inheritableThreadLocals(أي إنه ThreadLocalMap)، ثم يمكن الحصول على جميع أزواج المفاتيح والقيم في threadlocal عبر اجتياز جدول التجزئة واستخراج كل عناصر entry. وثمة نهج آخر هو إضافة طبقة تغليف حول ThreadLocal في JDK لإضافة الوظائف المذكورة أعلاه.
المشكلة 2:يمكن حلّها بتحسين منطق wrap:عند بدء تشغيل الخيط، يُسترجع سياق الخيط الحالي ويُحفظ مؤقتًا، ثم يُستبدل سياق هذا الخيط بسياق الخيط الذي أرسل المهمة. وقبل انتهاء تشغيل الخيط، لا يُمسح السياق، بل تُستخدم البيانات المحفوظة مؤقتًا لاستعادة سياقه.
transmittable-thread-local
يوجد بالفعل حل جاهز، ويقوم مبدأ تنفيذه على الاتجاهين المذكورين أعلاه، وقد أُطلق على هذه العملية اسم CRR(capture/replay/restore) نمط。
هناك ثلاث طرق للاستخدام:
1 ### التعديل Runnable و 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 تزيين مجمّع الخيوط
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 ### استخدام Java Agent لتعديل JDK صنف تنفيذ مجمع الخيوط
// ## 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 هي أنه لا يتدخل في شيفرة التطبيق، أما عيوبه فأرى أن أكبر مشكلة هي احتمال وقوع حادث إذا نسيت إضافة المعاملات عند تشغيل JVM.
لنلقِ نظرة سريعة على منطق الشيفرة المصدرية
بالنسبة إلى المشكلة B، لا تُؤخذ عادةً في الحسبان عند تطوير التطبيقات، لأن الحساسية تجاه الكفاءة الزمنية ليست عالية إلى هذا الحد. لكن في تطوير بعض البرمجيات الوسيطة منخفضة المستوى، قد يحظى تحسين الأداء باهتمام أكبر. فمثلًا، اختار netty أن ينفّذ بنفسه FastThreadLocal ليحل محل ThreadLocal في JDK، مع توفير ما يرافقه من FastThreadLocalThread، في FastThreadLocalThread و FastThreadLocal عند استخدامهما معًا، يمكن دائمًا ضمان تعقيد زمني للبحث قدره O(1). ويقوم هذا الحل على مبادلة المساحة بالزمن؛ ولتجنب تراجع كفاءة الاستعلام عند حدوث تصادمات التجزئة في جدول التجزئة، تُنشأ مصفوفة طويلة بما يكفي، وكل FastThreadLocal يُخصص لكل منها فهرس فريد في المصفوفة عند الإنشاء، وكأن لكل شخص خانته الخاصة، وعند الاستعلام يكفي استخدام فهرسه للوصول إلى العنصر مباشرة من المصفوفة.
سأورد هنا الشيفرة المصدرية بإيجاز