Synchronized یک مکانیزم همگامسازی داخلی در زبان Java است که دسترسی انحصاری به بخشهای بحرانی کد را فراهم میکند. به گزارش Oracle، 2024، اصلاحکننده synchronized تضمین میکند که تنها یک رشته میتواند متد یا بلوک مشخصشده را در یک لحظه خاص اجرا کند. این مکانیزم بر پایه مانیتورها — مفهوم بنیادی سیستمهای عامل — استوار است که عملکرد صحیح برنامههای چندرشتهای را در همه سطوح پیچیدگی تضمین میکند.
نکات اصلی
Synchronized یک کلمه کلیدی در Java است که تضمین میکند تنها یک رشته در یک زمان بخش محافظتشده کد را اجرا میکند و از خراب شدن دادهها در دسترسی همزمان جلوگیری مینماید. این کلمه از نسخه اول Java وجود داشته و سادهترین راه برای تضمین ایمنی رشتهای برای توسعهدهندگان در هر سطحی باقی مانده است.
اصلاحکننده synchronized دو مسئله را حل میکند: انحصار متقابل (mutual exclusion) و قابلیت مشاهده تغییرات (visibility). وقتی یک رشته از بلوک synchronized خارج میشود، تمام تغییرات برای رشتههای دیگری که وارد بلوک همگامسازی شده روی همان شیء میشوند، قابل مشاهده خواهد بود.
Synchronized میتواند به کل یک متد یا به یک بلوک کد دلخواه با مشخص کردن شیء-مانیتور اعمال شود. در هر دو حالت، JVM دستورالعملهای monitorenter و monitorexit را در سطح بایتکد وارد میکند.
در برنامههای چندرشتهای بدون همگامسازی، شرایط رقابتی (race condition) رخ میدهد — زمانی که دو رشته به طور همزمان دادههای یکسانی را تغییر میدهند و به نتایج غیرقابل پیشبینی منجر میشوند. Synchronized اولین و اصلیترین ابزار Java برای مقابله با این مشکل شد و نحوی ساده و اعلامی را ارائه کرد که برای هر توسعهدهندهای قابل دسترس است.
مکانیزم synchronized بر مفهوم مانیتور استوار است — یک اولیه همگامسازی سطح بالا که در هر شیء Java تعبیه شده است. مانیتور هنگام اولین استفاده از بلوک synchronized روی یک شیء به آن متصل میشود.
هر شیء در Java یک مانیتور مرتبط دارد. وقتی یک رشته وارد بلوک synchronized میشود، مانیتور شیء را تصاحب میکند. اگر مانیتور قبلاً توسط رشته دیگری اشغال شده باشد، رشته تا زمان آزادسازی مسدود میشود. در بایتکد، این با جفت دستورالعمل monitorenter و monitorexit مطابقت دارد.
JVM synchronized را از طریق چند سطح بهینه میکند: biased locking (قفل متمایل) برای دسترسی تکرشتهای، lightweight locking (قفل سبک) برای رقابت کم و heavyweight locking (قفل سنگین) برای رقابت شدید با مشارکت سیستمعامل. این سطوح بدون تغییر کد، عملکرد را افزایش میدهند.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Synchronized رابطه happens-before را برقرار میکند: همه اقدامات در یک رشته قبل از خروج از بلوک synchronized برای رشته دیگر پس از ورود به بلوک همگامسازی شده روی همان شیء قابل مشاهده است. این نه تنها انحصار متقابل، بلکه سازگاری دادهها را برای همه رشتهها تضمین میکند.
Java دو روش برای اعمال synchronized ارائه میدهد: در سطح متد و در سطح بلوک. انتخاب بین آنها بر عملکرد و دانهبندی همگامسازی تأثیر میگذارد.
با علامتگذاری یک متد با اصلاحکننده synchronized، آن را به طور خودکار روی نمونه فعلی (برای متد معمولی) یا روی شیء Class (برای متد ایستا) همگامسازی میکنید. این سادهترین راه برای تضمین انحصار متقابل است، اما اغلب اگر بخش بحرانی تنها بخش کوچکی از متد باشد و بقیه کد نیازی به همگامسازی نداشته باشد، بیش از حد است.
بلوک synchronized کنترل دقیقی ارائه میدهد: شما شیء-مانیتور را مشخص میکنید و فقط بخش لازم کد را همگامسازی میکنید و بقیه متد را خارج از قفل قرار میدهید. این زمان نگهداری مانیتور را به حداقل میرساند و عملکرد کلی برنامه را در محیط چندرشتهای بهبود میبخشد، زیرا رشتههای دیگر میتوانند کد نامرتبط را به صورت موازی اجرا کنند بدون اینکه منتظر آزاد شدن مانیتور بمانند.
class DataProcessor {
private final Object lock = new Object();
public void process() {
// کد خارج از بخش بحرانی - بدون همگامسازی
prepareData()
synchronized (lock) {
// فقط این بلوک محافظت میشود
updateSharedState()
}
// ادامه بدون قفل
cleanup()
}
}
| معیار | متد synchronized | بلوک synchronized |
|---|---|---|
| مانیتور | this (نمونه) یا Class | هر شیء دلخواه |
| دانهبندی | کل متد | فقط کد مورد نیاز |
| خوانایی | بالا | متوسط |
| عملکرد | پایینتر برای متد بزرگ | بالاتر برای بخش بحرانی کوچک |
در توسعه Android، synchronized به طور گسترده برای محافظت از SharedPreferences، دسترسی به پایگاه داده و کامپوننتهای UI استفاده میشود. با این حال، استفاده از آن در رشته اصلی به دلیل خطر هنگ کردن رابط کاربری به شدت توصیه نمیشود.
SharedPreferences در Android ایمنی رشتهای پایهای را فراهم میکند، اما هنگام ویرایش توسط چندین رشته از طریق Editor ممکن است همگامسازی خارجی لازم باشد. بلوک synchronized با یک شیء قفل جداگانه سازگاری تغییرات را تضمین میکند.
class PreferencesManager(private val prefs: SharedPreferences) {
private val lock = Any()
fun writeToken(token: String) {
synchronized (lock) {
prefs.edit()
.putString("auth_token", token)
.apply()
}
}
}
محدودیت اصلی synchronized در Android — مسدود کردن رشته. برخلاف کوروتینها با Mutex، synchronized کل رشته سیستم را مسدود میکند. در رشته اصلی این باعث ANR میشود. در توسعه مدرن Android توصیه میشود synchronized با کوروتینها (suspend Mutex) یا انواع اتمی (AtomicInteger) جایگزین شود.
Java و Kotlin مدرن چندین جایگزین برای synchronized ارائه میدهند که هر کدام مسائل مشابه را با محدودیتهای کمتر یا عملکرد بهتر حل میکنند.
رابط Lock با پیادهسازیهای ReentrantLock و ReadWriteLock مهلتهای زمانی، انتظار قابل قطع و چندین صف Condition را فراهم میکند. این از synchronized انعطافپذیرتر است اما نیاز به آزادسازی صریح در finally دارد که خطر خطا را در صورت فراموشی unlock افزایش میدهد.
کلاسهای AtomicInteger، AtomicLong، AtomicReference و دیگران از الگوریتمهای Lock-Free مبتنی بر CAS (Compare-And-Swap) استفاده میکنند. آنها در سناریوهای با رقابت متوسط به طور قابل توجهی سریعتر از synchronized هستند زیرا رشتهها را مسدود نمیکنند، بلکه تلاشهای مجدد خوشبینانه انجام میدهند و نیازی به تغییر زمینه توسط هسته سیستمعامل ندارند.
ThreadLocal یک رویکرد جایگزین ارائه میدهد: هر متغیر ThreadLocal در چارچوب یک رشته ایزوله شده و برای خواندن و نوشتن نیازی به همگامسازی ندارد. این نیاز به synchronized را برای دادههایی که نباید بین رشتهها به اشتراک گذاشته شوند کاملاً از بین میبرد. ThreadLocal به طور فعال در فریمورکها (Spring، Hibernate) برای ذخیره زمینه تراکنشها و جلسات استفاده میشود.
در پروژههای Kotlin برای Android، جایگزین synchronized Mutex از kotlinx.coroutines است. این رشته سیستم عامل را مسدود نمیکند، بلکه کوروتین را تا زمان آزاد شدن قفل معلق میکند — این امکان استفاده مؤثر از رشتههای حوضچه و جلوگیری از ANR را هنگام انتظار طولانی برای آزادسازی منبع فراهم میکند.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
val mutex = Mutex()
var counter = 0
suspend fun safeIncrement() {
mutex.withLock {
counter++
}
}
عملکرد synchronized در نسخههای اخیر Java به طور قابل توجهی تغییر کرده است. قبلاً آن را یک مکانیزم «سنگین» میدانستند، اما JVM مدرن بیشتر سربارها را با بهینهسازیهای پیشرفته کامپایلر JIT حذف کرده است. بیایید دقیقاً بررسی کنیم که ماشین مجازی چگونه کد همگامسازی شده را در زمان اجرا تسریع میکند.
کامپایلر JIT JVM چندین بهینهسازی را اعمال میکند: biased locking اگر قفل همیشه توسط یک رشته تصاحب شود، همگامسازی را حذف میکند؛ lock coarsening بلوکهای synchronized مجاور را در یکی ادغام میکند؛ lock elimination اگر شیء فقط برای یک رشته قابل دسترس باشد، همگامسازی را حذف میکند. این بهینهسازیها synchronized را در رقابت کم عملاً رایگان میکند.
JVM سطح رقابت را برای هر شیء تعیین میکند: در عدم رقابت biased locking فعال میشود، با ظهور رشته دوم قفل به حالت lightweight با spin-waiting منتقل میشود و تنها در انتظار طولانی — به heavyweight با mutex سیستم. این escalation به طور خودکار رخ میدهد و توسعهدهنده نیازی به انتخاب دستی استراتژی ندارد.
در بنچمارکهای مدرن (Java 17+) synchronized عملکرد قابل مقایسه با ReentrantLock در رقابت کم و متوسط نشان میدهد. در رقابت بالا، Lock ممکن است به دلیل صف انتظار کارآمدتر با پشتیبانی از مهلت زمانی و وقفه مزیت داشته باشد. برای سیستمهای با بار بالا که رقابت ثابت است، ReentrantLock در حالت fair رفتار قابل پیشبینیتری ارائه میدهد.
کلاسهای اتمی (AtomicInteger، AtomicReference) برای شمارندهها و پرچمهای ساده به دلیل پیادهسازی Lock-Free روی CAS سریعترین باقی میمانند. آنها اصلاً رشتهها را مسدود نمیکنند — در صورت تعارض، عملیات به سادگی در یک حلقه تکرار میشود. این باعث افزایش عملکرد 3-5 برابری در مقایسه با synchronized در عملیات افزایش شمارنده با 4-8 رشته میشود.
سوالات متداول
Synchronized هم انحصار متقابل و هم قابلیت مشاهده را تضمین میکند. Volatile فقط قابلیت مشاهده تغییرات را تضمین میکند — نوشتن در متغیر volatile برای همه رشتهها قابل مشاهده است، اما از تغییر همزمان جلوگیری نمیکند، یعنی در برابر شرایط رقابتی محافظت نمیکند.
بله، deadlock در همگامسازی تو در تو با ترتیب متفاوت مانیتورها ممکن است. مثلاً یک رشته synchronized(a) { synchronized(b) } و دیگری synchronized(b) { synchronized(a) } فراخوانی میکند. از بلوکهای synchronized تو در تو خودداری کنید یا یک ترتیب ثابت مانیتورها تعیین کنید.
مانیتور یک مکانیزم همگامسازی است که با هر شیء Java مرتبط است. این تضمین میکند که تنها یک رشته کد synchronized را روی آن شیء اجرا میکند. مانیتور شامل قفل، صف انتظار و مجموعه رشتههایی است که منتظر اعلان از طریق wait/notify هستند.
در نسخههای مدرن Java (17+) synchronized به لطف بهینهسازیهای JIT (biased locking, lock coarsening) از نظر عملکرد از Lock عقب نمیماند. Lock نه به خاطر سرعت، بلکه به دلیل قابلیتهای اضافی ترجیح داده میشود: مهلت زمانی، انتظار قابل قطع و چندین Condition.
متد ایستای synchronized از مانیتور شیء Class آن کلاس استفاده میکند، نه نمونه. این بدان معناست که همگامسازی تمام نمونههای کلاس را پوشش میدهد. متدهای غیرایستا و ایستای synchronized از مانیتورهای متفاوتی استفاده میکنند و یکدیگر را مسدود نمیکنند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید