ThreadLocal হলো JDK-এর java.lang প্যাকেজের একটি বহুল ব্যবহৃত ইউটিলিটি ক্লাস, যা থ্রেড কনটেক্সটের সুবিধা প্রদান করে। এর সবচেয়ে প্রচলিত ব্যবহার সম্ভবত লগইনের পর ব্যবহারকারীর কনটেক্সটের মতো গুরুত্বপূর্ণ তথ্য সংরক্ষণ করা।
থ্রেড কনটেক্সট দেখতে জটিল এমন অনেক সমস্যারও সুন্দর সমাধান করতে পারে, যেমন: spring-mybatis ফ্রেমওয়ার্কের সমন্বয়ের সমস্যা:
spring ইকোসিস্টেমভিত্তিক উন্নয়নের সময় আমরা সাধারণত ট্রানজ্যাকশন পরিচালনার দায়িত্ব spring-কে দিই। transactional অ্যানোটেশন দিয়ে ট্রানজ্যাকশন চালু করা, তার আইসোলেশন লেভেল ইত্যাদি কনফিগার করি। ডেটাবেসেও সরাসরি jdbc দিয়ে কাজ না করে mybatis ফ্রেমওয়ার্কের মাধ্যমে প্রবেশ করি। spring ও mybatis দুটি সম্পূর্ণ স্বাধীন ফ্রেমওয়ার্ক, অথচ ব্যবহারের সময় মনে হয় spring চালু করা ডেটাবেস connection সরাসরি mybatis পেয়ে ব্যবহার করতে পারে। আমাদের প্রায় কিছুই করতে হয় না; শুধু mybatis-spring-boot-stater-এর একটি jar প্যাকেজ যোগ করলেই চলে।
এই সুন্দর নকশা বাস্তবায়নের মূল চাবিকাঠি হলো ThreadLocal। মোটামুটি নীতিটি হলো, stater jar প্যাকেজ শেষ পর্যন্ত আমাদের জন্য একটি MybatisAutoConfiguration এবং SPI-এর মাধ্যমে আমাদের সার্ভিসের bean কনটেইনারে ইনজেক্ট করা হয়, এ MybatisAutoConfiguration এর মধ্যে কনফিগার করা হয়েছেmybatis প্রয়োজনীয়SqlSessionFactory,এবং Spring-এর নিজস্বটিও ইনজেক্ট করা হয়েছে SpringManagedTransactionFactory(বাস্তবায়ন করেছেTransactionFactory,লেনদেন ফ্যাক্টরি হলো mybatis-এর দেওয়া একটি সম্প্রসারণ ব্যবস্থা, যার মাধ্যমে লেনদেন পরিচালনার দায়িত্ব অন্যের হাতে অর্পণ করা যায়)。
সম্পূর্ণ নির্বাহপ্রক্রিয়াটি হলো, transactional অ্যানোটেশনযুক্ত মেথড শেষ পর্যন্ত AOP প্রক্সির মাধ্যমে পরিচালিত হয়; প্রক্সি ক্লাস পূর্ববর্তী অ্যাডভাইসে dataSource থেকে একটি connection সংগ্রহ করে এবং শেষে একটি ThreadLocal-এ রাখে(আসলে ThreadLocal-এ একটি map রাখা হয়, যার key হলো dataSource এবং value হলো connectionHolder, যা connection-এর সমতুল্য), পরে mybatis যখন sql চালায়, তখন প্রথমে এর মাধ্যমেTransactionFactory connection সংগ্রহ করুন, কারণ এই সময় ইনজেক্ট করাTransactionFactory হ্যাঁSpringManagedTransactionFactory,তাই ThreadLocal থেকে সংশ্লিষ্ট dataSource-এর connection নেওয়া হবে।
ThreadLocal-এর মূল কার্যপদ্ধতির সংক্ষিপ্ত পরিচয়:
ThreadLocalMap এটি মূলত সংঘর্ষ সমাধানে লিনিয়ার প্রোবিং ব্যবহার করা একটি হ্যাশ টেবিল; প্রতিটি থ্রেড এর একটি কপি তৈরি করে, প্রতিটি ThreadLocal অবজেক্টেও একটি থাকেthreadLocalHashCode হ্যাশ কী গণনা করতে ব্যবহৃত হয়।
HashMap-এর সঙ্গে তুলনা করে দেখলে,ThreadLocalMap সংঘর্ষ সমাধানের অ্যালগরিদম হিসেবে চেইনিংয়ের পরিবর্তে লিনিয়ার প্রোবিং বেছে নেওয়ার পেছনে নিম্নলিখিত বিবেচনাগুলি থাকতে পারে:
1、মেমরির বিবেচনা, ব্যবহারের পরিস্থিতি ভিন্ন হওয়ায় ThreadLocalMap-এর ডেটার পরিমাণ সাধারণত HashMap-এর তুলনায় অনেক কম হয়, অল্প ডেটার ক্ষেত্রে সেপারেট চেইনিং পদ্ধতিতে অনেক পয়েন্টারের জন্য অতিরিক্ত স্থান লাগে
2、দক্ষতা, 1-এর ভিত্তিতে, সংখ্যা কম হলে বাঁচানো স্থান দিয়ে কম লোড ফ্যাক্টরের একটি হ্যাশ টেবিল তৈরি করা যায়, সংঘর্ষ কমিয়ে অনুসন্ধানের গড় গতি বাড়ানো যায়

সমস্যা
A থ্রেড পুলভিত্তিক কাজের ক্ষেত্রে কনটেক্সট পৌঁছে দেওয়ার সমস্যা থাকে,বিশেষত Tomcat-ভিত্তিক ওয়েব প্রোগ্রামিং মডেলে,কারণ Tomcat নিজেই সার্ভার ও ক্লায়েন্টের অনুরোধ-প্রতিক্রিয়ার আদানপ্রদান সামলাতে থ্রেড পুল ব্যবহার করে,তাই প্রতিটি ওয়েব অ্যাপ্লিকেশনকেই থ্রেড কনটেক্সট সঠিকভাবে পৌঁছে দেওয়ার বিষয়টি নিজ হাতে সামলাতে হয়।
B দক্ষতার সমস্যা,অ্যাপ্লিকেশনে ThreadLocal অতিরিক্ত হলে,হ্যাশ টেবিল ফুলে ওঠার পর অনুসন্ধানের গতি কমে যায়
সমস্যা A-র ক্ষেত্রে প্রচলিত সমাধানগুলো মোটামুটি একই, সরাসরি আমাদের সিস্টেমের কোডটি দিচ্ছি
এই সমাধানে দুটি সমস্যা আছে:
1、পরে নতুন কোনো ThreadLocal যোগ হলে সেটি সঠিকভাবে পৌঁছে দিতে কোড পরিবর্তন করে wrap-এর যুক্তিতে নতুন ThreadLocal যোগ করতে হবে,একটিও বাদ পড়লে বাগ দেখা দেবে
2、কিছু পরিস্থিতিতে কাজ জমাদানকারী থ্রেড নিজেই তা কার্যকর করতে পারে, যেমন:
যখন থ্রেড পুল পূর্ণ হয়ে যায় এবং থ্রেড পুলের
RejectedExecutionHandlerব্যবহার করা হয়েছেCallerRunsPolicyসময়, থ্রেড পুলে জমা দেওয়া কাজ সরাসরি জমাদানকারী থ্রেডেই কার্যকর হবে; তখন শেষের পরিষ্কার করার যুক্তি জমাদানকারী থ্রেডের কনটেক্সট হারিয়ে ফেলবেএকইভাবে,ব্যবহার করে
ForkJoinPool(সমান্তরাল নির্বাহসহStreamওCompletableFuture,অন্তর্নিহিত স্তরে ব্যবহার করেForkJoinPool)এমন পরিস্থিতিতে, বিস্তৃতForkJoinTaskসরাসরি কাজ জমা দেওয়া থ্রেডেই কার্যকর হবে। শেষে পরিষ্কার করার ফলেও জমা দেওয়া থ্রেডের কনটেক্সট হারিয়ে যাবে
সমাধান:
সমস্যা1:যদি ThreadLocal ক্লাস এমন একটি API সমর্থন করত, যার মাধ্যমে অ্যাপ্লিকেশনে এই মুহূর্তে মোট কতটি ThreadLocal অবজেক্ট রয়েছে জানা যেত, তবে সমস্যাটি সহজেই মিটে যেত। দুর্ভাগ্যবশত, এটি তা সমর্থন করে না। জোর করে করতে চাইলে, রিফ্লেকশনের মাধ্যমে thread অবজেক্টের ভেতরের inheritableThreadLocals(অর্থাৎ একটিThreadLocalMap)প্রোপার্টি,তারপর হ্যাশ টেবিল ঘুরে সব entry বের করলেই সব threadlocal কী-ভ্যালু জোড়া পাওয়া যাবে。আরেকটি উপায় হলো jdk-এর ThreadLocal-এর ওপর আরেক স্তরের র্যাপার তৈরি করে,ওপরের কার্যকারিতাগুলো যোগ করা。
সমস্যা 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 jdk-এর ThreadLocal-এর বিকল্প হিসেবে কাজ করে, একই সঙ্গে এর সঙ্গে সামঞ্জস্যপূর্ণ FastThreadLocalThread,এ FastThreadLocalThread ও FastThreadLocal একসঙ্গে ব্যবহার করলে সর্বদা O(1) অনুসন্ধান সময়-জটিলতা নিশ্চিত করা যায়। এই সমাধানের মূলনীতি হলো অতিরিক্ত স্থান ব্যবহার করে সময় বাঁচানো। হ্যাশ সংঘর্ষের পর হ্যাশ টেবিলের অনুসন্ধান দক্ষতা কমে যাওয়া এড়াতে যথেষ্ট দীর্ঘ একটি অ্যারে তৈরি করা হয়, প্রতিটিFastThreadLocal তৈরির সময় প্রতিটিকে একটি অনন্য অ্যারে সূচক বরাদ্দ করা হয়,যেন প্রত্যেকের জন্য একটি নিজস্ব ঘর,খোঁজার সময় নিজের index দিয়ে সরাসরি অ্যারে থেকে মানটি নিলেই চলে।
সোর্স কোডটা সংক্ষেপে দিয়ে রাখি