المشكلة: ThreadLocal وملايين الخيوط الافتراضية
ThreadLocal أداة قديمة لمشاركة بيانات (مثل معرّف المستخدم الحالي أو سياق الطلب) دون تمريرها كمعامل لكل دالة. لكن لها مشكلتان تتضخّمان مع الخيوط الافتراضية (راجع درس الخيوط الافتراضية):
- الذاكرة: كل خيط — حتى الافتراضي الخفيف — يحمل نسخته الخاصة من كل
ThreadLocal. مع ملايين الخيوط الافتراضية المحتملة، هذا يتراكم بسرعة. - التسريب: لو نسيت استدعاء
remove()بعد انتهاء الاستخدام، القيمة القديمة تبقى عالقة — خطير خصوصًا مع تجميع الخيوط (pooling) حيث يُعاد استخدام نفس الخيط لاحقًا بقيمة من طلب سابق.
ScopedValue: القيمة تنتهي تلقائيًا بنهاية النطاق
منذ Java 25 (JEP 506)، ScopedValue حاوية غير قابلة للتغيير (immutable) مرئية فقط ضمن نطاق ديناميكي محدد — الدالة التي تربط القيمة، وكل ما تستدعيه من دوال (وحتى الخيوط الفرعية التي تنشئها)، ثم تختفي تلقائيًا بمجرد الخروج من ذلك النطاق. لا remove() ولا نسيان تنظيف:
public class Example {
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
public static void main(String[] args) {
ScopedValue.where(USER_ID, "user-42").run(() -> {
greet(); // داخل النطاق — USER_ID.get() تعمل
});
// هنا خارج النطاق — USER_ID.get() ترمي IllegalStateException
}
private static void greet() {
System.out.println("أهلًا، المستخدم: " + USER_ID.get());
}
}
(لاحظ أن ScopedValue موجودة في java.lang — مثل IO في الدرس السابق، لا تحتاج import صريح.)
القواعد الأساسية
ScopedValue.newInstance(): ينشئ حاوية فارغة — عادةstatic finalعلى مستوى الصنف.ScopedValue.where(VALUE, x).run(() -> {...}): يربط القيمةxبالحاوية، وينفّذ الكود داخلrunبهذا الربط فعّالًا.VALUE.get(): يقرأ القيمة الحالية — تعمل فقط داخلrunالمطابق، وإلا ترميIllegalStateException.VALUE.isBound(): يفحص هل القيمة مربوطة حاليًا قبل استدعاءget()، لتجنّب الاستثناء.
لماذا مثالية مع الخيوط الافتراضية
عندما ينشئ خيط ربط ScopedValue خيطًا افتراضيًا فرعيًا (مثلًا عبر Executors.newVirtualThreadPerTaskExecutor())، الخيط الفرعي يرث نفس الربط تلقائيًا — بلا تكلفة نسخ إضافية، وبلا احتمال تسريب لأن الربط يزول مع خروج run بغض النظر عن عدد الخيوط الفرعية التي أنشأها.
ScopedValue مقابل ThreadLocal
| الخاصية | ThreadLocal | ScopedValue |
|---|---|---|
| قابلية التعديل | set() يغيّر القيمة أي وقت | غير قابلة للتغيير بعد الربط |
| التنظيف | يدوي عبر remove() — يُنسى بسهولة | تلقائي بنهاية run |
| التكلفة مع آلاف/ملايين الخيوط | نسخة كاملة لكل خيط | خفيفة، مصممة لهذا الحجم |
| الوراثة للخيوط الفرعية | تحتاج InheritableThreadLocal خاصة | تلقائية عبر run |
💡 استخدم
ScopedValueللبيانات التي "تُحدَّد مرة وتُقرأ فقط" ضمن سلسلة استدعاء واحدة (سياق طلب، معرّف مستخدم، إعدادات تتبّع) — أما البيانات التي تحتاج تعديلًا متكررًا أثناء التنفيذ فتبقى تحتاج آلية أخرى (متغيّر عادي يُمرَّر كمعامل، أو حالة مشتركة محمية بقفل).
🎯 التالي: خلاصة مسار Java.