Lock مکانیزم همگامسازی است که دسترسی انحصاری به بخشهای بحرانی کد در برنامههای چندنخی را فراهم میکند. به گفته Oracle, 2024، رابط Lock کنترل همگامسازی انعطافپذیرتری نسبت به بلوکهای synchronized سنتی ارائه میدهد، از جمله تلاش برای قفل با تایماوت و پشتیبانی از چندین صف انتظار.
نکات اصلی
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()، پرچم بازنشانی میشود و یکی از نخهای منتظر بیدار میشود.
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 — پیادهسازی پایه و پرکاربردترین Lock است. از قفل مجدد توسط همان نخ پشتیبانی میکند: اگر نخ قبلاً قفل را در اختیار دارد، فراخوانی مجدد lock() آن را مسدود نمیکند. این کار از deadlock در فراخوانیهای بازگشتی جلوگیری میکند.
ReadWriteLock قفلها را به دو حالت تقسیم میکند: خواندن و نوشتن. چندین نخ میتوانند همزمان قفل خواندن را نگه دارند، اما نوشتن نیاز به دسترسی انحصاری دارد. این کار هنگام خواندن مکرر و نوشتن نادر، کارایی را به طور قابل توجهی افزایش میدهد.
StampedLock — جدیدترین پیادهسازی که در جاوا 8 ظاهر شد. از سه حالت پشتیبانی میکند: نوشتن، خواندن و خواندن خوشبینانه. خواندن خوشبینانه نخهای دیگر را مسدود نمیکند و پس از خواندن اعتبار دادهها را بررسی میکند که 10-20% افزایش کارایی نسبت به ReadWriteLock ایجاد میکند.
| قفل | نسخه جاوا | حالتها | کارایی |
|---|---|---|---|
| ReentrantLock | Java 5 | انحصاری | بالا |
| ReadWriteLock | Java 5 | خواندن + نوشتن | متوسط |
| StampedLock | Java 8 | خواندن + نوشتن + optimistic | بسیار بالا |
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، کشها و فایلها مفید است. مهم است به خاطر داشته باشید: هرگز قفل را در نخ اصلی نگیرید. برای کد ناهمگام، کوروتینها و Mutex از kotlinx.coroutines که نخ را مسدود نمیکنند بلکه کوروتین را متوقف میکنند، ترجیح داده میشوند.
در iOS، Lock استاندارد از 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
}
}
برای جلوگیری از deadlock، ترتیب یکسان قفل کردن همه قفلها را در پروژه رعایت کنید. به جای lock() در جاهایی که قفل طولانی مدت ممکن است، از tryLock با تایماوت استفاده کنید. استفاده از الگوریتمهای Lock-Free (AtomicReference, ConcurrentHashMap) را به جای قفلهای سنتی در نظر بگیرید.
استفاده از Lock نیاز به انضباط و رعایت چندین قانون دارد که از deadlock و کاهش کارایی جلوگیری میکند. این روشها توسط جامعه جاوا در طول 20 سال استفاده از بسته java.util.concurrent ایجاد شدهاند.
مهمترین الگو — lock در finally. صرفنظر از اینکه بخش بحرانی با موفقیت یا با استثنا به پایان رسیده است، قفل باید آزاد شود. این تضمین میکند که نخهای دیگر به دلیل یک خطا برای همیشه مسدود نخواهند شد. در Kotlin این الگو به طور ظریفی از طریق extension withLock حل میشود.
بخش بحرانی باید حداکثر کوتاه باشد. هرگز در داخل قفل عملیات ورودی-خروجی، درخواستهای شبکه یا محاسبات طولانی انجام ندهید. اگر نیاز به خواندن دادهها از سرور دارید، ابتدا آنها را دریافت کنید و سپس فقط برای بهروزرسانی وضعیت مشترک قفل را بگیرید. این کار رقابت را کاهش میدهد و توان عملیاتی سیستم را افزایش میدهد.
برای جلوگیری از deadlock هنگام کار با چندین Lock، ترتیب سراسری قفل کردن را در کل پروژه تعیین کنید. اگر ابتدا lockA و سپس lockB گرفته میشود — هر ترتیب معکوس باید توسط قوانین code review ممنوع شود. برای بررسی خودکار از تحلیلگرهای ایستا مانند SpotBugs و IntelliJ Inspections استفاده کنید.
سوالات متداول
Lock — رابط صریح با امکان تایماوت و انتظار قابل قطع. synchronized به طور خودکار مانیتور را قفل و آزاد میکند اما اجازه استفاده از tryLock، lockInterruptibly و چندین Condition را نمیدهد. Lock انعطافپذیرتر است اما نیاز به آزادسازی دستی در finally دارد.
قفل عادلانه ترتیب FIFO دسترسی را تضمین میکند: نخی که بیشترین انتظار را کشیده، اولین بار قفل را دریافت میکند. قفل ناعادلانه ممکن است دسترسی را به نخ جدید دور زدن صف بدهد که توان عملیاتی را افزایش میدهد اما میتواند باعث گرسنگی نخهای منتظر شود.
ترتیب ثابت قفل کردن همه قفلها را رعایت کنید، به جای lock بدون شرط از tryLock با تایماوت استفاده کنید و تعداد قفلهای همزمان را به حداقل برسانید. استفاده از ساختارهای داده Lock-Free نیز خطر deadlock را کاهش میدهد.
Condition — مشابه wait/notify برای Lock است که امکان سازماندهی چندین صف انتظار مستقل را فراهم میکند. هر فراخوانی newCondition() یک صف جداگانه ایجاد میکند که کنترل دقیقتری بر بیدار شدن نخها نسبت به صف تکی synchronized میدهد.
برای Android با کوروتینها از Mutex از kotlinx.coroutines استفاده کنید — آن کوروتین را متوقف میکند نه نخ را. برای iOS با Swift 5.7+ actors ترجیح داده میشوند که به طور خودکار دسترسی به وضعیت را همگامسازی میکنند. ReentrantLock را برای کد قدیمی و سناریوهای سطح پایین نگه دارید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید