ThreadLocal-এর সমস্যা যখন থ্রেড সস্তা হয়ে যায়

ThreadLocal বহু বছর ধরে জাভায় per-thread কনটেক্সট (যেমন request ID, security principal, ট্রানজেকশন কনটেক্সট) বহন করার প্রধান উপায়। কিন্তু ভার্চুয়াল থ্রেডের যুগে এটার তিনটা গুরুত্বপূর্ণ সমস্যা সামনে আসে।

প্রথমত, মেমরি। প্রতিটা ThreadLocal ভ্যারিয়েবল প্রতিটা থ্রেডের সাথে একটা এন্ট্রি নিয়ে বসে থাকে। কয়েকশো প্ল্যাটফর্ম থ্রেডে এটা কোনো সমস্যা না, কিন্তু লাখ লাখ ভার্চুয়াল থ্রেডে (Phase 5-এর শুরুতে যেমন দেখেছ) এটা মেমরির একটা বড় খরচ হয়ে দাঁড়াতে পারে, বিশেষ করে যদি ভুলবশত mutable বা ভারী অবজেক্ট সেট করা হয় আর clear করা না হয়।

দ্বিতীয়ত, InheritableThreadLocal-এর খরচ। যখন একটা থ্রেড আরেকটা থ্রেড তৈরি করে, প্রতিটা InheritableThreadLocal মান কপি করতে হয়। যদি একটা রিকোয়েস্ট হ্যান্ডলার হাজার হাজার চাইল্ড ভার্চুয়াল থ্রেড তৈরি করে (Phase 5-এর স্ট্রাকচার্ড কনকারেন্সিতে যেমন), এই কপি করার খরচ যোগ হতে থাকে।

তৃতীয়ত, mutability আর lifetime নিয়ন্ত্রণের অভাব। যেকোনো কোড যেকোনো সময় set() কল করে মান বদলে দিতে পারে, আর remove() না করলে মান "লিক" হয়ে যেতে পারে পরবর্তী কাজে (বিশেষ করে থ্রেড পুল রিইউজ করা হলে)।

ScopedValue: immutable, কাঠামোবদ্ধ, স্বয়ংক্রিয়ভাবে পরিষ্কার

Java-তে (প্রিভিউ ফিচার হিসেবে) ScopedValue যোগ করা হয়েছে ThreadLocal-এর একটা কাঠামোবদ্ধ বিকল্প হিসেবে। মূল আইডিয়া: একটা মান শুধু একটা নির্দিষ্ট, সীমাবদ্ধ কোড ব্লকের ভেতরেই দৃশ্যমান থাকবে — এবং সেই ব্লক শেষ হলে মানটা স্বয়ংক্রিয়ভাবে "আনবাইন্ড" হয়ে যাবে।

private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

void handleRequest(String id) {
    ScopedValue.where(REQUEST_ID, id).run(() -> {
        processRequest(); // এই ব্লকের ভেতরে REQUEST_ID.get() কাজ করবে
    });
    // এখানে REQUEST_ID.get() কল করলে exception ছোঁড়ে — স্কোপ শেষ
}

void processRequest() {
    log.info("Processing request: " + REQUEST_ID.get());
    fetchData(); // চাইল্ড কল থেকেও REQUEST_ID.get() কাজ করবে
}

কেন এটা ভার্চুয়াল থ্রেডের সাথে ভালো মানায়

ScopedValue.where(...).run(...) কল করলে, run()-এর ভেতরে ফর্ক করা যেকোনো ভার্চুয়াল থ্রেড (যেমন StructuredTaskScope-এর সাবটাস্ক) স্বয়ংক্রিয়ভাবে সেই মান দেখতে পায় — আর কোনো ম্যানুয়াল কপি ছাড়াই, InheritableThreadLocal-এর খরচ ছাড়াই। যেহেতু মানটা immutable (একবার bind হলে বদলানো যায় না) আর lifetime শুধু run() ব্লকের সাথে বাঁধা, JVM অনেক বেশি অপটিমাইজেশন করতে পারে — কোনো clean-up কোড লেখার দরকার নেই, কারণ ভুলে যাওয়ার কোনো সুযোগই নেই।

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    ScopedValue.where(REQUEST_ID, id).run(() -> {
        scope.fork(() -> fetchUser(id));   // REQUEST_ID দেখতে পাবে
        scope.fork(() -> fetchOrders(id)); // এটাও দেখতে পাবে
    });
    scope.join();
}

তুলনা: ThreadLocal বনাম ScopedValue

বিষয় ThreadLocal ScopedValue
Mutability যেকোনো সময় set() করা যায় একবার bind, immutable
Lifetime ম্যানুয়ালি remove() করতে হয় স্কোপ শেষে স্বয়ংক্রিয়
চাইল্ড থ্রেডে প্রচার InheritableThreadLocal দরকার, কপি খরচ আছে স্বাভাবিকভাবেই দৃশ্যমান, কপি ছাড়া
লিকের ঝুঁকি আছে (থ্রেড পুল রিইউজে) নেই

ব্যবহারিক পরামর্শ

ScopedValue সব জায়গায় ThreadLocal প্রতিস্থাপন করার জন্য না — যেখানে সত্যিকারের mutable, দীর্ঘস্থায়ী per-thread স্টেট দরকার (যেমন একটা reusable বাফার), সেখানে ThreadLocal-ই ঠিক আছে। কিন্তু request-scoped কনটেক্সট (correlation ID, security principal, locale) বহন করার জন্য, বিশেষ করে ভার্চুয়াল থ্রেড আর স্ট্রাকচার্ড কনকারেন্সির সাথে কাজ করার সময়, ScopedValue-ই স্বাভাবিক আর নিরাপদ পছন্দ।

Share