تخطَّ إلى المحتوى

شرح Java

القيم محدودة النطاق (Scoped Values)

الدرس 47 من 48· ⏱ 2 دقائق قراءة

المشكلة: 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

الخاصيةThreadLocalScopedValue
قابلية التعديلset() يغيّر القيمة أي وقتغير قابلة للتغيير بعد الربط
التنظيفيدوي عبر remove() — يُنسى بسهولةتلقائي بنهاية run
التكلفة مع آلاف/ملايين الخيوطنسخة كاملة لكل خيطخفيفة، مصممة لهذا الحجم
الوراثة للخيوط الفرعيةتحتاج InheritableThreadLocal خاصةتلقائية عبر run

💡 استخدم ScopedValue للبيانات التي "تُحدَّد مرة وتُقرأ فقط" ضمن سلسلة استدعاء واحدة (سياق طلب، معرّف مستخدم، إعدادات تتبّع) — أما البيانات التي تحتاج تعديلًا متكررًا أثناء التنفيذ فتبقى تحتاج آلية أخرى (متغيّر عادي يُمرَّر كمعامل، أو حالة مشتركة محمية بقفل).

🎯 التالي: خلاصة مسار Java.

شرح القيم محدودة النطاق (Scoped Values) — Java بالعربي
القيم محدودة النطاق (Scoped Values)Java بالعربي · The Code Fix

📚 لمزيد من التعمّق في Java، راجِع توثيق Java من Oracle.

هل كان هذا الدرس مفيدًا؟