Lock: چیست، انواع قفل‌ها و استفاده در همگام‌سازی

نویسنده: IT Sectr منتشر شده: 2026-03-19 زمان مطالعه: 8 دقیقه

Lock مکانیزم همگام‌سازی است که دسترسی انحصاری به بخش‌های بحرانی کد در برنامه‌های چندنخی را فراهم می‌کند. به گفته Oracle, 2024، رابط Lock کنترل همگام‌سازی انعطاف‌پذیرتری نسبت به بلوک‌های synchronized سنتی ارائه می‌دهد، از جمله تلاش برای قفل با تایم‌اوت و پشتیبانی از چندین صف انتظار.

نکات اصلی

  • Lock — رابطی برای مدیریت صریح قفل‌ها در جاوا.
  • ReentrantLock — پیاده‌سازی پایه با پشتیبانی از قفل مجدد توسط همان نخ.
  • ReadWriteLock قفل‌ها را به خواندن و نوشتن برای افزایش کارایی تقسیم می‌کند.
  • Deadlock — ریسک اصلی هنگام استفاده همزمان از چندین قفل.
  • برخلاف synchronized، Lock از تایم‌اوت و انتظار قابل قطع پشتیبانی می‌کند.

Lock چیست؟

Lock — رابطی از بسته java.util.concurrent.locks است که عملیات صریح قفل و باز کردن قفل را برای همگام‌سازی دسترسی به داده‌ها فراهم می‌کند. برخلاف synchronized، Lock کنترل کامل مکانیزم قفل را به توسعه‌دهنده می‌دهد.

تعریف و نقش در همگام‌سازی

رابط Lock در جاوا 5 به عنوان جایگزینی برای مکانیزم داخلی synchronized ظاهر شد. روش‌های اصلی — lock، unlock، tryLock و lockInterruptibly. قفل‌ها امکان سازماندهی دسترسی امن به داده‌ها در محیط چندنخی را فراهم می‌کنند و از شرایط مسابقه و آسیب داده‌ها جلوگیری می‌کنند.

مزیت اصلی Lock نسبت به synchronized — انعطاف‌پذیری. توسعه‌دهنده می‌تواند با تایم‌اوت سعی در قفل کند، اشغال بودن آن را بدون مسدود شدن بررسی کند یا چندین صف انتظار با اولویت‌های مختلف سازماندهی کند.

تاریخچه توسعه

قبل از ظهور رابط Lock در جاوا 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 lock queue) استفاده می‌کند که در آن هر نخ منتظر با یک گره نمایش داده می‌شود. هنگامی که قفل آزاد می‌شود، گره سر صف بیدار می‌شود. حالت fair (عادلانه) ترتیب FIFO را تضمین می‌کند و unfair به نخ جدید اجازه می‌دهد قبل از منتظران قفل را بگیرد تا توان عملیاتی افزایش یابد.

انواع اصلی قفل‌ها

در پشته مدرن جاوا چندین پیاده‌سازی از قفل‌ها وجود دارد که هر کدام برای سناریوهای خاصی بهینه شده‌اند. انتخاب قفل مناسب مستقیماً بر کارایی و قابلیت اطمینان برنامه چندنخی تأثیر می‌گذارد.

ReentrantLock

ReentrantLock — پیاده‌سازی پایه و پرکاربردترین Lock است. از قفل مجدد توسط همان نخ پشتیبانی می‌کند: اگر نخ قبلاً قفل را در اختیار دارد، فراخوانی مجدد lock() آن را مسدود نمی‌کند. این کار از deadlock در فراخوانی‌های بازگشتی جلوگیری می‌کند.

ReentrantReadWriteLock

ReadWriteLock قفل‌ها را به دو حالت تقسیم می‌کند: خواندن و نوشتن. چندین نخ می‌توانند همزمان قفل خواندن را نگه دارند، اما نوشتن نیاز به دسترسی انحصاری دارد. این کار هنگام خواندن مکرر و نوشتن نادر، کارایی را به طور قابل توجهی افزایش می‌دهد.

StampedLock

StampedLock — جدیدترین پیاده‌سازی که در جاوا 8 ظاهر شد. از سه حالت پشتیبانی می‌کند: نوشتن، خواندن و خواندن خوش‌بینانه. خواندن خوش‌بینانه نخ‌های دیگر را مسدود نمی‌کند و پس از خواندن اعتبار داده‌ها را بررسی می‌کند که 10-20% افزایش کارایی نسبت به ReadWriteLock ایجاد می‌کند.

قفلنسخه جاواحالت‌هاکارایی
ReentrantLockJava 5انحصاریبالا
ReadWriteLockJava 5خواندن + نوشتنمتوسط
StampedLockJava 8خواندن + نوشتن + optimisticبسیار بالا

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، کش‌ها و فایل‌ها مفید است. مهم است به خاطر داشته باشید: هرگز قفل را در نخ اصلی نگیرید. برای کد ناهمگام، کوروتین‌ها و Mutex از kotlinx.coroutines که نخ را مسدود نمی‌کنند بلکه کوروتین را متوقف می‌کنند، ترجیح داده می‌شوند.

قفل‌ها در iOS (Swift)

در iOS، Lock استاندارد از 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

برای جلوگیری از deadlock، ترتیب یکسان قفل کردن همه قفل‌ها را در پروژه رعایت کنید. به جای lock() در جاهایی که قفل طولانی مدت ممکن است، از tryLock با تایم‌اوت استفاده کنید. استفاده از الگوریتم‌های Lock-Free (AtomicReference, ConcurrentHashMap) را به جای قفل‌های سنتی در نظر بگیرید.

بهترین روش‌های کار با Lock

استفاده از Lock نیاز به انضباط و رعایت چندین قانون دارد که از deadlock و کاهش کارایی جلوگیری می‌کند. این روش‌ها توسط جامعه جاوا در طول 20 سال استفاده از بسته java.util.concurrent ایجاد شده‌اند.

آزادسازی در finally

مهمترین الگو — lock در finally. صرف‌نظر از اینکه بخش بحرانی با موفقیت یا با استثنا به پایان رسیده است، قفل باید آزاد شود. این تضمین می‌کند که نخ‌های دیگر به دلیل یک خطا برای همیشه مسدود نخواهند شد. در Kotlin این الگو به طور ظریفی از طریق extension withLock حل می‌شود.

به حداقل رساندن زمان نگهداری

بخش بحرانی باید حداکثر کوتاه باشد. هرگز در داخل قفل عملیات ورودی-خروجی، درخواست‌های شبکه یا محاسبات طولانی انجام ندهید. اگر نیاز به خواندن داده‌ها از سرور دارید، ابتدا آنها را دریافت کنید و سپس فقط برای به‌روزرسانی وضعیت مشترک قفل را بگیرید. این کار رقابت را کاهش می‌دهد و توان عملیاتی سیستم را افزایش می‌دهد.

ترتیب یکسان قفل کردن

برای جلوگیری از deadlock هنگام کار با چندین Lock، ترتیب سراسری قفل کردن را در کل پروژه تعیین کنید. اگر ابتدا lockA و سپس lockB گرفته می‌شود — هر ترتیب معکوس باید توسط قوانین code review ممنوع شود. برای بررسی خودکار از تحلیلگرهای ایستا مانند SpotBugs و IntelliJ Inspections استفاده کنید.

سوالات متداول

تفاوت Lock با synchronized چیست؟

Lock — رابط صریح با امکان تایم‌اوت و انتظار قابل قطع. synchronized به طور خودکار مانیتور را قفل و آزاد می‌کند اما اجازه استفاده از tryLock، lockInterruptibly و چندین Condition را نمی‌دهد. Lock انعطاف‌پذیرتر است اما نیاز به آزادسازی دستی در finally دارد.

قفل عادلانه (fair lock) چیست؟

قفل عادلانه ترتیب FIFO دسترسی را تضمین می‌کند: نخی که بیشترین انتظار را کشیده، اولین بار قفل را دریافت می‌کند. قفل ناعادلانه ممکن است دسترسی را به نخ جدید دور زدن صف بدهد که توان عملیاتی را افزایش می‌دهد اما می‌تواند باعث گرسنگی نخ‌های منتظر شود.

چگونه در استفاده از Lock از deadlock جلوگیری کنیم؟

ترتیب ثابت قفل کردن همه قفل‌ها را رعایت کنید، به جای lock بدون شرط از tryLock با تایم‌اوت استفاده کنید و تعداد قفل‌های همزمان را به حداقل برسانید. استفاده از ساختارهای داده Lock-Free نیز خطر deadlock را کاهش می‌دهد.

Condition در Lock چیست؟

Condition — مشابه wait/notify برای Lock است که امکان سازماندهی چندین صف انتظار مستقل را فراهم می‌کند. هر فراخوانی newCondition() یک صف جداگانه ایجاد می‌کند که کنترل دقیق‌تری بر بیدار شدن نخ‌ها نسبت به صف تکی synchronized می‌دهد.

کدام Lock را برای برنامه موبایل انتخاب کنیم؟

برای Android با کوروتین‌ها از Mutex از kotlinx.coroutines استفاده کنید — آن کوروتین را متوقف می‌کند نه نخ را. برای iOS با Swift 5.7+ actors ترجیح داده می‌شوند که به طور خودکار دسترسی به وضعیت را همگام‌سازی می‌کنند. ReentrantLock را برای کد قدیمی و سناریوهای سطح پایین نگه دارید.

خلاصه

  • Lock — رابط مدیریت صریح قفل‌ها از java.util.concurrent.locks.
  • ReentrantLock — پیاده‌سازی اصلی با پشتیبانی از قفل مجدد و عدالت.
  • ReadWriteLock قفل‌ها را برای سناریوهای read-heavy به خواندن و نوشتن تقسیم می‌کند.
  • StampedLock خواندن خوش‌بینانه را برای حداکثر کارایی اضافه می‌کند.
  • تایم‌اوت‌ها و Condition — مزایای کلیدی Lock نسبت به synchronized.
  • Deadlock با ترتیب ثابت قفل کردن و استفاده از tryLock جلوگیری می‌شود.
  • در توسعه موبایل کوروتین‌ها (Android) و actors (iOS) توصیه می‌شوند.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید