کلاس های Atomic و بررسی واسط Lock و ReadWriteLock

05 فروردین 1398
java-multithreading-atomic

سلام در بخش قبلی آموزش با پکیج Java.util.concurrent و کلاس های آن آشنا شدیم، در این بخش از آموزش با مفهوم Atomic و کلاس آن آشنا می شویم.

عملیات Atomic چیست؟

به طور کلی در برنامه نویسی گاهی لازم است یک دستور به به چند دستور سطح پایین ترجمه شود، مثلا ++p را در نظر بگیرید که به p=p+1 ترجمه می شود که این خود شامل سه دستور سطح پایین خواندن p، عملیات جمع و تغییر مقدار p می باشد پس در میان اجرای این عملیات، هیچ Thread دیگری نباید از این متغیر استفاده کند.

با توجه به توضیحات بالا عملیات atomic به معنی اجرای کل عملیات به صورت یکجا است به طوری که در میان اجرای آن هیچ Thread دیگری وارد نشود.

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

توحه داشته باشید که برای انجام عملیات هایی مانند خواندن و تغییر یک متغیر (عدد، آرایه و ...) به عملیات atomic نیاز داریم که جاوا کلاس های مفیدی برای این منظور معرفی کرده است که در ادامه به توضیح آن می پردازیم.

کلاس های اتمیک (Atomic Class)

کلاس های atomic جاوا در پکیج java.util.concurrent.atomic قرار دارند و استفاده از این کلاس ها باعث می شود، برخی از عملیات ها بر روی شی این کلاس ها به صورت atomic انجام شود.

مزیت استفاده از کلاس های atomic این است که از پیاده سازی قفل، synchronized و روش های دیگر بسیار بهینه تر بوده و ممکن است کل یک عملیات پیچیده توسط سخت افزار به صورت یکجا انجام شود؛ کلیه ی متغیرهای کلاس های atomic به صورت Thread-safe و look-free (بدون نیاز به پیاده سازی قفل و synchronized) هستند.

کلاس های atomic در جاوا عبارت اند از:

AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater <T>
AtomicLong
AtomicLongArray
AtomicLongFieldUpdater <T>
AtomicMarkableReference <V>
AtomicReference <V>
AtomicReferenceArray <E>
AtomicReferenceFieldUddater <T، V>
AtomicStampedReference <V>

تذکر: مبحث کلاس های Atomic تا حدودی مشکل و پیچیده است و دارای کلاس ها و متدهای زیادی بوده، لذا بررسی همه ی آن ها در یک جلسه ممکن نیست و نیاز به آموزشی جداگانه دارد تمامی مطالب بیان شده در بالا صرفا برای آشنایی با این کلاس ها بوده اگر علاقه مند به یادگیری کامل این مبحث هستید، می توانید به این لینک که متعلق به سایت رسمی جاوا است، مراجعه کنید که هر کلاس و تمامی متدهای آن مورد بررسی قرار گرفته است.

واسط Lock

پکیج java.util.concurrent.locks که از جاوای 5 به بعد به زبان جاوا اضافه شد، شامل واسط ها و کلاس های جدیدی برای مدیریت قفل در برنامه های همروند است.

در آموزش های قبلی با استفاده از Synchronized، دسترسی همزمان چند Thread را به یک شی کنترل کردیم که آن هم توسط یک قفل فرضی انجام می شد، اما در اینجا با استفاده از واسط Lock این قفل را به صورت صریح پیاده سای خواهیم کرد که در مقایسه با Synchronized پیچیده تر و انعطاف پذیرتر است، اما هدف از پیاده سازی هر دو کنترل Thread ها در اجرای بخش بحرانی است.

واسط Lock دارای متدهای متعددی است که مهم ترین آن ها lock  unlock و tryLock هستند.

با استفاده از متد lock، قفل شی مورد نظر توسط Thread گرفته می شود و توسط متد unlock قفل آن آزاد می شود؛ استفاده از این دو متد همراه با هم دقیقا مانند پیاده سازی بلوک Synchronized است اما کارایی بهتری دارد.

نکته ای که باید به آن توجه داشت این است که متد lock و tryLock تا حدودی شبیه به هم هستند با این تفاوت که متد lock در صورت آزاد نبودن قفل متوقف می شود (Block) در حالی که متد tryLock  همان طور که از نامش مشخص است تا آزاد شدن دوباره ی قفل صبر می کند و به اصطلاح non-blocking است و در صورت عدم موفقیت در گرفتن قفل در زمانی مشخص مقدار false را بر می گرداند.

توجه: از آنجایی که Lock به صورت یه واسط پیاده سازی شده و در جاوا امکان ساختن شی از واسط به صورت مستقیم امکان ندارد باید با استفاده از قوانین polymorphism اقدام به ساخت شی کرد. یکی از کلاس هایی که واسط Lock را پیاده سازی کرده کلاس ReentrantLock می باشد که در پکیج java.util.concurrent.locks.ReentrantLock قرار دارد.

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

public class LockClass  {
    public static void main(String[] args) {
      PrintDemo PD = new PrintDemo();
      ThreadDemo t1 = new ThreadDemo("Thread - 1 ", PD);
      ThreadDemo t2 = new ThreadDemo("Thread - 2 ", PD);
      ThreadDemo t3 = new ThreadDemo("Thread - 3 ", PD);
      ThreadDemo t4 = new ThreadDemo("Thread - 4 ", PD);
      t1.start();
      t2.start();
      t3.start();
      t4.start();
    }
}
class ThreadDemo extends Thread {
   PrintDemo  printDemo;

   ThreadDemo(String name,  PrintDemo printDemo) {
      super(name);
      this.printDemo = printDemo;
   } 
   @Override
   public void run() {
      System.out.printf("%s starts printing a document\n", Thread.currentThread().getName());
      printDemo.print();
   }
}
class PrintDemo {
   private final Lock queueLock = new ReentrantLock();

   public void print() {
      queueLock.lock();
      try {
         Long duration = (long) (Math.random() * 10000);
         System.out.println(Thread.currentThread().getName()+ "  Time Taken " + (duration / 1000) + " seconds.");
         Thread.sleep(duration);
      } catch (InterruptedException e) {
         e.printStackTrace();
      } finally {
         System.out.printf("%s printed the document successfully.\n", Thread.currentThread().getName());
         queueLock.unlock();
      }
   }
}

در این مثال برای هر Thread، زمان مشخصی به صورت Random تولید می شود که قفل را گرفته و بعد از اتمام زمان تعیین شده، قفل آزاد می شود تا به Thread بعدی اجازه ورود داده شود؛ توجه کنید که دستورات بعد از lock در بلوک try-catch قرار گرفته اند و متد unlock نیز در بخش finally پیاده سازی شده است چون ممکن است دستورات داخل try اجرا نشوند و با خطا داشته باشند ولی در هر صورت باید قفل گرفته شده آزاد شود تا برنامه برای استفاده ی دوباره از آن بخش با مانع روبرو نشود؛ کد را در سیستم خود اجرا کنید و خروجی را با دقت بررسی کنید.

خروجی
خروجی

واسط ReadWriteLock

در بعضی از برنامه ها ممکن است تعداد زیادی Thread همزمان به یک منبع مشترک دسترسی داشته باشند که برای جلوگیری از تداخل با استفاده از روش های بررسی شده (Synchronized ، Semaphore و ...) آن ها را کنترل می کنیم. اما حالتی را در نظر بگیرید که مثلا سه Thread قصد دارند فقط منبع مشترک را بخوانند (بدون تغییر منبع) و دو Thread نیز آن را تغییر دهند.

در اینجا دو موضوع (مشکل) مهم پیش می آید:

  1. اگر شی مشترک مثلا با Synchronized  پیاده سازی شود سه Thread که قصد خواندن آن شی را دارند باید تا پایان کار دیگری منتظر بمانند که زیاد جالب و کارا نیست.
  2. اگر بلوک Synchronized  پیاده سازی نشود دو Thread که قصد تغییر منبع را دارند ممکن است با هم تداخل پیدا کنند که این مورد نیز جالب و مورد انتظار نیست.

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

عملیات قفل گذاری توسط دو متد readLock برای خواندن و WriteLock برای نوشتن انجام می شود.

مانند واسط Lock، این واسط نیز برای پیاده سازی نیاز به استفاده از polymorphism دارد و یکی از کلاس هایی که واسط ReadWriteLock را پیاده سازی کرده ReentrantReadWriteLock است.

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

public class ReentrantReadWriteLockExampleMain {
 
 private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
 
 private static String number = "0";
 
 public static void main(String[] args) throws InterruptedException{
  Thread t1 = new Thread(new Reader(),"Thread 1");
  Thread t2 = new Thread(new Reader(),"Thread 2");
  Thread t3 = new Thread(new WriterOdd(),"Write odd Thread");
  Thread t4 = new Thread(new WriterEven(),"Write even Thread");
  t1.start();
  t2.start();
  t3.start();
  t4.start();
  t1.join();
  t2.join();
  t3.join();
  t4.join();
 }
 static class Reader implements Runnable {
 
  public void run() {
   for(int i = 0; i< 10; i ++) {
    lock.readLock().lock();
    System.out.println(Thread.currentThread().getName() + " ---> Number is " + number );
    lock.readLock().unlock();
    }
   }
  }
 static class WriterOdd implements Runnable {
 
  public void run() {
   for(int i = 1; i<= 7; i+=2) {
    try {
     lock.writeLock().lock();
     System.out.println(Thread.currentThread().getName() +"\t"+"Writer odd is writing");
     number = number.concat(" "+i);
    } finally {
     lock.writeLock().unlock();
    }
   }
   }
  }
 static class WriterEven implements Runnable {
 
  public void run() {
   for(int i = 2; i<= 8; i +=2) {
    try {
     lock.writeLock().lock();
     System.out.println(Thread.currentThread().getName() +"\t"+"Writer Even is writing");
     number = number.concat(" "+i);
    } finally {
     lock.writeLock().unlock();
    }
   }
   }
  }
}

در این مثال دو Thread وظیفه ی خواندن و چاپ مقدار number و دو Thread وظیفه ی تولید اعداد فرد و زوج را بر عهده دارند. توجه کنید که سه کلاس Reader و WriterEven و WriterOdd کلاس های داخلی هستند که برای هر Thread، نامی مناسب در نظر گرفته شده است.

به متد run کلاس Reader دقت کنید که lock.readLock().lock پیاده سازی شده ،یعنی نوع قفل آن از نوع reader تعیین شده و سپس با فراخوانی متد lock قفل آن گرفته و در آخر نیز به همان صورت آزاد شده است (در کلاس های WriterEvenو WriterOdd که از نوع نویسنده هستند، گرفتن قفل توسط lock.writeLock().lock() انجام می شود).

در کلاس Reader، به تعداد دفعات چاپ شدن عبارت داخل متد run دقت کنید، با توجه به این که دو Thread به طور همزمان به آن دسترسی دارند و از آنجایی که در متد run فقط عملیات خواندن  شی انجام می شود، هر دو Thread اجازه ی دسترسی همزمان با این بخش را دارند اما در دو کلاس WriterEven و WriterOdd که متد run آن ها توسط lock.writeLock() پیاده سازی شده، در هر لحظه فقط یک Thread اجازه ی کار با شی مشترک را دارد که شی مشترک میان چهار Thread شی number است.

خروجی
خروجی

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

موفق باشید.

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

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