Lock: ما هو، أنواع الأقفال واستخدامها في المزامنة

المؤلف: IT Sectr نُشر: 2026-03-19 وقت القراءة: 8 دق

Lock هي آلية مزامنة توفر وصولاً حصرياً إلى الأقسام الحرجة من الكود في التطبيقات متعددة الخيوط. وفقاً لـ Oracle، 2024، توفر واجهة Lock تحكماً أكثر مرونة في المزامنة مقارنة بكتل synchronized التقليدية، بما في ذلك محاولات الاستحواذ مع مهلة زمنية ودعم قوائم انتظار متعددة.

النقاط الرئيسية

  • Lock هي واجهة للإدارة الصريحة للأقفال في Java.
  • ReentrantLock هو تطبيق أساسي يدعم إعادة الاستحواذ من نفس الخيط.
  • ReadWriteLock يفصل أقفال القراءة والكتابة لتحسين الأداء.
  • Deadlock هو الخطر الرئيسي عند استخدام أقفال متعددة في وقت واحد.
  • على عكس synchronized، يدعم Lock المهلات والانتظار القابل للمقاطعة.

ما هو Lock؟

Lock هي واجهة من حزمة java.util.concurrent.locks توفر عمليات صريحة للقفل والفتح لمزامنة الوصول إلى البيانات. على عكس synchronized، تمنح Lock المطور تحكماً كاملاً في آلية القفل.

التعريف والدور في المزامنة

تم تقديم واجهة Lock في Java 5 كبديل لآلية synchronized المدمجة. الطرق الرئيسية هي lock وunlock وtryLock وlockInterruptibly. تتيح الأقفال تنظيم الوصول الآمن إلى البيانات في بيئة متعددة الخيوط، مما يمنع حالات السباق وتلف البيانات.

الميزة الرئيسية لـ Lock على synchronized هي المرونة. يمكن للمطور محاولة الحصول على القفل بمهلة زمنية، أو التحقق من توفره دون حظر، أو تنظيم قوائم انتظار متعددة بأولويات مختلفة.

التاريخ والتطور

قبل ظهور واجهة Lock في Java 5، كانت طريقة المزامنة الوحيدة هي synchronized، التي عانت من قيود: عدم وجود مهلات زمنية، عدم وجود انتظار قابل للمقاطعة، وقائمة انتظار واحدة. صمم Doug Lea حزمة java.util.concurrent، متضمناً Lock كلبنة أساسية.

كيف يعمل القفل؟

يدير القفل الوصول من خلال علامة حالة داخلية وقائمة انتظار. عندما يستدعي خيط lock()، يتحقق الآلية مما إذا كان القفل حراً، إما أن تستحوذ عليه أو تضع الخيط في قائمة الانتظار حتى يتم تحريره.

الاستحواذ والتحرير الذري

في قلب أي قفل توجد عملية ذرية للمقارنة والتبديل (CAS). عند استدعاء lock()، يحاول الخيط تعيين علامة الانشغال بشكل ذري. إذا كانت العلامة مضبوطة بالفعل، يتم حظر الخيط. عند unlock()، يتم مسح العلامة وإيقاظ أحد الخيوط المنتظرة.

kotlin
import java.util.concurrent.locks.ReentrantLock

val lock = ReentrantLock()

fun performTask() {
    lock.lock()
    try {
        // قسم حرج
        println("الخيط ${Thread.currentThread().name} يعمل")
    } finally {
        lock.unlock()
    }
}

قائمة الانتظار والاستيقاظ

يستخدم ReentrantLock داخلياً قائمة مرتبطة مزدوجة (قائمة انتظار CLH) حيث يتم تمثيل كل خيط منتظر بعقدة. عندما يتم تحرير القفل، يتم إيقاظ العقدة الرئيسية في قائمة الانتظار. يضمن الوضع العادل (fair) ترتيب FIFO، بينما يسمح الوضع غير العادل لخيط جديد بالحصول على القفل قبل الخيوط المنتظرة لتحسين الإنتاجية.

الأنواع الرئيسية للأقفال

في النظام البيئي الحديث لـ Java، توجد عدة تطبيقات لـ الأقفال، كل منها محسّن لسيناريوهات محددة. اختيار القفل المناسب يؤثر مباشرة على أداء وموثوقية التطبيق متعدد الخيوط.

ReentrantLock

ReentrantLock هو تطبيق Lock الأساسي والأكثر استخداماً. يدعم إعادة الاستحواذ من نفس الخيط: إذا كان الخيط يمتلك القفل بالفعل، فإن استدعاء lock() مرة أخرى لا يحظره. هذا يمنع deadlock في الاستدعاءات المتكررة.

ReentrantReadWriteLock

يفصل ReadWriteLock الأقفال إلى وضعين: قراءة وكتابة. يمكن لعدة خيوط الاحتفاظ بقفل القراءة في وقت واحد، لكن الكتابة تتطلب وصولاً حصرياً. هذا يحسن الأداء بشكل كبير في ظل القراءات المتكررة والكتابات النادرة.

StampedLock

StampedLock هو أحدث تطبيق، تم تقديمه في Java 8. يدعم ثلاثة أوضاع: الكتابة والقراءة والقراءة المتفائلة. القراءة المتفائلة لا تحجب الخيوط الأخرى وتتحقق من صحة البيانات بعد القراءة، مما يوفر تحسناً في الأداء بنسبة 10-20% مقارنة بـ ReadWriteLock.

القفلإصدار Javaالأوضاعالأداء
ReentrantLockJava 5حصريعالٍ
ReadWriteLockJava 5قراءة + كتابةمتوسط
StampedLockJava 8قراءة + كتابة + متفائلعالٍ جداً

ReentrantLock ومميزاته

ReentrantLock هو تطبيق Lock الأكثر شعبية، ويوفر عدة ميزات غير متوفرة في synchronized. فهم خصائصه ضروري للعمل الفعال مع تعدد الخيوط.

عدالة القفل (fairness)

يقبل مُنشئ ReentrantLock معلمة fair. عندما تكون true، يضمن القفل ترتيب FIFO؛ عندما تكون false، قد يحصل خيط جديد على القفل قبل الخيوط المنتظرة. الوضع العادل يمنع التجويع لكنه يقلل الإنتاجية بنسبة 10-20% بسبب الحمل الإضافي للحفاظ على قائمة الانتظار.

المهلات والانتظار القابل للمقاطعة

على عكس synchronized، يدعم ReentrantLock tryLock بمهلة زمنية. إذا تعذر الحصول على القفل خلال الوقت المحدد، يواصل الخيط التنفيذ بدلاً من الحظر إلى أجل غير مسمى. تسمح طريقة lockInterruptibly بمقاطعة خيط منتظر عبر Thread.interrupt().

kotlin
val lock = ReentrantLock()

fun tryTask() {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            println("تم الحصول على القفل")
        } finally {
            lock.unlock()
        }
    } else {
        println("فشل الحصول على القفل")
    }
}

الشروط (Conditions)

يدعم ReentrantLock متغيرات شرطية متعددة عبر طريقة newCondition(). لكل Condition قائمة انتظار خاصة بها، مما يتيح سيناريوهات استيقاظ معقدة. استبدلت طريقتا await() و signal() طريقتا wait() و notify() من كتل synchronized، لكن مع دعم قوائم انتظار متعددة.

ReadWriteLock وStampedLock

ReadWriteLock وStampedLock يعالجان تحسين الوصول عندما تغلب القراءات على الكتابات. هما أكثر كفاءة بشكل ملحوظ من ReentrantLock في السيناريوهات التي تحدث فيها القراءة أكثر من الكتابة.

ReadWriteLock في الممارسة

تحتوي واجهة ReadWriteLock على طريقتين: readLock() وwriteLock(). يمكن لقفل القراءة أن يحتفظ به عدة خيوط في وقت واحد، بينما قفل الكتابة حصري. مثال نموذجي هو ذاكرة تخزين مؤقت آمنة للخيوط: العديد من الخيوط تقرأ البيانات بينما يقوم واحد فقط بتحديثها دورياً.

kotlin
class SafeCache<K, V> {
    private val map = mutableMapOf<K, V>()
    private val rwLock = ReentrantReadWriteLock()

    fun get(key: K): V? {
        rwLock.readLock().lock()
        return try { map[key] } finally { rwLock.readLock().unlock() }
    }

    fun put(key: K, value: V) {
        rwLock.writeLock().lock()
        return try { map[key] = value } finally { rwLock.writeLock().unlock() }
    }
}

StampedLock والقراءة المتفائلة

StampedLock يضيف وضعاً ثالثاً — tryOptimisticRead. هذا الوضع لا يحجب الخيوط الأخرى، بل يسجل فقط طابع (stamp) للحالة. بعد القراءة، يستدعي المطور validate(stamp) للتحقق مما إذا كانت البيانات قد تغيرت أثناء القراءة. إذا تغيرت البيانات، يجب إعادة العملية.

الأقفال في تطوير التطبيقات المحمولة

في التطبيقات المحمولة، تُستخدم الأقفال لتنسيق الوصول إلى البيانات المشتركة بين الخيوط. لكن استخدامها يتطلب حذراً خاصاً بسبب موارد الجهاز المحدودة والحاجة إلى الحفاظ على استجابة واجهة المستخدم.

الأقفال في Android (Kotlin)

على Android، ReentrantLock مفيد عند العمل مع Room وذاكرة التخزين المؤقت والملفات. من المهم تذكر: لا تحصل أبداً على قفل في الخيط الرئيسي. للكود غير المتزامن، يُفضل استخدام coroutines وMutex من kotlinx.coroutines، حيث تعلق coroutine بدلاً من حظر الخيط.

الأقفال في iOS (Swift)

في iOS، يُستخدم NSLock القياسي بشكل أقل — يفضل المطورون DispatchQueue مع علامات barrier أو os_unfair_lock. يوفر Swift 5.7+ آليات مزامنة حديثة عبر actors، التي تحمي الحالة تلقائياً.

swift
import Foundation

actor DataStore {
    private var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }

    func getAll() -> [String] {
        items
    }
}

توصيات لمنع deadlock

لتجنب deadlocks، اتبع ترتيب استحواذ ثابت في جميع أنحاء المشروع. استخدم tryLock بمهلة زمنية بدلاً من lock() أينما كان الحظر المطول ممكناً. فكر في استخدام خوارزميات Lock-Free (AtomicReference, ConcurrentHashMap) بدلاً من الأقفال التقليدية.

أفضل الممارسات للعمل مع Lock

يتطلب استخدام Lock الانضباط والالتزام بعدة قواعد تمنع deadlocks وتدهور الأداء. طور مجتمع Java هذه الممارسات على مدى 20 عاماً من استخدام حزمة java.util.concurrent.

التحرير في finally

النمط الأكثر أهمية هو lock في finally. بغض النظر عما إذا كان القسم الحرج قد اكتمل بنجاح أو ألقى استثناءً، يجب تحرير القفل. هذا يضمن عدم حظر الخيوط الأخرى إلى الأبد بسبب خطأ واحد. في Kotlin، يتم حل هذا النمط بأناقة عبر الامتداد withLock.

تقليل وقت الاحتفاظ

يجب أن يكون القسم الحرج قصيراً قدر الإمكان. لا تقم أبداً بإجراء الإدخال/الإخراج أو طلبات الشبكة أو العمليات الحسابية الطويلة داخل القفل. إذا كنت بحاجة لقراءة بيانات من خادم، احصل عليها أولاً، ثم احصل على القفل فقط لتحديث الحالة المشتركة. هذا يقلل التنافس ويحسن إنتاجية النظام.

ترتيب استحواذ ثابت

لمنع deadlocks عند العمل مع أقفال متعددة، حدد ترتيب استحواذ عام في جميع أنحاء المشروع. إذا تم الحصول على lockA أولاً ثم lockB، يجب حظر أي تسلسل عكسي بواسطة قواعد مراجعة الكود. استخدم أدوات التحليل الثابت مثل SpotBugs وIntelliJ Inspections للتحقق التلقائي.

الأسئلة الشائعة

ما الفرق بين Lock وsynchronized؟

Lock هي واجهة صريحة مع دعم المهلة والانتظار القابل للمقاطعة. synchronized يحصل ويحرر الشاشة تلقائياً، لكنه لا يسمح باستخدام tryLock أو lockInterruptibly أو Conditions متعددة. Lock أكثر مرونة لكنه يتطلب تحريراً يدوياً في finally.

ما هو القفل العادل (fair lock)؟

القفل العادل يضمن ترتيب FIFO: الخيط الذي انتظر أطول وقت يحصل على القفل أولاً. القفل غير العادل قد يمنح الوصول لخيط جديد قبل الخيوط المنتظرة، مما يزيد الإنتاجية لكنه قد يسبب تجويع الخيوط المنتظرة.

كيف أتجنب deadlock عند استخدام Lock؟

اتبع ترتيباً ثابتاً للحصول على جميع الأقفال، استخدم tryLock بمهلة زمنية بدلاً من lock() غير المشروط، وقلل عدد الأقفال المحتفظ بها في وقت واحد. استخدام هياكل بيانات Lock-Free يقلل أيضاً من خطر deadlock.

ما هي Condition في Lock؟

Condition هي نظير wait/notify لـ Lock، مما يتيح قوائم انتظار متعددة مستقلة. كل استدعاء newCondition() ينشئ قائمة انتظار منفصلة، مما يوفر تحكماً أكثر دقة في إيقاظ الخيوط مقارنة بقائمة الانتظار الواحدة في synchronized.

أي Lock يجب أن أختار لتطبيق محمول؟

لـ Android مع coroutines، استخدم Mutex من kotlinx.coroutines — يعلق coroutine بدلاً من حظر الخيط. لـ iOS مع Swift 5.7+، يُفضل actors التي تزامن الوصول إلى الحالة تلقائياً. اترك ReentrantLock للكود القديم والسيناريوهات منخفضة المستوى.

الخلاصة

  • Lock هي واجهة إدارة أقفال صريحة من java.util.concurrent.locks.
  • ReentrantLock هو التطبيق الرئيسي مع دعم إعادة الاستحواذ والعدالة.
  • ReadWriteLock يفصل أقفال القراءة والكتابة لسيناريوهات كثرة القراءة.
  • StampedLock يضيف القراءة المتفائلة لأقصى أداء.
  • المهلات و Conditions هي المزايا الرئيسية لـ Lock على synchronized.
  • Deadlock يُمنع بترتيب استحواذ ثابت واستخدام tryLock.
  • في التطوير المحمول، يُوصى باستخدام coroutines (Android) و actors (iOS).

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا