Multithreading در جاوا - سطح پیشرفته (کلاس های Exchancher و CyclicBarrier)

java-multithreading-advanced-level

سلام در آموزش های قبلی با امکانات اولیه ی جاوا برای برنامه نویسی MultiThread  آشنا شدیم و مثال هایی ساده از آن ها را بررسی کردیم، اما برای استفاده در برنامه های بزرگ خیلی کارآمد نیستند و به کارگیری آن ها با پیچیدگی و سختی همراه است اما جاوا با ارائه ی نسخه ی 5 امکانات بسیار زیادی برای برنامه نویسی MultiThread  معرفی کرد که در ادامه به بررسی مهم ترین آن ها خواهیم پرداخت.

معرفی پکیج Java.util.concurrent

این پکیج برای اولین بار در نسخه ی 5 جاوا معرفی شد و دارای کلاس ها و متدهای بسیار زیادی برای برنامه نویسی multiThread است.

همان طور که از اسم این پکیج پیداست برای تعیین سطح دسترسی ها و همروندی به کار می رود قبلا با استفاده از synchronization ،wait ،notify و... این کار انجام می شد که امکانات سطح پایین به شمار می روند اما پکیج Java.util.concurrent دارای امکانات سطح بالا و بسیار متنوعی برای برای برنامه نویسی همروند است.

کارایی بهتر و قوی تر، پشتیانی بهتر از پردازنده های چند هسته ای و آسان تر کردن برنامه نویسی را می توان بخشی از ویژگی های آن بیان کرد.

آشنایی با مفهوم Thread-safe

در جاوا برخی از کلاس ها به اصطلاح Thread-safe هستند یعنی از اشیای این کلاس ها را می توان به طور مشترک در چند Thread استفاده کرد بدون این که نیاز باشد از Lock یا synchronization استفاده کنیم چون برنامه نویسان جاوا از قبل امکاناتی برای این منظور در نظر گرفته اند.

به عناون مثال کلاس های Vector و StringBuffer به اصطلاح Thread-safe هستند و شی ساخته شده از آن ها می تواند بدون استفاده از اصول قبلی به طور مشترک در چند Thread استفاده شوند بدون آن که مشکلی پیش آید اما در مقابل آن ها کلاس های ArrayList و StringBuilder کلاس های Thread-safe نیستند و اشیای آن در برنامه نویسی همروند باید در بلوک synchronized قرار بگیرند تا از مشکلات احتمالی جلوگیری شود.

به طور کلی زمانی که نوع برنامه نویسی، همروند نیست، بهتر است از کلاس های معمولی استفاده شود چون تغییراتی که در کلاس های Thread-safe انجام شده منجر به پیاده سازی سطح پایین synchronized می شود. بنابراین روند اجرای برنامه کند تر می شود مثلا زمانی که StringBuilder نیاز ما را در کدنویسی برآورده می کند لازم نیست از StringBuffer استفاده کنیم.

نکته: تمامی اشیای تغییر ناپذیر همواره Thread-safe هستند، مثلا اشیای کلاس String و Integer همگی از نوع تغییر ناپذیر هستند و تغییر ویژگی های آن ها بعد از ساخته شدن شی غیر ممکن است و روشن ترین دلیل آن هم این است که setter ندارند پس توسط هیچ Thread ی قابل تغییر نیستند پس بدون Lock در برنامه نویسی همروند قابل استفاده هستند.

حالا ممکن است این سوال مطرح شود که شی کلاس StringBuffer با استفاده از متد append قابل تغییر است پس چطور Thread-safe است؟

پاسخ آن در چند خط قبل داده شده در واقع تمامی متدهای کلاس StringBuffer (به طور کلی کلاس های Thread-safe) با استفاده از synchronization پیاده سازی شده اند پس علاوه بر این که اشیای این کلاس قابل تغییر هستند، Thread-safe نیز می باشند. در ادامه مثال هایی در رابطه با کلاس های Thread-safe بررسی خواهیم کرد.

Synchronizer چیست؟

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

  • Exchancher
  • CyclicBarrier
  • Semaphore
  • CountDownLatch

هستند که در واقع مانند notify و wait عمل می کنند اما دارای کارایی و انعطاف بیشتری هستند که در ادامه با آن ها آشنا می شویم.

برای استفاده از این کلاس ها باید پکیج Java.util.concurrent را import کنید.

کلاس Exchancher

این کلاس برای هماهنگی و تبادل بین دو Thread ایجاد شده، به طوری که هر Thread با فراخوانی متد exchange یک پیغام (به طور کلی یک شی) ارسال می کند و درجا متوقف می شود تا یک Thread دیگر نیز با فراخوانی متد exchange باعث شود Thread دیگر فعال شود و عملیات تبادل انجام گردد. به عبارت دیگر شی که قرار است بین دو Thread تبادل شود در این متد قرار می گیرد به مثال زیر توجه کنید.

کارکرد کلی کلاس Exchanger
کارکرد کلی کلاس Exchanger
public class Main {
    public static void main(String[] args) {
        Exchanger<String> ex = new Exchanger<>();
        ExchangeExample T1 = new ExchangeExample(ex, "A");
        ExchangeExample T2 = new ExchangeExample(ex, "B");
        T1.setName("T1");
        T2.setName("T2");
        T1.start();
        T2.start();  
    }
}
class ExchangeExample extends Thread{
    Exchanger<String> ex=null;
    String st =null;
    public ExchangeExample(Exchanger<String> ex,String st) {
        this.ex=ex;
        this.st=st; 
    }
    @Override
    public void run() {
        try {
            String pre = st;
            st=ex.exchange(st);
            System.out.println("Thread name:"+Thread.currentThread().getName()+" "+"Exchange " +pre+ " for " +st);
        } catch (InterruptedException ex) { }
    } 
}

در متد main سازنده ی کلاس ExchangeExample دو پارامتر می گیرد که اولی از نوع Exchanger است که نوع داده های انتقال آن از نوع String تعیین شده، مقدار A به T1 و مقدار B به T2 داده شده است و در کلاس ExchangeExample در متد run مقدار رشته ای گرفته شده از سازنده در متغییر محلی pre ذخیره و مقدار جدید st با خط کد st=ex.exchange(st) دریافت و در st ذخیره می شود.

در ادامه نام Thread که این عملیات را انجام داده همراه با مقدار قبلی رشته و مقدار exchange شده چاپ می شود که خروجی آن به شکل زیر است.

خروجی
خروجی

مشاهده می کنید که مقادیر بین دو Thread تعویض شده اند در حالی که در T1 کاراکتر A ارسال شده ولی کاراکتر B نیز در آن دریافت شده و برعکس.

تذکر: استفاده از متد exchange باعث برتاب خطای InterruptedException می شود.

کلاس CyclicBarrier

این کلاس نیز یک مکانیزم هماهنگ کننده است، گاهی لازم است چند Thread به نقطه ی خاص برسند تا ادامه ی روند برنامه انجام شود، به عبارتی دیگر این کلاس در نقطه ای از برنامه مانعی ایجاد می کند و تا زمانی که همه ی Thread های مشخص شده به آن نقطه نرسند اجرای برنامه در آن نقطه wait می شود و وقتی که همه ی Thread های مشخص شده برای کلاس CyclicBarrier به آن نقطه (مانع) برسند برنامه دوباره به روند اجرا باز می گردد.

روش کلی استفاده از شی کلاس CyclicBarrier به صورت زیر است.

CyclicBarrier ObjectName = new CyclicBarrier()

این کلاس دارای دو سازنده می باشد که اولی یک پارامتر از جنس int می گیرد که این مقدار بیانگر تعداد Thread هایی است که باید، تا قبل از فراخوانی متد ObjectName .await() به آن برسد. مثلا اگر این عدد 2 اعلام شود باید هر دو Thread به آن نقطه برسند تا اجرا ادامه یابد یا به بیان ساده تر شی CyclicBarrier باید بین دو Thread هماهنگی ایجاد کند.

سازنده ی دیگری علاوه بر پارامتر int یک پارامتر از جنس اینترفیس Runnable می گیرد که در متد run آن، عملیاتی که بعد از رسیدن Thread ها به یک نقطه باید انجام شود را مشخص می کند در واقع این عملی است که توسط کلاس CyclicBarrier باید انجام شود. حال این عمل می تواند یک پردازش پیچیده باشد یا چاپ یک اعلان ساده.

برای درک بهتر مطالب گفته شده مثال زیر را اجرا کنید و خروجی را مشاهده کنید و به دقت آن را بررسی کنید.

import java.util.concurrent.*;
public class MainClass {
    public static void main(String[] args) {
        Runnable CyclicBarrierAction1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("BarrierAction 1 executed ");
            }
        };
        Runnable CyclicBarrierAction2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("BarrierAction 2 executed ");
            }
        };
        CyclicBarrier barrier1 = new CyclicBarrier(2, CyclicBarrierAction1);
        CyclicBarrier barrier2 = new CyclicBarrier(2, CyclicBarrierAction2);
        CyclicBarrierExample T1 = new CyclicBarrierExample(barrier1, barrier2);
        CyclicBarrierExample T2 = new CyclicBarrierExample(barrier1, barrier2);
        T1.start();
        T2.start();
    }
}
class CyclicBarrierExample extends Thread {
    CyclicBarrier barrier1 = null;
    CyclicBarrier barrier2 = null;
    public CyclicBarrierExample(CyclicBarrier barrier1, CyclicBarrier barrier2) {
        this.barrier1 = barrier1;
        this.barrier2 = barrier2;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " " + "waiting at barrier 1");
            barrier1.await();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " " + "waiting at barrier 2");
            barrier2.await();
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " done!");
        } catch (InterruptedException ex) {
        } catch (BrokenBarrierException ex) {
        }
    }
}
خروجی
خروجی

در کلاس CyclicBarrierExample دو عدد شی CyclicBarrier در سازنده ی کلاس مقدار دهی شده اند و در متد run ابتدا 2 ثانیه زمان انتظار در نظر گرفته شده و بعد از آن نام Thread که در حال اجرای این متد است چاپ می شود و سپس barrier1.await() در نقطه مانع مشترک صبر می کند تا Thread دیگر نیز به آن نقطه برسد و روند اجرای برنامه ادامه پیدا کند در ادامه دوباره 2 ثانیه زمان انتظار در نظر گرفته شده و نام Thread در حال اجرا چاپ می شود و این بار مانع مشترک 2 ایجاد شده، پس یک Thread دیگر باید به این نقطه برسد تا در پایان روند اجرا به پایان برسد.

در کلاس main ابتدا دو مقدار Runnable با متد run تعریف شده اند تا به عنوان پارامتر سازنده ی شی CyclicBarrier استفاده شوند.

دستورات قرار داده شده در متد run، چاپ یک عبارت ساده است تا به کاربر اعلام کند: نقطه اجرای برنامه از Barrier عبور کرده است، در ادامه دو شی CyclicBarrier ایجاد می شود و تعداد Thread های آن، دو عدد در نظر گرفته شده، یعنی باید بین دو Thread هماهنگی ایجاد کند.

برنامه را چندین بار اجرا و خروجی را با دقت بررسی کنید، حالا عدد Thread یکی از CyclicBarrier ها را تغییر دهید مثلا:

CyclicBarrier barrier1 = new CyclicBarrier(3, CyclicBarrierAction1)

خب در اینجا تعداد Thread هایی که باید با هم هماهنگ شوند، سه عدد اعلام شده در صورتی که فقط دو Thread در این برنامه در حال اجرا هستند.

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

خروجی
خروجی

مشاهده می کنید که برنامه در این نقطه گیر کرده و هرگز ادامه پیدا نمی کند چون منتظر Thread سوم است در حالی که چنین Thread  در برنامه ایجاد نشده است.

نمای کلی مثال بالا مطابق شکل زیر است.

نمای کلی مثال
نمای کلی مثال

تذکر: استفاده از متدهای کلاس CyclicBarrier باعث برتاب خطای BrokenBarrierException می شود.

در بخش بعدی آموزش نیز دو کلاس Semaphore و CountDownLatch را بررسی خواهیم کرد که دارای اهمیت فراوانی بوده و در عین حال درک آن ها ساده است.

موفق باشید.

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

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