أدوات التزامن المتقدّمة

الأقفال والشروط

15 دقيقة الدرس 8 من 13

الأقفال والشروط

خدمت الكلمة المفتاحية synchronized مطوّري Java منذ عام 1995، غير أنّ لها قيودًا حقيقية: لا تستطيع تحديد مهلة انتظار قفل، ولا مقاطعة خيط منتظر، ولا يدعم كائن المراقبة الواحد سوى قائمة انتظار واحدة. حلّت حزمة java.util.concurrent.locks المُقدَّمة في Java 5 جميع هذه المشكلات بتحويل القفل إلى كائن صريح يمكن التحكّم فيه برمجيًا.

لماذا لا نكتفي بـ synchronized؟

قبل استعراض واجهات برمجة التطبيقات الجديدة، يُفيد تحديد ما يعجز عنه synchronized بدقّة:

  • لا اكتساب بمهلة زمنية — يظلّ الخيط الذي يستدعي تابعًا متزامنًا محجوبًا إلى الأبد.
  • لا انتظار قابل للمقاطعة — لا يمكن إلغاء خيط ينتظر الحصول على مراقب.
  • شرط واحد لكل قفل — ترتبط wait()/notifyAll() بالكائن نفسه؛ لا يمكن توفير قوائم انتظار منفصلة "ليس ممتلئًا" و"ليس فارغًا" على القفل ذاته.
  • لا تمييز بين القراءة والكتابة — كل وصول حصري حتى عمليات القراءة البحتة.

ReentrantLock — البديل المباشر

يتصرّف ReentrantLock مثل synchronized لكنّه يمنحك تحكّمًا برمجيًا. القاعدة الأهم: احرر القفل دائمًا في كتلة finally حتى يُحرَّر حتى عند إطلاق استثناء.

import java.util.concurrent.locks.ReentrantLock; public class Counter { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); // دائمًا في finally } } public int get() { lock.lock(); try { return count; } finally { lock.unlock(); } } }
لا تنسَ كتلة finally أبدًا. إذا خرج استثناء بين lock.lock() وجسم الكتلة try، يبقى القفل محجوزًا إلى الأبد وكل خيط آخر يتوقف. اقرن دائمًا lock.lock() بـ try { ... } finally { lock.unlock(); }.

الاكتساب بمهلة زمنية والاكتساب القابل للمقاطعة هما ما يتفوّق فيه ReentrantLock بوضوح على synchronized:

import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class ResourceManager { private final ReentrantLock lock = new ReentrantLock(); // يُعيد false بدلًا من الحجب إلى الأبد public boolean tryProcess() throws InterruptedException { boolean acquired = lock.tryLock(500, TimeUnit.MILLISECONDS); if (!acquired) { System.out.println("تعذّر اكتساب القفل في الوقت المحدد — تجاوز"); return false; } try { // نفّذ العمل return true; } finally { lock.unlock(); } } // يستجيب لـ Thread.interrupt() public void interruptibleWork() throws InterruptedException { lock.lockInterruptibly(); try { // نفّذ العمل } finally { lock.unlock(); } } }

علامة العدالة خيار إضافي: new ReentrantLock(true) يمنح القفل للخيط الأطول انتظارًا، ممّا يمنع المجاعة لكنّه يُقلّل الإنتاجية إذ تنتظر الخيوط في طابور بدلًا من التنافس.

ReadWriteLock — الفصل بين القراءة والكتابة

كثيرًا ما تُقرَأ هياكل البيانات أكثر مما تُكتَب. تحتفظ ReentrantReadWriteLock بقفلَين من كائن واحد: قفل قراءة يمكن لعدة خيوط حمله في آنٍ واحد، وقفل كتابة حصري.

import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class CachedData { private final Map<String, String> cache = new HashMap<>(); private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); public String get(String key) { rwLock.readLock().lock(); // يُسمح لعدة قرّاء في آنٍ واحد try { return cache.get(key); } finally { rwLock.readLock().unlock(); } } public void put(String key, String value) { rwLock.writeLock().lock(); // حصري — يحجب كل القرّاء والكتّاب try { cache.put(key, value); } finally { rwLock.writeLock().unlock(); } } }
متى يُحقّق ReadWriteLock فائدة؟ يتفوّق حين تكون القراءات متكرّرة والكتابات نادرة وحين يكون العمل داخل القفل غير تافه (لتبرير تكلفة كائنَي القفل). في المقاطع الحرجة القصيرة جدًا، قد يكون ReentrantLock البسيط بل synchronized أسرع لأنّ ReadWriteLock ينطوي على محاسبة داخلية أكبر.

StampedLock — البديل الحديث

قدّمت Java 8 StampedLock التي تضيف وضع القراءة التفاؤلية. لا تحجب القراءة التفاؤلية الكتّابَ البتّة — تقرأ دون اكتساب أي قفل ثم تتحقّق من ختم لمعرفة ما إذا حدثت كتابة أثناء القراءة. فقط عند فشل التحقّق تُعاد المحاولة بقفل قراءة حقيقي.

import java.util.concurrent.locks.StampedLock; public class Point { private double x, y; private final StampedLock sl = new StampedLock(); public void move(double deltaX, double deltaY) { long stamp = sl.writeLock(); try { x += deltaX; y += deltaY; } finally { sl.unlockWrite(stamp); } } public double distanceFromOrigin() { long stamp = sl.tryOptimisticRead(); // لا قفل مكتسب double curX = x, curY = y; if (!sl.validate(stamp)) { // حدثت كتابة — أعِد المحاولة بقفل حقيقي stamp = sl.readLock(); try { curX = x; curY = y; } finally { sl.unlockRead(stamp); } } return Math.sqrt(curX * curX + curY * curY); } }

الشروط — قوائم انتظار متعددة على قفل واحد

Condition هو ما تستخدمه بديلًا عن wait()/notify() حين تستخدم أقفالًا صريحة. تُنشئ شرطًا واحدًا لكل محمول منطقي؛ يمكن لقفل واحد امتلاك شروط متعددة، مما يُلغي الاستيقاظ الطارئ للخيوط التي لم تكن تنتظر المحمول الذي تحقّق للتوّ.

import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class BoundedQueue<T> { private final Queue<T> queue = new ArrayDeque<>(); private final int capacity; private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public BoundedQueue(int capacity) { this.capacity = capacity; } public void put(T item) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // يُحرّر القفل أثناء الانتظار } queue.add(item); notEmpty.signal(); // أيقظ مستهلكًا واحدًا منتظرًا } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } T item = queue.poll(); notFull.signal(); // أيقظ منتجًا واحدًا منتظرًا return item; } finally { lock.unlock(); } } }
أعِد التحقّق من المحمول دائمًا في حلقة. حتى مع Condition.await()، الاستيقاظ الطارئ ممكن. النمط المعياري هو while (!condition) { await(); } وليس if (!condition) { await(); }.

اختيار الأداة المناسبة

  • synchronized — المقاطع الحرجة البسيطة التي لا تحتاج مهلة ولا إلغاء ولا شروطًا متعددة. افضّله لوضوحه وتحسينات JVM له (قفل متحيّز، إلغاء القفل).
  • ReentrantLock — حين تحتاج اكتسابًا بمهلة أو قابلًا للمقاطعة، أو كائنات Condition متعددة على القفل ذاته.
  • ReadWriteLock — أحمال العمل الثقيلة بالقراءة مع كتابات نادرة وعمل ذي مغزى داخل القفل.
  • StampedLock — أقصى إنتاجية على بيانات مهيمنة بالقراءة، حين تكون مرتاحًا للواجهة الأكثر تعقيدًا وحقيقة أنّه غير قابل لإعادة الدخول.

الخلاصة

يمنحك ReentrantLock كل ما يقدّمه synchronized مضافًا إليه الاكتساب بمهلة والمقاطعة وكائنات Condition متعددة. تُحسّن ReadWriteLock التزامن حين تهيمن القراءات. تمضي StampedLock أبعد مع القراءات التفاؤلية. استخدم الأقفال الصريحة فقط حين تعجز الأدوات الأبسط — المرونة الإضافية تأتي بمسؤولية إضافية لتحرير الأقفال بصواب.