Multithreading در جاوا – سطح پیشرفته (کلاس‌های Semaphore و CountDownLatch)

java-multithreading-Semaphore-countDownLatch

سلام در جلسه ی قبلی آموزش با پکیج جدیدی در برنامه نویسی همروند آشنا شدیم و کاربرد دو کلاس Exchancher و CyclicBarrier را بررسی کردیم.

در این بخش قصد داریم با دو کلاس باقی مانده از بخش قبل آشنا و مثال هایی از آن ها را بررسی کنیم.

کلاس Semaphore

با استفاده از شی این کلاس می توان دسترسی به منابع مشترک را کنترل کرد.

استفاده از این کلاس به صورت زیر است:

Semaphore objectName = new Semaphore(int permits)

در سازنده مقدار int را عدد Semaphore (حالت) گویند که با استفاده از آن می توان تعیین کرد که به طور همزمان حداکثر چند نخ اجازه دارند با منبع مشترک کار کنند.

این کلاس دارای متدهای زیادی است که مهم ترین آن ها acquire و release است.

هر Thread قبل از استفاده از منبع مشترک باید متد acquire را فراخوانی کند که در نتیجه ی آن از عدد Semaphore یکی کم می شود، فرض کنید که شی زیر را از کلاس Semaphore ایجاد کرده اید:

Semaphore s = new Semaphore(2)

در اینجا عدد Semaphore، مقدار ۲ اعلام شده یعنی دو Thread اجازه دارند همزمان با یک منبع مشترک کار کنند.

وقتی یکی از Thread ها متد acquire را فراخوانی کند این عدد 1 می شود و با فراخوانی دوباره ی acquire توسط Thread دیگر مقدار این عدد صفر می شود حال اگر Thread شماره 3 نیز acquire را فراخوانی کند بلاک می شود چون عدد Semaphore صفر است و Thread 3 تا زمانی که عدد Semaphore آزاد نشده در حالت بلاک باقی می ماند.

تا اینجا دیدم که با فراخوانی متد acquire توسط یک Thread، یک مقدار از عدد Semaphore کم می شود؛ حال این مقدار بعد از پایان کار Thread بر روی شی مشترک، باید دوباره آزاد شود که این کار توسط متد release انجام می شود بدین صورت که Thread جاری بعد از اتمام کار بر روی شی مشترک باید متد release را فراخوانی کند.

به مثال زیر توجه کنید:

public class MainClass_1 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);
        for (int i = 0; i < 10; i++) {
            new SemaphoreExample(semaphore).start();
        }
    }  
}
class SemaphoreExample extends Thread{
    private Semaphore semaphore;

    public SemaphoreExample(Semaphore semaphore) {
        this.semaphore=semaphore;  
    }
    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println("Thread ID= "+Thread.currentThread().getId()+"\t"+"Start Process");
            Thread.sleep(1000);
            System.out.println("Thread ID= "+Thread.currentThread().getId()+"\t"+"Finished Process");
        } catch (InterruptedException ex) {}
        semaphore.release();
    }

در کلاس SemaphoreExample یک Semaphore تعریف شده و در سازنده ی کلاس مقدار دهی می شود در متد run ابتدا متد acquire فرخوانی شده است و ID نخ جاری نیز چاپ می شود در این بین یک sleep قرار داده شده تا روند اجرای برنامه قابل مشاهده باشد. در واقع sleep شبیه ساز یک پردازش است که مدت زمانی طول می کشد و در ادامه دوباره ID نخ جاری و پایان عملیات چاپ می شود و در پایان کل عملیات، Thread جاری متد release را فراخوانی می کند تا عدد Semaphore که قبلا در اختیار گرفته را برای دسترسی Thread های دیگر آزاد کند.

در متد Main نیز یک شی Semaphore با عدد 1 تعریف شده و در یک حلقه ی for، ده Thread ایجاد و Start شده اند. با توجه به این که عدد Semaphore برابر ۱ در نظر گرفته شده، در یک زمان مشخص فقط یک Thread اجازه ی دستیابی به شی مشترک را دارا می باشد.

در واقع وقتی یک Thread متد acquire را فراخوانی کرد عدد Semaphore صفر می شود و هیچ Thread دیگری اجازه ی دسترسی به آن شی را ندارد تا زمانی که Thread جاری متد release را فراخوانی کند. برنامه را اجرا و خروجی را مشاهده کنید خواهید دید که در زمانی مشخص فقط یک Thread در حال اجرا خواهد بود که کاملا شبیه به استفاده از بلوک synchronized است به این صورت که در ابتدای ورود، قفل شی را گرفته و در زمان خروج آن را آزاد می کند.

خروجی
خروجی

توجه: استفاده از متد acquire باعث پرتاب خطای InterruptedException می شود.

کلاس CountDownLatch

این کلاس نیز یک Synchronizer است که به چند Thread اجازه می هد تا پایان یک شمارش معکوس متوقف شوند به بیانی دیگر وقتی قرار است در چند Thread عملیاتی انجام شود تا امکان ادامه ی Thread های دیگر فراهم شود، از این کلاس استفاده می شود.

استفاده از این کلاس به شکل زیر است:

 CountDownLatch cdl = new CountDownLatch(int count)

این کلاس تنها یک سازنده دارد که پارامتر ورودی آن داده ای از نوع int می باشد که مشخص می کند چند Thread باید اجازه دهند تا Thread دیگری به ادامه ی فعالیت بپردازد.

متدهای اصلی این کلاس await و countDown می باشد که await منتظر پایان شمارش معکوس می شود و متد countDown شمارش معکوس را یک واحد افزایش می دهد برای درک بهتر به مثال زیر توجه کنید:

فرض کنید شی CountDownLatch به صورت زیر ساخته شده است:

CountDownLatch latch = new CountDownLatch(3)

مشاهده می کنید که عدد داخل آن 3 اعلام شده حال فرض کنید در Thread_1 قرار است عملیات چاپ یک جمله انجام شود ولی قبل از آن متد await فراخوانی شده است.

#Thread_1
latch.await();
System.out.println("Hi this is a test");

با توجه به این که عدد CountDownLatch سه است پس باید متد countDown سه بار فراخوانی شود. حال این سه بار فراخوانی می تواند توسط سه Thread مختلف یا توسط یک Thread در سه نقطه انجام شود تا نقطه اجرای برنامه که در خط latch.await گیر کرده (منتظر است)، آزاد شود و ادامه ی روند اجرا، انجام شود.

به مثال زیر توجه کنید.

public class MainClass_2 {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(4); 
        CountDownLatchExample T = new CountDownLatchExample(latch);
        ReleaserClass R = new ReleaserClass(latch);
        T.start();
        R.start();
    }  
}
class CountDownLatchExample extends Thread{
    CountDownLatch latch ;

    public CountDownLatchExample(CountDownLatch latch) {
        this.latch=latch;
    }
    @Override
    public void run() {
        try {
            latch.await();
            Thread.sleep(1000);
        } catch (InterruptedException ex) {}
        System.out.println("Thread ID: "+Thread.currentThread().getId()+"\t"+"CountDownLatchExample Released");
    }  
}
class ReleaserClass extends Thread{
    CountDownLatch latch ;

    public ReleaserClass(CountDownLatch latch) {
        this.latch=latch;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            latch.countDown();
            System.out.println("Thread ID: "+Thread.currentThread().getId()+"\t"+"countDown "+latch.getCount());
            Thread.sleep(1000);
            latch.countDown();
            System.out.println("Thread ID: "+Thread.currentThread().getId()+"\t"+"countDown "+latch.getCount());
            Thread.sleep(1000);
            latch.countDown();
            System.out.println("Thread ID: "+Thread.currentThread().getId()+"\t"+"countDown "+latch.getCount());
            Thread.sleep(1000);
            latch.countDown();
            System.out.println("Thread ID: "+Thread.currentThread().getId()+"\t"+"countDown "+latch.getCount());
        } catch (InterruptedException ex) {}  
    }
}

دو کلاس ReleaserClass و CountDownLatchExample دارای یک شی CountDownLatch  مشترک هستند که در سازنده ی آن ها مقدار دهی شده است در متد run کلاس CountDownLatchExample  قبل از چاپ پیغام متد await استفاده شده بنابراین Thread جاری در این نقطه گیر می کند تا در جایی دیگر متد countDown به مقدار لازم فراخوانی شود. چون در Main شی اصلی CountDownLatch با عدد 4 مشخص شده پس 4 بار باید متد countDown فراخوانی شود تا Thread کلاس CountDownLatchExample آزاد شود و ادامه ی اجرا خود را آغاز کند.

در کلاس ReleaserClass، طی 4 مرحله متد countDown فراخوانی شده و عدد Count آن نیز توسط متد getCount اعلام می شود. به محض اتمام شمارش معکوس و رسیدن آن به صفر Thread کلاس CountDownLatchExample به ادامه ی اجرا می پردازد.

خروجی را اجرا کنید و به ID نخ ها و عدد Count دقت کنید.

تذکر: استفاده از متد sleep فقط برای دیدن روند تغییرات بوده و کاربرد دیگری ندارد.

خروجی
خروجی

برای درک بهتر این کلاس بسیار مهم به مثال دوم که ترکیبی از Semaphore و CountDownLatch توجه کنید.

public class MainClass_2 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);
        CountDownLatch latch = new CountDownLatch(5); 
        for (int i = 0; i < 5; i++) {
            new Thread(new Worker(latch,semaphore)).start();
        }
        try {
            Thread.sleep(100);
            latch.await();
        } catch (InterruptedException ex) {}
        System.out.println("Thread ID: "+Thread.currentThread().getId()+"\t"+" has finished"); 
    }  
}
class Worker implements Runnable 
{ 
    private CountDownLatch latch; 
    private Semaphore semaphore;
  
    public Worker( CountDownLatch latch,Semaphore semaphore) 
    { 
        this.latch = latch; 
        this.semaphore=semaphore;
    } 
    @Override
    public void run() 
    { 
        try
        { 
            semaphore.acquire();
            Thread.sleep(1500); 
            latch.countDown(); 
            System.out.println("Thread ID: "+Thread.currentThread().getId()+"\t"+"Count:"+latch.getCount()+ " finished"); 
            semaphore.release();
        } 
        catch (InterruptedException e) { } 
    } 
}

کاربرد Semaphore که کاملا مشخص است در واقع کار synchronized انجام می شود اما به شکل بسیار بهتر و بهینه تر اما کاربرد کلاس CountDownLatch کاملا شبیه به مثال قبل است فقط در اینجا عدد Count پنج در نظر گرفته شده و این پنج مرتبه توسط پنج Thread مختلف countDown می شود (بر خلاف مثال قبل) و در اینجا متد await در کلاس main توسط Thread اصلی اجرا شده، برنامه را run کنید و خروجی را با دقت بررسی نمایید. صد البته به عدد count و ID نخ توجه کنید.

خروجی
خروجی

توجه: استفاده از متد await باعث پرتاب خطای InterruptedException می شود.

این بخش از آموزش هم به پایان رسید ولی توجه داشته باشید برای یادگیری بهتر این مباحث، حتما مثال های بیشتر و پیچیده تر حل کنید، در بخش بعدی با کلاس ها و عملیات اتمیک (Atomic) آشنا می شویم.

موفق باشید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش برنامه نویسی MultiThreading در جاوا توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.