Lock هي آلية مزامنة توفر وصولاً حصرياً إلى الأقسام الحرجة من الكود في التطبيقات متعددة الخيوط. وفقاً لـ Oracle، 2024، توفر واجهة Lock تحكماً أكثر مرونة في المزامنة مقارنة بكتل synchronized التقليدية، بما في ذلك محاولات الاستحواذ مع مهلة زمنية ودعم قوائم انتظار متعددة.
النقاط الرئيسية
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()، يتم مسح العلامة وإيقاظ أحد الخيوط المنتظرة.
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 هو تطبيق Lock الأساسي والأكثر استخداماً. يدعم إعادة الاستحواذ من نفس الخيط: إذا كان الخيط يمتلك القفل بالفعل، فإن استدعاء lock() مرة أخرى لا يحظره. هذا يمنع deadlock في الاستدعاءات المتكررة.
يفصل ReadWriteLock الأقفال إلى وضعين: قراءة وكتابة. يمكن لعدة خيوط الاحتفاظ بقفل القراءة في وقت واحد، لكن الكتابة تتطلب وصولاً حصرياً. هذا يحسن الأداء بشكل كبير في ظل القراءات المتكررة والكتابات النادرة.
StampedLock هو أحدث تطبيق، تم تقديمه في Java 8. يدعم ثلاثة أوضاع: الكتابة والقراءة والقراءة المتفائلة. القراءة المتفائلة لا تحجب الخيوط الأخرى وتتحقق من صحة البيانات بعد القراءة، مما يوفر تحسناً في الأداء بنسبة 10-20% مقارنة بـ ReadWriteLock.
| القفل | إصدار Java | الأوضاع | الأداء |
|---|---|---|---|
| ReentrantLock | Java 5 | حصري | عالٍ |
| ReadWriteLock | Java 5 | قراءة + كتابة | متوسط |
| StampedLock | Java 8 | قراءة + كتابة + متفائل | عالٍ جداً |
ReentrantLock هو تطبيق Lock الأكثر شعبية، ويوفر عدة ميزات غير متوفرة في synchronized. فهم خصائصه ضروري للعمل الفعال مع تعدد الخيوط.
يقبل مُنشئ ReentrantLock معلمة fair. عندما تكون true، يضمن القفل ترتيب FIFO؛ عندما تكون false، قد يحصل خيط جديد على القفل قبل الخيوط المنتظرة. الوضع العادل يمنع التجويع لكنه يقلل الإنتاجية بنسبة 10-20% بسبب الحمل الإضافي للحفاظ على قائمة الانتظار.
على عكس synchronized، يدعم ReentrantLock tryLock بمهلة زمنية. إذا تعذر الحصول على القفل خلال الوقت المحدد، يواصل الخيط التنفيذ بدلاً من الحظر إلى أجل غير مسمى. تسمح طريقة lockInterruptibly بمقاطعة خيط منتظر عبر Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("تم الحصول على القفل")
} finally {
lock.unlock()
}
} else {
println("فشل الحصول على القفل")
}
}
يدعم ReentrantLock متغيرات شرطية متعددة عبر طريقة newCondition(). لكل Condition قائمة انتظار خاصة بها، مما يتيح سيناريوهات استيقاظ معقدة. استبدلت طريقتا await() و signal() طريقتا wait() و notify() من كتل synchronized، لكن مع دعم قوائم انتظار متعددة.
ReadWriteLock وStampedLock يعالجان تحسين الوصول عندما تغلب القراءات على الكتابات. هما أكثر كفاءة بشكل ملحوظ من ReentrantLock في السيناريوهات التي تحدث فيها القراءة أكثر من الكتابة.
تحتوي واجهة ReadWriteLock على طريقتين: readLock() وwriteLock(). يمكن لقفل القراءة أن يحتفظ به عدة خيوط في وقت واحد، بينما قفل الكتابة حصري. مثال نموذجي هو ذاكرة تخزين مؤقت آمنة للخيوط: العديد من الخيوط تقرأ البيانات بينما يقوم واحد فقط بتحديثها دورياً.
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 يضيف وضعاً ثالثاً — tryOptimisticRead. هذا الوضع لا يحجب الخيوط الأخرى، بل يسجل فقط طابع (stamp) للحالة. بعد القراءة، يستدعي المطور validate(stamp) للتحقق مما إذا كانت البيانات قد تغيرت أثناء القراءة. إذا تغيرت البيانات، يجب إعادة العملية.
في التطبيقات المحمولة، تُستخدم الأقفال لتنسيق الوصول إلى البيانات المشتركة بين الخيوط. لكن استخدامها يتطلب حذراً خاصاً بسبب موارد الجهاز المحدودة والحاجة إلى الحفاظ على استجابة واجهة المستخدم.
على Android، ReentrantLock مفيد عند العمل مع Room وذاكرة التخزين المؤقت والملفات. من المهم تذكر: لا تحصل أبداً على قفل في الخيط الرئيسي. للكود غير المتزامن، يُفضل استخدام coroutines وMutex من kotlinx.coroutines، حيث تعلق coroutine بدلاً من حظر الخيط.
في iOS، يُستخدم NSLock القياسي بشكل أقل — يفضل المطورون DispatchQueue مع علامات barrier أو os_unfair_lock. يوفر Swift 5.7+ آليات مزامنة حديثة عبر actors، التي تحمي الحالة تلقائياً.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
لتجنب deadlocks، اتبع ترتيب استحواذ ثابت في جميع أنحاء المشروع. استخدم tryLock بمهلة زمنية بدلاً من lock() أينما كان الحظر المطول ممكناً. فكر في استخدام خوارزميات Lock-Free (AtomicReference, ConcurrentHashMap) بدلاً من الأقفال التقليدية.
يتطلب استخدام Lock الانضباط والالتزام بعدة قواعد تمنع deadlocks وتدهور الأداء. طور مجتمع Java هذه الممارسات على مدى 20 عاماً من استخدام حزمة java.util.concurrent.
النمط الأكثر أهمية هو lock في finally. بغض النظر عما إذا كان القسم الحرج قد اكتمل بنجاح أو ألقى استثناءً، يجب تحرير القفل. هذا يضمن عدم حظر الخيوط الأخرى إلى الأبد بسبب خطأ واحد. في Kotlin، يتم حل هذا النمط بأناقة عبر الامتداد withLock.
يجب أن يكون القسم الحرج قصيراً قدر الإمكان. لا تقم أبداً بإجراء الإدخال/الإخراج أو طلبات الشبكة أو العمليات الحسابية الطويلة داخل القفل. إذا كنت بحاجة لقراءة بيانات من خادم، احصل عليها أولاً، ثم احصل على القفل فقط لتحديث الحالة المشتركة. هذا يقلل التنافس ويحسن إنتاجية النظام.
لمنع deadlocks عند العمل مع أقفال متعددة، حدد ترتيب استحواذ عام في جميع أنحاء المشروع. إذا تم الحصول على lockA أولاً ثم lockB، يجب حظر أي تسلسل عكسي بواسطة قواعد مراجعة الكود. استخدم أدوات التحليل الثابت مثل SpotBugs وIntelliJ Inspections للتحقق التلقائي.
الأسئلة الشائعة
Lock هي واجهة صريحة مع دعم المهلة والانتظار القابل للمقاطعة. synchronized يحصل ويحرر الشاشة تلقائياً، لكنه لا يسمح باستخدام tryLock أو lockInterruptibly أو Conditions متعددة. Lock أكثر مرونة لكنه يتطلب تحريراً يدوياً في finally.
القفل العادل يضمن ترتيب FIFO: الخيط الذي انتظر أطول وقت يحصل على القفل أولاً. القفل غير العادل قد يمنح الوصول لخيط جديد قبل الخيوط المنتظرة، مما يزيد الإنتاجية لكنه قد يسبب تجويع الخيوط المنتظرة.
اتبع ترتيباً ثابتاً للحصول على جميع الأقفال، استخدم tryLock بمهلة زمنية بدلاً من lock() غير المشروط، وقلل عدد الأقفال المحتفظ بها في وقت واحد. استخدام هياكل بيانات Lock-Free يقلل أيضاً من خطر deadlock.
Condition هي نظير wait/notify لـ Lock، مما يتيح قوائم انتظار متعددة مستقلة. كل استدعاء newCondition() ينشئ قائمة انتظار منفصلة، مما يوفر تحكماً أكثر دقة في إيقاظ الخيوط مقارنة بقائمة الانتظار الواحدة في synchronized.
لـ Android مع coroutines، استخدم Mutex من kotlinx.coroutines — يعلق coroutine بدلاً من حظر الخيط. لـ iOS مع Swift 5.7+، يُفضل actors التي تزامن الوصول إلى الحالة تلقائياً. اترك ReentrantLock للكود القديم والسيناريوهات منخفضة المستوى.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا