پیاده سازی Threadها و مدیریت همزمانی در جاوا

پیاده سازی Threadها و مدیریت همزمانی در جاوا

نوشته شده توسط: احمدرضا صدیقی
تاریخ انتشار: ۱۰ آذر ۱۳۹۷
آخرین بروزرسانی: 17 تیر 1403
زمان مطالعه: 10 دقیقه
۴
(۱)

مقدمه

Thread ها یکی از بخش‌های جذاب در برنامه نویسی مدرن است. اینکه بتوانیم چندین کار همزمان را به صورت موازی توسط پردازنده سیستم اجرا کنیم بسیار جالب است و البته در بسیار از موارد «یک نیاز» است. مثلا یک برنامه وب را تصور کنیم که به کاربران خود امکان می‌دهد تا از طریق مرورگرهای خود وارد برنامه شوند، منطقی است که انتظار داشته باشیم کاربران به جای اینکه به ترتیب در یک صف سرویس داده شوند، به صورت موازی و همزمان پاسخ داده شوند. به این ترتیب یک کاربر مجبور نیست تا منتظر اتمام کار یک کاربر دیگر که زمان‌بر هم هست بماند.
یک Thread اصلاحا به بخشی از یک برنامه گفته می‌شود‌ که امکان اجرا شدن به موازای بقیه بخش‌های برنامه را دارد. هر Thread در یک زمانی شروع به اجرا شدن می‌کند، و پس از اجرای عملیاتی که در آن تعریف شده است، آن Thread نیز به پایان حیات خود رسیده است.

پیاده سازی Thread

در جاوا دو راه برای پیاده سازی Thread وجود دارد.
• توسعه کلاس java.lang.Thread
• پیاده سازی اینترفیس java.lang.Runnable
به کلاس MyFirstThread که در زیر پیاده سازی شده است توجه کنید.

public class MyFirstThread extends Thread {
    public void run(){
      //do something
    }
}

 در اینجا کلاس Thread توسعه داده شده و متد ()run در آن پیاده سازی شده است. به شکل مشابه، کلاس MySecondThread که در زیر نشان داده شده است اینترفیس Runnable را پیاده سازی کرده است و متد run() آنرا نیز پیاده سازی نموده است.

public class MySecondThread implements Runnable {
    public void run(){
      //do something
    }
}

 در هر دو فرم از پیاده سازی Thread، می بایست منطق یا عملیاتی که Thread می‌بایست انجام دهد در متد ()run پیاده سازی شود.
اگر از فرم اول برای پیاده سازی Thread استفاده شود، اجرا آن صرفا با فراخوانی متد ()start از آبجکت Thread امکانپذیر است.

MyFirstThread th1 = new MyFirstThread();
th1.start();

 به محض اجرای متد ()start انتظار داریم که اجرای برنامه در خطوط بعد از متد ()start ادامه یابد و همزمان منطق تعریف شده در متد ()run نیز اجرا شود.
اگر از فرم دوم برای پیاده سازی Threadها (یعنی پیاده سازی اینترفیس Runnable) استفاده شود، اجرای Thread به صورت زیر خواهد بود.

MySecondThread mst = new MySecondThread();
Thread th2 = new Thread(mst);
th2.start();

 کنترل همزمانی

وقتی دو یا چند Thread از یک آبجکت مشترک استفاده می کنند ممکن است باعث تداخل در کار یکدیگر شوند. بنابراین باید به روشی دسترسی آنها به منبع مشترک را کنترل کرد به گونه‌ای که دو Thread بدون تداخل در عملکرد یکدیگر به درستی به اجرای خود ادامه دهند. به این فرایند، کنترل همزمانی گفته می شود.
کنترل همزمانی در حقیقت از وظایف JVM است، آنچه به ما به عنوان طراح یا برنامه نویس مربوط می‌شود مشخص کردن نقاطی از آبجکت مشترک است که در زمان اجرای Threadها ممکن است منجر به تداخل عملکرد آنها شود. برای درک این موضوع، کلاس Counter زیر را ملاحظه کنید.

public class Counter {
int index=0;
public void incrementAndPrint(){
index++;
System.out.println(index);
}
}

 این کلاس دارای یک فیلد index و یک متد ()incrementAndPrint است که با هر فراخوانی مقدار فیلد index را یک واحد افزایش داده و در کنسول برنامه چاپ می‌کند. حال تصور کنید که این متد همزمان توسط دو Thread فراخوانی شود. در فراخوانی اول، مقدار index یک واحد افزایش می‌دهد و درست قبل از اینکه مقدار index در کنسول چاپ شود، فراخوانی دوم توسط Thread دوم شروع می‌شود و مقدار index یک واحد دیگر افزایش می‌یابد. سپس فراخوانی اول مقدار index را که در حال حاضر ۲ است در کنسول برنامه چاپ می‌کند و خاتمه می‌یابد و سپس فراخوانی دوم نیز همین مقدار ۲ را در کنسول نمایش می‌دهد. این درحالیست که انتظار داریم که با فراخوانی اول مقدار ۱ و با فراخوانی دوم مقدار ۲ در کنسول برنامه چاپ شوند.
در واقع برای اینکه متد ()incrementAndPrint درست کار کند Threadها نباید به صورت همزمان این متد را فراخوانی کنند و اگر فراخوانی کنند می‌باید با استفاده از روشی مانع از فراخوانی همزمان این متد شویم. این کار با افزودن کلمه کلیدی Synchronized به تعریف متد ()incrementAndPrint امکانپذیر است.

public class Counter1 {
int counter = 0;
public synchronized int incrementAndReturn() {
counter++;
return counter;
}
}

 کلمه Synchronized نشانه‌ای است که به JVM اعلان می‌کند متد ()IncrementAndPrint نمی‌تواند همزمان توسط دو یا چندین Thread فراخوانی شود و درصورتیکه فراخوانی شود باید Threadها یکی یکی این متد را فراخوانی کنند و تا زمانیکه یک فراخوانی به اتمام نرسیده Thread بعدی نمی‌تواند این متد را فراخوانی کند.
کنترل همزمانی با استفاده از یک مکانیسم قفل گذاری توسط JVM انجام می‌ شود. براساس این مکانیسم وقتی یک متد Synchronized فراخوانی می‌شود، JVM روی آبجکت آن متد (آبجکتی که متد Synchronized به آن تعلق دارد) یک قفل قرار می دهد، البته مشروط به اینکه قبلا روی آن آبجکت قفلی قرار نگرفته باشد. پس از پایان فراخونی متد Synchronized، قفل از روی آبجکت برداشته می شود. تا مادامیکه قفل روی آبجکت وجود داشته باشد، هیچ Thread دیگری نمی تواند آن متد Synchronized یا متدهای Synchronized دیگر آن آبجکت را فراخوانی کند.
حال که مفهوم Synchronized و مکانیسم قفل گذاری را آموختید به کلاس ProcessorThread که در زیر پیاده سازی شده است توجه کنید

public class ProcessorThread extends Thread {
Counter counter;
public ProcessorThread(Counter counter) {
this.counter = counter;
}
public void run(){
for(int i=0; i<10; i++){
counter.incrementAndPrint();
}
}
}

 این کلاس Thread، فیلدی از Counter دارد و در اجرای خود ده بار متد() IncrementAndPrint را فراخوانی می‌کند. اگر دو آبجکت از روی این کلاس با یک آبجکت مشترک از کلاس Counter کار کنند انتظار داریم که تداخلی بین عملکرد آنها ایجاد نشود.

Counter counter = new Counter();
ProcessorThread thread1 = new ProcessorThread(counter);
ProcessorThread thread2 = new ProcessorThread(counter);
thread1.start();
thread2.start();

 با اجرای کد فوق، انتظار داریم که اعداد ۱ تا ۲۰ در کنسول برنامه نمایش داده شوند. درصورتیکه اگر متد IncrementAndPrint() به صورت Synchronized پیاده سازی نشده بود ممکن بود برخی اعداد در بازه ۱ تا ۲۰ اصلا نمایش نیابند و برخی اعداد دوبار نمایش داده شوند.
دقت کنید که بحث همزمانی برای متدهای غیراستاتیک وقتی مطرح است که یک آبجکت مشترک توسط دو یا چندین Thread استفاده شود. بنابراین اگر کد فوق به صورت زیر تغییر یابد اساسا مشکل همزمانی وجود نخواهد داشت زیرا هر Thread با آبجکت Counter جداگانه ای سروکار دارد.

Counter counter1 = new Counter();
Counter counter2 = new Counter();
ProcessorThread thread1 = new ProcessorThread(counter1);
ProcessorThread thread2 = new ProcessorThread(counter2);
thread1.start();
thread2.start();

 همانطور که می‌دانید، می‌توان متدهای استاتیک یک کلاس را بدون ایجاد آبجکت از یک کلاس فراخوانی نمود. با این توصیف، آیا می‌توان متدهای استاتیک را به صورت Synchronized تعریف کرد؟ جواب این سوال مثبت است. در اینصورت و در شرایطی که آبجکتی از آن کلاس برای قفل گذاری ایجاد نشده است از چه آبجکتی برای قفل گذاری و هماهنگی بین Threadها استفاده می‌شود؟ جواب این سوال، کلاسی است که متد استاتیک آن فراخوانی شده است. معنی آن این است که متدهای استاتیک از کلاسی که فراخوانی شده اند و متدهای غیراستاتیک از آبجکتی که فراخوانی شده اند برای قفل گذاری استفاده می کنند. به این ترتیب، از آنجاییکه محل قفل گذاری محل قفل گذاری متدهای استاتیک و غیراستاتیک متفاوت است، یک متد استاتیک می تواند همزمان با یک متد غیراستاتیک فراخوانی شود اما دو متد استاتیک یا دو متد غیراستاتیک نمی‌توانند به صورت همزمان فراخوانی شوند.

بلوک Synchronized

اگرچه می‌توان با افزودن کلمه کلیدی Synchronized به تعریف یک متد، دسترسی همزمانِ چندین Thread به آن متد را کنترل نمود، اما با اینکار موازی سازی و اجرای همزمانِ آن متد از بین رفته است. این بدین معناست که Synchronization نقطه مقابل Threadها قرار دارد زیرا در حالیکه Threadها تلاش می‌کنند تا با همزمان شدن اجرای قسمت‌های مختلف برنامه، کارایی برنامه را افزایش دهند، Synchronization با متوالی کردن اجرای بخش‌های برنامه، اگر نگوییم هدف Threadها را بی اثر می‌کند اما لااقل کم اثر می‌کند. اگر متدی که Synchronized می‌شود طولانی باشد، یعنی بدنه بزرگی داشته باشد دراینصورت حجم زیادی از کد از اجرای همزمان محروم شده است! خوشبختانه در چنین مواقعی با بهبود طراحی می‌توان تا حدودی از اثرات همزمانی کاست. برای این منظور به جای اینکه کل متد به صورت Synchronized تعریف شود می‌توان بخشی از متد که کنترل همزمانی در آن اهمیت دارد را در قالب یک بلوک Synchronized تعریف نمود. در اینصورت بخش‌هایی از متد که خارج از بلوک قرار دارند می‌توانند به صورت همزمان توسط Threadهای مختلف اجرا شوند اما بخش‌های داخل بلوک synchronized از اجرای همزمان توسط Threadهای مختلف محافظت می‌شوند.

synchronized(object){
//statements to be synchronized
}

 بلوک Synchronized می‌تواند در هرجایی از بدنه یک متد که Synchronized نیست ظاهر شود. در این بلوک، بلافاصله بعد از کلمه Synchronized داخل پرانتز نام یک آبجکت آورده می‌شود و سپس جملاتی که باید همزمانی آنها کنترل شود داخل {} قرار می‌گیرند. آبجکتی که داخل پرانتز و بعد از کلمه Synchronized مشخص می‌شود، محلی است که قفل گذاری روی آن انجام می شود. این امکانی است که در متد synchronized وجود ندارد زیرا در متدهای Synchronized قفل روی آبجکتی که متد آن فراخوانی می‌شود قرار می‌گیرد، اما در بلوک Synchronized این امکان وجود دارد تا هر آبجکتی برای قفل گذاری انتخاب شود.
وقتی یک بلوک Synchronized اجرا می‌شود، JVM آبجکتی را که برای قفل گذاری انتخاب شده بررسی می‌کند تا مطئن شود قفلی روی آن وجود ندارد. درصورتیکه آن آبجکت آزاد باشد، روی آن قفل گذاشته می‌شود و بلوک Synchronized اجرا می‌شود. در پایان اجرای بلوک، قفل از روی آبجکت برداشته می‌شود.
بلوک یا متد Synchronized می‌تواند برای کنترل دسترسی همزمان و فراخوانی متدهای آبجکت‌ها استفاده شود. اما سوالی که در اینجا مطرح می‌شود این است که چگونه می‌توان خواندن و نوشتن متغیرها و دسترسی همزمان به آنها را کنترل نمود؟

در مورد آبجکت‌ها، هر تغییری در وضعیت آبجکت برای تمام Threadهای برنامه قابل مشاهده است، به عبارت دیگر تغییر مقدار هر یک از فیلدهای یک آبجکت به مجردی که انجام شود توسط Threadهای دیگر قابل مشاهده و دسترسی است و از این لحاظ نگرانی وجود ندارد. اما در مورد متغیرهایی از جنس داده‌های پایه، به دلیل روشی که جاوا در سازماندهی این داده‌ها در حافظه استفاده می‌کند می‌بایست با استفاده از کلمه کلیدی Volatile که در تعریف آن فیلد استفاده می‌شود، دسترسی همزمان به آن فیلد برای خواندن یا تغییر مقدار آنرا کنترل نمود. به عبارت دیگر، اگر متغیری به صورت Volatile تعریف شده باشد به مجردِ اینکه مقدار آن تغییر کند مقدار آن توسط همه Threadهای برنامه قابل مشاهده خواهد بود و درغیر اینصورت تضمینی برای آن وجود ندارد. کد زیر نحوه بکارگیری Volatile را نشان می‌دهد.

public class SharedObject {
public volatile int counter = 0;
}

 

چه رتبه ای می‌دهید؟

میانگین ۴ / ۵. از مجموع ۱

اولین نفر باش

گوش به زنگ یلدا
title sign
معرفی نویسنده
احمدرضا صدیقی
مقالات
6 مقاله توسط این نویسنده
محصولات
3 دوره توسط این نویسنده
احمدرضا صدیقی

احمدرضا صدیقی متخصص و معمار ارشد جاوا است. از دیگر سوابق حرفه ای او می توان به:معمار ارشد در حوزه جاوا مربوط به پروژه دانشگاه علوم پزشکی، معمار ارشد در حوزه جاوا مربوط به پروژه شرکت خبره پردا، معمار ارشد در حوزه جاوا مربوط به پروژه شرکت کیاتک بنیا، معمار ارشد در حوزه جاوا مربوط به پروژه دانشگاه مالک اشتر، مشاور پروژه‌ ملی طرح جامع مالیاتی، مشاور پروژه‌ ملی وزارت بهداشت، مشاور پروژه‌ بانک ملت، مولف مجموعه کتاب‌های جاوا (فارسی و انگلیسی)، بیش از ۱۲ سال سابقه تدریس جاوا، ارائه فریم‌ورک تخصصی جاوا (اطلس) اشاره کرد.

title sign
معرفی محصول
title sign
دیدگاه کاربران