Skip to content

7. رشته‌های اجرایی

7.1. مقدمه

هنگامی که یک برنامه اجرا می‌شود، در یک جریان اجرایی به نام نخ (thread) اجرا می‌گردد. کلاسی که مدل‌سازی thread را انجام می‌دهد، کلاس java.lang.Thread است که برخی از ویژگی‌ها و متدهای آن در زیر فهرست شده‌اند:

currentThread()
رشته‌ای را که در حال اجراست بازمی‌گرداند
setName()
نام نخ را تعیین می‌کند
getName()
نام نخ
isAlive()
نشان می‌دهد که آیا موضوع فعال است (true) یا خیر (false)
start()
شروع یک تاپیک
run()
متدی که پس از اجرای متد شروع قبلی، به‌طور خودکار اجرا می‌شود
sleep(n)
اجرای یک نخ را برای n میلی‌ثانیه معلق می‌کند
join()
عملیات مسدودکننده – منتظر پایان نخ می‌شود قبل از اینکه به دستور بعدی ادامه دهد

سازنده‌های پرکاربرد به شرح زیر هستند:

Thread()
یک مرجع به یک وظیفه ناهمزمان ایجاد می‌کند. این وظیفه هنوز غیرفعال است. وظیفه ایجاد شده باید دارای متد `run` باشد: در اکثر موارد، از کلاسی مشتق شده از `Thread` استفاده خواهد شد.
Thread(Runnable object)
همین موضوع صدق می‌کند، اما این شیء Runnable است که به‌عنوان پارامتر ارسال شده و متد run را پیاده‌سازی می‌کند.

بیایید نگاهی به یک برنامهٔ اولیه بیندازیم که وجود یک نخ اجرای اصلی را برجسته می‌کند، نخ‌ای که در آن تابع main یک کلاس اجرا می‌شود:

// استفاده از رشته‌ها

import java.io.*;
import java.util.*;

public class thread1{
    public static void main(String[] arg)throws Exception {
         // ابتدایی‌سازی نخ جاری
        Thread main=Thread.currentThread();
         // نمایش
        System.out.println("Thread courant : " + main.getName());
         // تغییر نام
        main.setName("myMainThread");
         //بررسی
        System.out.println("Thread courant : " + main.getName());

         //حلقه بی‌نهایت
        while(true){
        // بازیابی زمان
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
             // نمایش
            System.out.println(main.getName() + " : " +H);
             // ایست موقت
            Thread.sleep(1000);
        }//در حالی که
    }//اصلی
}//کلاس

نتایج صفحه:


Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o

مثال قبلی نکات زیر را نشان می‌دهد:

  • تابع main در یک تِرد به‌درستی اجرا می‌شود
  • می‌توانید از طریق Thread.currentThread() به ویژگی‌های این نخ دسترسی پیدا کنید
  • نقش متد sleep. در اینجا، تریدی که main را اجرا می‌کند، بین دو عملیات نمایش در فواصل زمانی منظم به مدت ۱ ثانیه به خواب می‌رود.

7.2. ایجاد نخ‌های اجرایی

ممکن است برنامه‌هایی وجود داشته باشند که در آن‌ها بخش‌هایی از کد به‌طور «هم‌زمان» در نخ‌های اجرایی مختلف اجرا شوند. وقتی می‌گوییم threadها به‌طور هم‌زمان اجرا می‌شوند، اغلب این یک اصطلاح نادرست است. اگر ماشین تنها یک پردازنده داشته باشد – که هنوز هم اغلب چنین است – threadها این پردازنده را به اشتراک می‌گذارند: هر یک به نوبت برای مدت کوتاهی (چند میلی‌ثانیه) به آن دسترسی پیدا می‌کنند. این همان چیزی است که توهم اجرای موازی را ایجاد می‌کند. میزان زمانی که به یک thread اختصاص داده می‌شود به عوامل مختلفی بستگی دارد، از جمله اولویت آن، که یک مقدار پیش‌فرض دارد اما می‌تواند از طریق برنامه‌نویسی نیز تنظیم شود. هنگامی که یک thread پردازنده را در اختیار دارد، معمولاً از آن برای تمام مدت زمان اختصاص‌یافته استفاده می‌کند. با این حال، ممکن است آن را زودتر آزاد کند:

  • با انتظار برای یک رویداد (انتظار، پیوست)
  • با ورود به حالت خواب برای مدت زمان مشخصی (sleep)
  • یک نخ T می‌تواند به روش‌های مختلفی ایجاد شود
    • با ارث‌بری از کلاس Thread و بازنویسی متد run آن.
    • با پیاده‌سازی رابط Runnable در یک کلاس و استفاده از سازندهٔ جدید Thread(Runnable). Runnable یک رابط است که تنها یک متد را تعریف می‌کند: public void run(). بنابراین آرگومان سازنده مذکور می‌تواند هر نمونه کلاس باشد که این متد run را پیاده‌سازی کرده باشد.

در مثال زیر، رشته‌ها با استفاده از یک کلاس ناشناس که از کلاس Thread ارث می‌برد، ایجاد می‌شوند:

            // ایجاد نخ i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//تعریف وظیفه [i]

متد run به سادگی در اینجا متد affiche را فراخوانی می‌کند.

  • اجرای نخ T توسط T.start() آغاز می‌شود: این متد به کلاس Thread تعلق دارد و پیش از فراخوانی خودکار متد run از کلاس Thread یا اینترفیس Runnable، تعدادی inizializasyon را انجام می‌دهد. برنامه‌ای که دستور T.start() را اجرا می‌کند، منتظر اتمام وظیفه T نمی‌ماند: این برنامه بلافاصله به دستور بعدی می‌رود. در این حالت دو وظیفه به‌طور موازی در حال اجرا هستند. این وظایف اغلب باید بتوانند با یکدیگر ارتباط برقرار کنند تا پیشرفت کار مشترک را مشخص کنند. این مسئله، مشکل همگام‌سازی نخ‌ها است.
  • پس از راه‌اندازی، thread به‌طور مستقل اجرا می‌شود. این تابع زمانی متوقف می‌شود که تابع run که در حال اجرای آن است، کار خود را به پایان برساند.
  • ما می‌توانیم با تابع T.join() منتظر پایان اجرای نخ T بمانیم. این یک دستور مسدودکننده است: برنامه‌ای که آن را اجرا می‌کند تا زمانی که وظیفه T کار خود را به پایان نرساند، مسدود می‌شود. این همچنین یک وسیله همگام‌سازی است.

بیایید برنامه زیر را بررسی کنیم:

// استفاده از رشته‌ها

import java.io.*;
import java.util.*;

public class thread2{
    public static void main(String[] arg) {
         // ابتدایی‌سازی نخ فعلی
        Thread main=Thread.currentThread();
         // اعطای نام به نخ فعلی
        main.setName("myMainThread");
         // شروع تابع اصلی
        System.out.println("début du thread " +main.getName());

         //ایجاد نخ‌های اجرایی
        Thread[] tâches=new Thread[5];
        for(int i=0;i<tâches.length;i++){
             // ایجاد نخ i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//تعریف وظیفه [i]
             // نام نخ را تنظیم می‌کند
            tâches[i].setName(""+i);
             // راه‌اندازی اجرای نخ i
            tâches[i].start();
        }//برای

         //پایان main
        System.out.println("fin du thread " +main.getName());
    }//اصلی

    public static void affiche() {
         // بازیابی زمان
    Calendar calendrier=Calendar.getInstance();
    String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // نمایش «آغاز اجرای»
        System.out.println("Début d'exécution de la méthode affiche dans le Thread " + 
        Thread.currentThread().getName()+ " : " + H);
         // خواب برای ۱ ثانیه
        try{
        Thread.sleep(1000);
    }catch (Exception ex){}
     // بازیابی زمان
    calendrier=Calendar.getInstance();    
    H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // نمایش پایان اجرا
        System.out.println("Fin d'exécution de la méthode affiche dans le Thread " 
    +Thread.currentThread().getName()+ " : " + H);
    }//نمایش می‌دهد
}//کلاس

رشته اصلی که تابع main را اجرا می‌کند، پنج رشته دیگر را ایجاد می‌کند که مسئول اجرای متد استاتیک affiche هستند. نتایج به شرح زیر است:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4

این نتایج بسیار آموزنده هستند:

  • اولاً، می‌توانیم ببینیم که شروع اجرای یک تار نخ مسدودکننده نیست. متد main اجرای پنج تار نخ را به‌طور موازی آغاز کرد و اجرای خود را قبل از آن‌ها به پایان رساند. عملیات
             // رشته i را راه‌اندازی می‌کند
            tâches[i].start();

اجرای نخ tâches[i] را آغاز می‌کند، اما پس از انجام این کار، اجرای برنامه بلافاصله با دستور بعدی ادامه می‌یابد و منتظر پایان کار نخ نمی‌ماند.

  • تمام رشته‌های ایجاد شده باید متد affiche را اجرا کنند. ترتیب اجرا غیرقابل پیش‌بینی است. اگرچه در این مثال، به نظر می‌رسد ترتیب اجرا از ترتیبی که رشته‌ها با آن راه‌اندازی شده‌اند پیروی می‌کند، اما نمی‌توان از این موضوع نتیجه‌گیری کلی کرد. در اینجا سیستم‌عامل ۶ نخ و یک پردازنده دارد. این سیستم‌عامل پردازنده را بر اساس قوانین خود به این ۶ نخ تخصیص می‌دهد.
  • نتایج، نتیجه روش sleep را نشان می‌دهند. در این مثال، رشته 0 اولین کسی است که روش affiche را اجرا می‌کند. پیام شروع اجرا نمایش داده می‌شود، و پس از آن متد sleep اجرا می‌شود که آن را برای ۱ ثانیه معلق می‌کند. سپس پردازنده را از دست می‌دهد و در اختیار یک رشته دیگر قرار می‌گیرد. مثال نشان می‌دهد که این رشته ۱ است که آن را به دست می‌آورد. رشته ۱ نیز مانند سایر رشته‌ها، همین توالی را دنبال می‌کند. هنگامی که دوره خواب یک ثانیه‌ای رشته ۰ به پایان می‌رسد، اجرای آن می‌تواند از سر گرفته شود. سیستم پردازنده را به آن اختصاص می‌دهد و می‌تواند اجرای متد affiche را تکمیل کند.

بیایید برنامه خود را با دستورات زیر اصلاح کنیم تا متد main خاتمه یابد:

         //پایان برنامه اصلی
        System.out.println("fin du thread " +main.getName());
     // خاموش شدن برنامه
    System.exit(0);

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


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45

به محض اینکه متد main دستور را اجرا می‌کند:

    System.exit(0);

تمام رشته‌های برنامه را متوقف می‌کند، نه فقط رشته main را. ممکن است متد main بخواهد قبل از خاتمه دادن به خود، منتظر تمام شدن اجرای نخ‌هایی که ایجاد کرده است بماند. این کار را می‌توان با استفاده از متد join از کلاس Thread انجام داد:

     // منتظر تمام رشته‌ها
        for(int i=0;i<tâches.length;i++){
            //در انتظار نخ i
            tâches[i].join();
    }//برای  

         //پایان main
        System.out.println("fin du thread " +main.getName());
     // برنامه متوقف شد
    System.exit(0);

این نتایج زیر را می‌دهد:

début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread

7.3. اهمیت رشته‌ها

اکنون که وجود یک نخ پیش‌فرض – نخی که متد Main را اجرا می‌کند – را برجسته کرده‌ایم و می‌دانیم چگونه نخ‌های دیگری ایجاد کنیم، بیایید مزایای نخ‌ها را برای خود در نظر بگیریم و دلیل بحث در مورد آن‌ها در اینجا را بررسی کنیم. نوعی از برنامه‌ها وجود دارد که برای استفاده از نخ‌ها بسیار مناسب است: برنامه‌های مشتری-سرور در اینترنت. در چنین کاربردی، یک سرور واقع در ماشینی S1 به درخواست‌های کلاینت‌هایی که در ماشین‌های راه دور C1، C2، …، Cn قرار دارند، پاسخ می‌دهد.

Image

هر روز از برنامه‌های اینترنتی‌ای استفاده می‌کنیم که با این مدل مطابقت دارند: سرویس‌های وب، ایمیل، مرور انجمن‌ها، انتقال فایل… در نمودار بالا، سرور S1 باید به‌طور همزمان به مشتریان Ci خدمت‌رسانی کند. اگر مثال سرور FTP (پروتکل انتقال فایل) را در نظر بگیریم که فایل‌ها را به مشتریان خود تحویل می‌دهد، می‌دانیم که انتقال یک فایل گاهی ممکن است چندین ساعت طول بکشد. البته، این غیرقابل تصور است که یک مشتری به مدت طولانی سرور را به انحصار خود درآورد. کاری که معمولاً انجام می‌شود این است که سرور به ازای تعداد مشتریان، به همان تعداد نخ اجرایی (execution threads) ایجاد کند. سپس هر نخ مسئول رسیدگی به یک مشتری خاص است. از آنجایی که پردازنده به صورت چرخه‌ای بین تمام نخ‌های فعال روی دستگاه به اشتراک گذاشته می‌شود، سرور زمان کمی را با هر مشتری صرف می‌کند و بدین ترتیب اطمینان حاصل می‌کند که خدمات به طور همزمان ارائه می‌شوند.

Image

7.4. یک ساعت گرافیکی

برنامه زیر را در نظر بگیرید که پنجره‌ای حاوی یک ساعت و دکمه‌ای برای متوقف کردن یا راه‌اندازی مجدد ساعت را نمایش می‌دهد:

برای اینکه ساعت کار کند، یک فرآیند باید مسئول به‌روزرسانی زمان در هر ثانیه باشد. در عین حال، رویدادهای رخ‌دهنده در داخل پنجره نیز باید نظارت شوند: وقتی کاربر دکمه «ایست» را کلیک می‌کند، ساعت باید متوقف شود. این شامل دو وظیفه موازی و ناهمزمان است: کاربر ممکن است در هر زمانی کلیک کند.

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

راه حل این مشکل این است که با کلیک کاربر روی دکمه «شروع»، یک وظیفه برای مدیریت ساعت راه‌اندازی شود، در حالی که به برنامه اجازه داده می‌شود به شنیدن رویدادهای رخ‌داده در پنجره ادامه دهد. در این صورت، دو وظیفه مجزا به طور موازی در حال اجرا خواهیم داشت:

  • مدیریت ساعت
  • گوش دادن به رویدادهای پنجره

بیایید به ساعت گرافیکی خود بازگردیم:

خیر.
نوع
name
نقش
1
JTextField (قابل ویرایش=خیر)
txtHorloge
زمان را نمایش می‌دهد
2
JButton
btnGoStop
ساعت را متوقف یا شروع می‌کند

کد منبع برنامه‌ای که با JBuilder ساخته شده است به شرح زیر است:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;

public class interfaceHorloge extends JFrame {
  JPanel contentPane;
  JTextField txtHorloge = new JTextField();
  JButton btnGoStop = new JButton();

  //ویژگی‌های نمونه
  boolean finHorloge=true;

   //چارچوب را بساز
  public interfaceHorloge() {
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void runHorloge(){
    // حلقه تا زمانی که دستور توقف داده شود
    while( ! finHorloge){
       // زمان را دریافت کنید
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       //آن را در فیلد T نمایش دهید
      txtHorloge.setText(H);
       // یک ثانیه صبر کنید
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        //خروج با خطا
        System.exit(1);
      }//تلاش کنید
    }// در حالی که
  }//runHorloge

   //کامپوننت را راه‌اندازی کنید
  private void jbInit() throws Exception  {
...................
  }

   //جایگزین شد، بنابراین می‌توانیم هنگام بسته‌شدن پنجره خارج شویم
  protected void processWindowEvent(WindowEvent e) {
.............
  }

  void btnGoStop_actionPerformed(ActionEvent e) {
    // شروع/توقیف تایمر
     // بازیابی برچسب دکمه
    String libellé=btnGoStop.getText();
    // شروع؟
    if(libellé.equals("Lancer")){
      // رشته‌ای را ایجاد می‌کند که ساعت در آن اجرا خواهد شد
      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }
      };//تعریف نخ
       // اجازه می‌دهد تا نخ شروع شود
      finHorloge=false;
       // تغییر برچسب دکمه
      btnGoStop.setText("Arrêter");
      // راه‌اندازی نخ
      thHorloge.start();
      //پایان
      return;
    }//اگر
    // ایست
    if(libellé.equals("Arrêter")){
      // به تِرد بگویید متوقف شود
      finHorloge=true;
       // برچسب دکمه را تغییر دهید
      btnGoStop.setText("Lancer");
       // پایان
      return;
    }//if
  }
} 

وقتی کاربر روی دکمه «اجرا» کلیک می‌کند، یک تِرد با استفاده از یک کلاس ناشناس ایجاد می‌شود:

      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }

متد run نخ، متد runHorloge برنامه را فراخوانی می‌کند. پس از انجام این کار، نخ راه‌اندازی می‌شود:

      // شروع کردن نخ
      thHorloge.start();

سپس متد runHorloge اجرا خواهد شد:

  private void runHorloge(){
    // حلقه تا زمانی که دستور توقف داده شود ادامه می‌یابد
    while( ! finHorloge){
       // زمان را دریافت کنید
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       //آن را در فیلد T نمایش دهید
      txtHorloge.setText(H);
       // یک ثانیه صبر کنید
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        //خروجی با خطا
        System.exit(1);
      }//تلاش کنید
    }// در حالی که
  }//runHorloge

اصل روش به شرح زیر است:

  1. زمان فعلی را در کادر متن txtHorloge نمایش می‌دهد
  2. به مدت ۱ ثانیه مکث می‌کند
  3. مرحله ۱ را تکرار می‌کند، پس از آنکه ابتدا متغیر بولی finHorloge را بررسی کرده باشد، که وقتی کاربر دکمه Arrêter را کلیک کند، روی true تنظیم خواهد شد.

7.5. اپلت ساعت

ما برنامه گرافیکی قبلی را با استفاده از روش معمول به یک اپلت تبدیل می‌کنیم و سند زیر را ایجاد می‌کنیم: HTML appletHorloge.htm:

<html>
  <head>
    <title>Applet Horloge</title>
  </head>
  <body>
      <h2>Une applet horloge</h2>
    <applet
      code="appletHorloge.class"
      width="150"
      height="130"
    ></applet>
    </center>
  </body>
</html>

وقتی این سند را با دوبار کلیک کردن مستقیماً در IE بارگذاری می‌کنیم، نمایش زیر را مشاهده می‌کنیم:

Image

در این مثال، تمام عناصر مورد نیاز اپلت در یک پوشه قرار دارند:

E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002  12:17                3 174 appletHorloge.class
13/06/2002  12:17                  658 appletHorloge$1.class
13/06/2002  12:17                  512 appletHorloge$2.class
13/06/2002  12:20                  245 appletHorloge.htm

اپلت ما قابل بهبود است. ما بیان کرده‌ایم که وقتی اپلت بارگذاری می‌شود، متد init اجرا می‌شود و پس از آن، در صورت وجود، متد start اجرا می‌گردد. علاوه بر این، هنگامی که کاربر صفحه را ترک می‌کند، متد stop در صورت وجود اجرا می‌شود. وقتی کاربر به صفحهٔ اپلت بازمی‌گردد، متد start دوباره فراخوانی می‌شود. زمانی که یک اپلت از نخ‌های انیمیشن بصری استفاده می‌کند، متدهای start و stop اپلت اغلب برای شروع و توقف نخ‌ها استفاده می‌شوند. در واقع، هیچ دلیلی ندارد که یک نخ انیمیشن بصری در حالی که انیمیشن پنهان است، به اجرای خود در پس‌زمینه ادامه دهد.

بنابراین، ما روش‌های زیر، start و stop، را به اپلت خود اضافه می‌کنیم:

  public void stop(){
    // صفحه پنهان است
     //پیگیری
    System.out.println("Page stop");
     // صفحه مخفی است – نخ پایان می‌یابد
    finHorloge=true;
  }

  public void start(){
     //صفحه دوباره ظاهر می‌شود
     //پیگیری
    System.out.println("Page start");
     // در صورت لزوم یک نخ ساعت جدید مجدداً راه‌اندازی می‌شود
    if(btnGoStop.getText().equals("Arrêter")){
      // برچسب را تغییر دهید
      btnGoStop.setText("Lancer");
       //و شبیه‌سازی کلیک کاربر روی آن
      btnGoStop_actionPerformed(null);
    }//اگر
  }//شروع

علاوه بر این، ما نظارت را به متد run در تِرد اضافه کرده‌ایم تا زمان شروع و توقف آن را پیگیری کنیم:

  private void runHorloge(){
    // و سپس
    System.out.println("Thread horloge lancé");
    // ما تا زمانی که به ما گفته شود متوقف شویم، حلقه می‌زنیم
    while( ! finHorloge){
       //زمان را بازیابی می‌کنیم
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       //آن را در فیلد T نمایش می‌دهد
      txtHorloge.setText(H);
       // یک ثانیه صبر کنید
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        //خروج با خطا
        return;
      }//تلاش کنید
    }// در حالی که
    //پیگیری
    System.out.println("Thread horloge terminé");
  }//runHorloge

اکنون اپلت را با AppletViewer اجرا می‌کنیم:

E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start    // اپلت راه‌اندازی شد – صفحه نمایش داده شد
Thread horloge lancé    // رشته به‌طور متناسب راه‌اندازی می‌شود
Page stop    // اپلت کوچک‌سازی شد
Thread horloge terminé    // رشته متوقف شد
Page start    // اپلت دوباره نمایش داده شد
Thread horloge lancé    // رشته مجدداً راه‌اندازی شد
Thread horloge terminé    // دکمه توقف فشرده شد
Thread horloge lancé    // دکمه شروع فشرده شد
Page stop    // اپلت کوچک شده است    
Thread horloge terminé    // رشته مطابق انتظار متوقف شد
Page start    // اپلت مجدداً نمایش داده شد
Thread horloge lancé    // رشته به‌طور متناسب مجدداً راه‌اندازی شد

با AppletViewer، رویداد start زمانی رخ می‌دهد که پنجره AppletViewer قابل مشاهده باشد، و رویداد stop زمانی رخ می‌دهد که کوچک شده باشد. نتایج بالا نشان می‌دهد که وقتی سند HTML پنهان می‌شود، اگر نخ فعال باشد، در واقع متوقف می‌شود.

7.6. همگام‌سازی وظایف

در مثال قبلی ما، دو وظیفه وجود داشت:

  • وظیفه اصلی، که توسط خود برنامه نمایش داده می‌شود
  • وظیفه‌ای که مسئول ساعت بود

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

7.6.1. یک شمارنده ناهمزمان

رابط کاربری گرافیکی زیر را در نظر بگیرید:

Image

خیر.
نوع
name
نقش
1
JTextField
txtAGénérer
تعداد رشته‌هایی را که باید تولید شوند، مشخص می‌کند
۲
JTextfield
(قابل ویرایش نیست)
txtGénéres
تعداد رشته‌های تولید شده را نشان می‌دهد
۳
JTextField
(قابل ویرایش نیست)
txtStatus
اطلاعاتی در مورد خطاهای رخ داده و خود برنامه ارائه می‌دهد
۴
JButton
btnGénérer
شروع تولید نخ

کاربرد به شرح زیر عمل می‌کند:

  • کاربر تعداد رشته‌های تولیدشده را در فیلد ۱ وارد می‌کند
  • آنها با استفاده از دکمه ۴ شروع به تولید این رشته‌ها می‌کنند
  • رشته‌ها مقدار فیلد ۲ را می‌خوانند، آن را افزایش می‌دهند و مقدار جدید را نمایش می‌دهند. در ابتدا این فیلد حاوی مقدار ۰ است.

رشته‌های تولیدشده یک منبع را به اشتراک می‌گذارند: مقدار فیلد ۲. هدف ما در اینجا نشان دادن مشکلاتی است که در چنین وضعیتی به وجود می‌آید. در اینجا مثالی از اجرای برنامه آورده شده است:

Image

می‌توانیم ببینیم که درخواست ۱٬۰۰۰ نخ شده است، اما تنها ۷ نخ شمارش شده‌اند. کد مرتبط با برنامه به شرح زیر است:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class interfaceSynchro extends JFrame {
  JPanel contentPane;
  JLabel jLabel1 = new JLabel();
  JTextField txtAGénérer = new JTextField();
  JButton btnGénérer = new JButton();
  JTextField txtStatus = new JTextField();
  JTextField txtGénérés = new JTextField();
  JLabel jLabel2 = new JLabel();

   // متغیرهای نمونه
    Thread[] tâches=null;   // رشته‌ها
    int[] compteurs=null;   //شمارنده‌ها

   //فریم را بسازید
  public interfaceSynchro() {
..........
  }

   //کامپوننت را инициалиزه کنید
  private void jbInit() throws Exception  {
......................
  }

   //جایگزین شد، تا بتوانیم هنگام بسته‌شدن پنجره خارج شویم
  protected void processWindowEvent(WindowEvent e) {
..................
  }

  void btnGénérer_actionPerformed(ActionEvent e) {
     //تولید رشته‌ها

     // تعداد رشته‌های تولیدشده را می‌خواند
    int nbThreads=0;
    try{
      // خواندن فیلدی که حاوی تعداد رشته‌ها است
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      //مثبت >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //خطا
      txtStatus.setText("Nombre invalide");
       // تلاش مجدد
      txtAGénérer.requestFocus();
      return;
    }//catch

     //در ابتدا هیچ رشته‌ای ایجاد نشده است
    txtGénérés.setText("0");  // شمارنده وظایف در 0

     //تولید و راه‌اندازی نخ‌ها
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      //در حال ایجاد نخ i
      tâches[i]=new Thread() {
          public void run() {
          incrémente();
        }
      };//thread i
       // نام آن تعریف می‌شود
      tâches[i].setName(""+i);
      // اجرای آن را آغاز می‌کند
      tâches[i].start();
    }//برای
  }//تولید

   // افزایش
  private void incrémente(){
     // بازیابی شمارهٔ نخ
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     //مقدار شمارنده وظیفه را می‌خواند
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // آن را افزایش می‌دهد
    compteurs[iThread]++;

     // منتظر ۱۰۰ میلی‌ثانیه بمانید – سپس نخ CPU را از دست می‌دهد
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }

     //نمایش شمارنده جدید
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
    //پیگیری
    System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
  }// افزایش

}//کلاس

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

  • این پنجره دو متغیر نمونه را اعلام می‌کند:
   // متغیرهای نمونه
    Thread[] tâches=null;   // رشته‌ها
    int[] compteurs=null;   // شمارنده‌ها

آرایه tâches آرایهٔ رشته‌های تولیدشده خواهد بود. آرایه compteurs با آرایه tâches مرتبط خواهد بود. هر وظیفه دارای شمارندهٔ خود خواهد بود تا مقدار فیلد txtGénérés را از رابط کاربری گرافیکی بازیابی کند.

  • هنگامی که دکمه Générer کلیک می‌شود، متد btnGénérer_actionPerformed اجرا می‌گردد.
  • این متد با بازیابی تعداد رشته‌هایی که باید ایجاد شوند، آغاز می‌شود. در صورت لزوم، اگر این عدد نامعتبر باشد، یک خطا ایجاد می‌شود. سپس رشته‌های درخواستی را ایجاد می‌کند و با دقت ارجاعات آن‌ها را در یک آرایه ثبت کرده و به هر کدام یک شماره اختصاص می‌دهد. متد run برای رشته‌های تولیدشده، متد کلاس incrémente را فراخوانی می‌کند. تمام رشته‌ها راه‌اندازی می‌شوند (start). آرایهٔ شمارنده‌ها که با رشته‌ها مرتبط است نیز ایجاد می‌شود.
  • روش incrémente:
  • مقدار فعلی فیلد txtGénérés را می‌خواند و آن را در شمارنده‌ی مربوط به رشته‌ای که در حال اجراست ذخیره می‌کند
  • برای واگذاری عمدی پردازنده، به مدت ۱۰۰ میلی‌ثانیه مکث می‌کند
  • مقدار جدید را در فیلد txtGénérés نمایش می‌دهد

اکنون توضیح می‌دهیم که چرا تعداد رشته‌ها نادرست است. فرض کنید دو رشته برای ایجاد وجود داشته باشند. آنها به ترتیبی غیرقابل پیش‌بینی اجرا می‌شوند. یکی از آنها ابتدا اجرا می‌شود و مقدار 0 را از شمارنده می‌خواند. سپس آن را روی 1 تنظیم می‌کند اما آن را در پنجره ثبت نمی‌کند: عمداً خود را برای 100 میلی‌ثانیه معلق می‌کند. سپس پردازنده را از دست می‌دهد که به یک نخ دیگر واگذار می‌شود. این نخ به همان شیوه نخ قبلی عمل می‌کند: مقدار شمارنده را از پنجره می‌خواند و 0 را که هنوز آنجا هست بازیابی می‌کند. آن شمارنده را روی 1 تنظیم می‌کند و مانند قبلی، به مدت 100 میلی‌ثانیه مکث می‌کند. سپس پردازنده دوباره به نخ اول اختصاص داده می‌شود: این نخ مقدار 1 را در شمارنده پنجره می‌نویسد و متوقف می‌شود. اکنون پردازنده به نخ دوم اختصاص می‌یابد، که آن نیز مقدار 1 را می‌نویسد. نتیجه نادرست است.

مشکل از کجا ناشی می‌شود؟ نخ دوم یک مقدار نادرست خواند زیرا نخ اول قبل از تکمیل وظیفه‌اش، یعنی به‌روزرسانی شمارنده پنجره، متوقف شده بود. این ما را به مفهوم منبع بحرانی و بخش بحرانی در یک برنامه می‌رساند:

  • منبع بحرانی منبعی است که در هر لحظه تنها یک نخ می‌تواند آن را در اختیار داشته باشد. در اینجا، منبع بحرانی، شمارنده ۲ در پنجره است.
  • بخش بحرانی یک برنامه، دنباله‌ای از دستورالعمل‌ها در جریان اجرای یک نخ است که در طی آن به یک منبع بحرانی دسترسی پیدا می‌کند. ما باید اطمینان حاصل کنیم که در طول این بخش بحرانی، تنها همان نخ به منبع دسترسی دارد.

7.6.2. شمارش همگام‌سازی‌شده توسط متد

در مثال قبلی، هر نخ متد incrémente پنجره را اجرا کرد. متد incrémente به شرح زیر اعلام شده بود:

    private void incremente()

اکنون آن را به شیوه‌ای متفاوت اعلام می‌کنیم:

   // افزایش
  private synchronized void incrémente(){

کلمه کلیدی synchronized به این معنی است که تنها یک نخ در هر لحظه می‌تواند متد incrémente را اجرا کند. به نمادهای زیر توجه کنید:

  • شیء پنجره F، که رشته‌ها را در btnGénérer_actionPerformed ایجاد می‌کند
  • دو نخ، T1 و T2، که توسط F ایجاد شده‌اند

هر دو نخ توسط F ایجاد و سپس اجرا می‌شوند. بنابراین هر دو متد F.run را اجرا خواهند کرد. فرض کنید T1 ابتدا فراخوانی می‌شود. این متد F.run و سپس F.incremente را که یک متد همگام‌سازی‌شده است، اجرا می‌کند. آن مقدار 0 را از شمارنده می‌خواند، آن را افزایش می‌دهد و سپس به مدت 100 میلی‌ثانیه متوقف می‌شود. سپس پردازنده به T2 واگذار می‌شود که به نوبه خود F.run و سپس F.incremente را اجرا می‌کند. در این نقطه، این تابع مسدود شده است زیرا نخ T1 در حال حاضر در حال اجرای F.incremente است، و کلمه کلیدی synchronized تضمین می‌کند که تنها یک نخ در هر زمان می‌تواند F.incremente را اجرا کند. سپس T2 به نوبه خود کنترل پردازنده را از دست می‌دهد، بدون اینکه توانسته باشد مقدار شمارنده را بخواند. پس از ۱۰۰ میلی‌ثانیه، T1 پردازنده را دوباره به دست می‌آورد، مقدار شمارنده را ۱ نمایش می‌دهد، از F.incremente و سپس از F.run خارج می‌شود و متوقف می‌گردد. سپس T2 کنترل پردازنده را بازپس می‌گیرد و این بار قادر است F.incremente را اجرا کند، زیرا T1 دیگر این متد را اجرا نمی‌کند. سپس T2 مقدار ۱ را از شمارنده می‌خواند، آن را افزایش می‌دهد و به مدت ۱۰۰ میلی‌ثانیه مکث می‌کند. پس از ۱۰۰ میلی‌ثانیه، پردازنده را دوباره به دست می‌گیرد، مقدار ۲ را از شمارنده نمایش می‌دهد و خود نیز خاتمه می‌یابد. این بار مقدار به‌دست‌آمده صحیح است. در اینجا یک مثال آزمایش‌شده آورده شده است:

Image

7.6.3. شمارش همگام‌شده توسط یک شیء

در مثال قبلی، دسترسی به شمارنده txtGénérés توسط یک متد همگام‌سازی شد. اگر پنجره‌ای که رشته‌ها را ایجاد می‌کند F نامیده شود، می‌توانیم همچنین بگوییم که متد F.incremente نمایانگر منبعی است که قرار است در هر لحظه تنها توسط یک رشته استفاده شود. بنابراین این یک منبع بحرانی است. دسترسی همگام به این منبع توسط کلمه کلیدی synchronized تضمین شد:

    private synchronized void incrémente()

همچنین می‌توان گفت که منبع بحرانی خود شیء F است. این سخت‌گیرانه‌تر از موردی است که در آن منبع بحرانی F.incremente باشد. در واقع، در مورد دوم، اگر یک نخ T1، F.incremente را اجرا کند، یک نخ T2 قادر به اجرای F.incremente نخواهد بود اما قادر خواهد بود روش دیگری از شیء F را اجرا کند، چه همگام باشد چه نباشد. در صورتی که خود شیء F منبع بحرانی باشد، هنگامی که یک نخ T1 یک بخش همگام‌سازی‌شده از آن شیء را اجرا می‌کند، تمام بخش‌های همگام‌سازی‌شده دیگر آن شیء برای نخ‌های دیگر غیرقابل دسترسی می‌شوند. بنابراین، اگر یک نخ T1 روش همگام‌سازی‌شده F.incremente را اجرا کند، نخ T2 قادر نخواهد بود نه تنها F.incremente بلکه هیچ بخش همگام‌سازی‌شده دیگری از F را نیز اجرا کند. حتی اگر هیچ رشته‌ای از آن استفاده نکند. بنابراین این رویکرد محدودکننده‌تر است.

بنابراین فرض کنیم که پنجره به منبع بحرانی تبدیل می‌شود. در این صورت خواهیم نوشت:

   // افزایش
  private void incrémente(){
     // بخش بحرانی
    synchronized(this){
       //بازیابی شماره نخ
      int iThread=0;
      try{
        iThread=Integer.parseInt(Thread.currentThread().getName());
      }catch(Exception ex){}
       //مقدار شمارنده وظیفه را می‌خواند
      try{
        compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
      } catch (Exception e){}
       // آن را افزایش می‌دهد
      compteurs[iThread]++;

       // منتظر ۱۰۰ میلی‌ثانیه بمانید – سپس نخ CPU را از دست می‌دهد
      try{
        Thread.sleep(100);
      } catch (Exception e){
        System.exit(0);
      }

       //نمایش شمارنده جدید
      txtGénérés.setText("");
      txtGénérés.setText(""+compteurs[iThread]);
    }//همگام‌سازی شده
  }// افزایش

تمام رشته‌ها از پنجره this برای همگام‌سازی استفاده می‌کنند. در زمان اجرا، همان نتایج صحیح قبلی به دست می‌آید. در واقع، همگام‌سازی می‌تواند روی هر شیئی که برای همه رشته‌ها شناخته شده باشد، انجام شود. در اینجا، به عنوان مثال، روش دیگری آورده شده است که همان نتایج را می‌دهد:

   // متغیرهای نمونه
    Thread[] tâches=null;   // رشته‌ها
    int[] compteurs=null;   //شمارنده‌ها
    Object synchro=new Object(); // یک شیء همگام‌سازی نخ

   // افزایش
  private void incrémente(){
     //بخش بحرانی
    synchronized(synchro){
..............
    }//همگام‌سازی‌شده
  }// افزایش

پنجره یک شیء از نوع Object ایجاد می‌کند که برای همگام‌سازی رشته‌ها استفاده خواهد شد. این روش نسبت به روشی که بر روی شیء this همگام‌سازی می‌کند ترجیح داده می‌شود زیرا محدودیت کمتری دارد. در اینجا، اگر یک نخ T1 در داخل بخش همگام‌سازی‌شده incrémente باشد ویک نخ T2 که می‌خواهد بخش همگام‌سازی‌شده دیگری از همان شیء «this» را که توسط شیئی غیر از synchro همگام‌سازی شده است، اجرا کند، قادر به انجام این کار خواهد بود.

7.6.4. همگام‌سازی مبتنی بر رویداد

این بار، از یک متغیر بولی peutPasser برای اطلاع‌رسانی به یک نخ در مورد مجاز بودن یا نبودن ورود به یک بخش بحرانی استفاده می‌کنیم. یک پیاده‌سازی غیرهمگام ممکن است به این صورت باشد:

while(! peutPasser);        //در انتظار درست شدن peutPasser
peutPasser=false;            //هیچ نخ دیگری نباید از آن عبور کند
section critique;            // در اینجا نخ به تنهایی است
peutPasser=true;            // یک نخ دیگر ممکن است وارد بخش بحرانی شود

دستور اول، جایی که یک نخ در حالی که منتظر است peutPasser به مقدار true ارزیابی شود، در حلقه می‌ماند، ناکارآمد است: این نخ پردازنده را به طور غیرضروری درگیر می‌کند. این حالت به عنوان انتظار فعال (active wait) شناخته می‌شود. کد را می‌توان به شرح زیر بهبود بخشید:

while(! peutPasser){        //ما منتظر می‌مانیم تا peutPasser درست شود
   Thread.sleep(100);    // برای ۱۰۰ میلی‌ثانیه مکث کنید
}
peutPasser=false;            // اجازه اجرای هیچ نخ دیگری داده نمی‌شود
section critique;            // در اینجا نخ به‌تنهایی قرار دارد
peutPasser=true;            // یک نخ دیگر ممکن است وارد بخش بحرانی شود

حلقهٔ انتظار در اینجا بهتر است: اگر نخ نتواند ادامه دهد، قبل از بررسی مجدد اینکه آیا می‌تواند ادامه دهد یا خیر، به مدت ۱۰۰ میلی‌ثانیه به خواب می‌رود. در این مدت، پردازنده به سایر نخ‌های سیستم اختصاص داده می‌شود.

در واقع هر دو روش نادرست هستند: آن‌ها مانع از ورود همزمان دو نخ به بخش بحرانی نمی‌شوند. فرض کنید نخ T1 تشخیص دهد که peutPasser درست است. سپس به دستور بعدی می‌رود، جایی که peutPasser را روی false تنظیم می‌کند تا سایر نخ‌ها را مسدود کند. با این حال، ممکن است در این نقطه متوقف شود، یا به این دلیل که سهم زمانی پردازنده آن تمام شده است، یا به این دلیل که یک وظیفه با اولویت بالاتر پردازنده را درخواست کرده است، یا به دلیلی دیگر. نتیجه این است که پردازنده را از دست می‌دهد. کمی بعد آن را دوباره به دست خواهد آورد. در این فاصله، وظایف دیگری پردازنده را به دست می‌آورند، از جمله شاید یک نخ T2 که در حین انتظار برای درست شدن peutPasser در یک حلقه می‌ماند. آن نیز متوجه خواهد شد که peutPasser روی true تنظیم شده است (رشته اول فرصت نکرده بود آن را روی false تنظیم کند) و وارد بخش بحرانی خواهد شد. این آن چیزی نبود که مد نظر بود.

ترتیب


while(! peutPasser){            //در انتظار درست شدن peutPasser
   try{
        Thread.sleep(100);    // برای ۱۰۰ میلی‌ثانیه مکث کنید
    } catch (Exception e) {}
}// در حالی که
peutPasser=false;                // هیچ نخ دیگری نباید وارد شود

یک توالی بحرانی است که باید با همگام‌سازی محافظت شود. با ادامه مثال قبلی، می‌توان نوشت:


    synchronized(synchro){
        while(! peutPasser){            //در انتظار درست شدن peutPasser
            try{
                Thread.sleep(100);    //مکث برای ۱۰۰ میلی‌ثانیه
            } catch (Exception e) {}
        }//در حالی که
        peutPasser=false;                // هیچ نخ دیگری نباید ادامه دهد
    }// همگام‌سازی شده
    section critique;                    // در اینجا نخ به تنهایی است
    peutPasser=true;                    // یک نخ دیگر ممکن است وارد بخش بحرانی شود

این مثال به درستی کار می‌کند. می‌توان آن را با اجتناب از حالت انتظار نیمه‌فعالِ تِرد (thread) بهبود بخشید، در حالی که به طور منظم مقدار متغیر بولی peutPasser را زیر نظر دارد. به جای بیدار شدن منظم هر ۱۰۰ میلی‌ثانیه برای بررسی وضعیت peutPasser، می‌تواند به خواب برود و درخواست بیدار شدن را بدهد وقتی peutPasser درست باشد. این به صورت زیر نوشته شده است:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            //اگر نتواند ادامه دهد، منتظر می‌ماند
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            //هیچ رشتهٔ دیگری نباید عبور کند
}//هماهنگ

عملیات synchro.wait() تنها می‌تواند توسط رشته‌ای که مالک فعلی شیء synchro است، انجام شود. در این صورت، توالی به این صورت است:


synchronized(synchro){

}// همگام

که تضمین می‌کند که نخ مالک شیء synchro است. از طریق عملیات synchro.wait()، نخ مالکیت قفل همگام‌سازی را واگذار می‌کند. چرا چنین است؟ به طور کلی به این دلیل که منابع لازم برای ادامه کار را ندارد. بنابراین، به جای مسدود کردن سایر نخ‌هایی که منتظر منبع synchro هستند، آن را آزاد می‌کند و برای منبعی که به آن نیاز دارد وارد حالت انتظار می‌شود. در مثال ما، منتظر درست شدن مقدار بولین peutPasser است. چگونه از این رویداد مطلع خواهد شد؟ به شرح زیر:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // اگر نتوانیم ادامه دهیم، منتظر می‌مانیم
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // هیچ نخ دیگری نباید پیش رود
}//هماهنگ
section critique...
synchronized(synchro){
      synchro.notify();
    }

بیایید اولین رشته‌ای را که قفل همگام‌سازی را به‌دست می‌آورد در نظر بگیریم. بیایید آن را T1 بنامیم. فرض کنید که این نخ اول می‌بیند مقدار بولی peutPasser درست است، زیرا نخ اول است. بنابراین آن را به نادرست تنظیم می‌کند. سپس از بخش بحرانی که توسط شیء synchro قفل شده است، خارج می‌شود. سپس یک نخ دیگر می‌تواند وارد بخش بحرانی شود تا مقدار peutPasser را بررسی کند. این نخ مقدار آن را false می‌یابد و سپس منتظر یک رویداد (wait) می‌ماند. با این کار، مالکیت شیء synchro را واگذار می‌کند. سپس یک نخ دیگر می‌تواند وارد بخش بحرانی شود: آن نیز منتظر خواهد ماند زیرا peutPasser نامعتبر است. بنابراین ممکن است چندین نخ برای یک رویداد روی شیء synchro منتظر بمانند.

بیایید به نخ T1 بازگردیم که اکنون دسترسی به آن اعطا شده است. این نخ بخش بحرانی را اجرا می‌کند و سپس سیگنال می‌دهد که نخ دیگری می‌تواند اکنون ادامه دهد. این کار را با استفاده از توالی زیر انجام می‌دهد:


synchronized(synchro){
      synchro.notify();
    }

ابتدا باید با استفاده از دستور synchronized مالکیت شیء synchro را بازپس گیرد. این نباید مشکلی ایجاد کند، زیرا با رشته‌هایی رقابت می‌کند که اگر برای لحظه‌ای شیء synchro را به دست آورند، باید آن را از طریق wait واگذار کنند، زیرا مقدار peutPasser را false می‌یابند. بنابراین نخ ما T1 در نهایت مالکیت شیء synchro را به دست خواهد آورد. پس از انجام این کار، از طریق عملیات synchro.notify سیگنال می‌دهد که یکی از رشته‌هایی که توسط synchro.wait مسدود شده است، باید بیدار شود. سپس دوباره مالکیت شیء synchro را واگذار می‌کند که این مالکیت سپس به یکی از رشته‌های منتظر اعطا می‌شود. آن نخ با دستور بعدی wait که آن را در حالت تعلیق قرار داده بود، به اجرای خود ادامه می‌دهد. به نوبه خود، این نخ بخش بحرانی را اجرا کرده و یک synchro.notify صادر می‌کند تا نخ دیگری را آزاد کند. و به همین ترتیب.

بیایید با استفاده از مثال شمارشی که قبلاً مطالعه کرده‌ایم، ببینیم این چگونه کار می‌کند.

  void btnGénérer_actionPerformed(ActionEvent e) {
     //تولید نخ

     // تعداد رشته‌های قابل ایجاد را می‌خواند
    int nbThreads=0;
    try{
      //خواندن فیلدی که حاوی تعداد رشته‌ها است
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      //مثبت >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //خطا
      txtStatus.setText("Nombre invalide");
       // دوباره تلاش کنید
      txtAGénérer.requestFocus();
      return;
    }//catch

     // RAZ شمارنده وظیفه
    txtGénérés.setText("0");  // شمارنده وظیفه در 0
    // اولین نخ می‌تواند ادامه دهد
    peutPasser=true;
     // رشته‌ها تولید و راه‌اندازی می‌شوند
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // رشته i ایجاد شد
      tâches[i]=new Thread() {
          public void run() {
          synchronise();
        }
      };//thread i
       //نام آن را تنظیم می‌کند
      tâches[i].setName(""+i);
      // اجرای آن را آغاز می‌کند
      tâches[i].start();
    }//برای
  }//تولید

اکنون، رشته‌ها دیگر متد incrémente را اجرا نمی‌کنند، بلکه متد زیر synchronise را اجرا می‌کنند:

   // مرحله همگام‌سازی نخ
  public void synchronise(){
     //درخواست دسترسی به بخش بحرانی
    synchronized(synchro){
      try{
        //می‌توانیم ادامه دهیم؟
        if(! peutPasser){
          System.out.println(Thread.currentThread().getName()+ " en attente");
          synchro.wait();
        }
         //ما عبور کرده‌ایم – سایر نخ‌ها از عبور جلوگیری می‌شوند
        peutPasser=false;
      } catch(Exception e){
        txtStatus.setText(""+e);
                    return;
      }//تلاش
    }// همگام

    //بخش بحرانی
    System.out.println(Thread.currentThread().getName()+ " passé");
    incrémente();

     //کار ما تمام شد – هر رشته‌ای را که ممکن است در ورودی بخش بحرانی مسدود شده باشد آزاد می‌کنیم
    peutPasser=true;
    System.out.println(Thread.currentThread().getName()+ " terminé");
    synchronized(synchro){
      synchro.notify();
    }//هماهنگ شده
  } //هماهنگ می‌کند

هدف متد synchronise پردازش رشته‌ها به‌صورت یکی‌یکی است. برای این کار از متغیر همگام‌سازی synchro استفاده می‌کند. متد incrémente دیگر توسط کلمه کلیدی synchronized محافظت نمی‌شود:

   // افزایش
  private void incrémente(){
     // بازیابی شمارهٔ نخ
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // مقدار شمارنده وظیفه را می‌خواند
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // آن را افزایش می‌دهد
    compteurs[iThread]++;
     // منتظر ۱۰۰ میلی‌ثانیه بمانید – سپس نخ CPU را از دست می‌دهد
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }
     //نمایش شمارنده جدید
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
  }//افزایش

برای ۵ رشته، نتایج به‌دست‌آمده به شرح زیر است:

0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé

فرض کنید T0 تا T4 پنج رشته‌ای هستند که توسط برنامه تولید شده‌اند. T0 اولین کسی است که قفل synchro را به‌دست می‌آورد و می‌بیند که peutPasser درست است. آن مقدار peutPasser را به false تنظیم می‌کند و ادامه می‌دهد: این هدف پیام اول، passé است. به احتمال زیاد، آن ادامه می‌دهد و بخش بحرانی را اجرا می‌کند، به‌ویژه متد incrémente. در این متد، به مدت ۱۰۰ میلی‌ثانیه به خواب می‌رود (sleep). بنابراین پردازنده را آزاد می‌کند. سپس پردازنده به یک نخ دیگر، یعنی نخ T1، اختصاص داده می‌شود که در ادامه مالکیت شیء synchro را به دست می‌آورد. متوجه می‌شود که نمی‌تواند ادامه دهد و وارد حالت انتظار (wait) می‌شود. سپس مالکیت شیء synchro و همچنین پردازنده را واگذار می‌کند. سپس پردازنده به نخ T2 اختصاص داده می‌شود که سرنوشت مشابهی را تجربه می‌کند. در طول ۱۰۰ میلی‌ثانیه‌ای که T0 متوقف است، نخ‌های T1 تا T4 نیز در حالت تعلیق قرار می‌گیرند. این معنای چهار پیام «در حالت تعلیق» است. پس از ۱۰۰ میلی‌ثانیه، T0 پردازنده را بازپس می‌گیرد و کار خود را به پایان می‌رساند: این معنای پیام «0 terminé» است. سپس یکی از رشته‌های مسدود شده را آزاد می‌کند و متوقف می‌شود. پردازنده آزادشده سپس به یک رشته در دسترس تخصیص داده می‌شود: همان رشته‌ای که به تازگی آزاد شده است. در این مورد، این T1 است. سپس رشته T1 وارد بخش بحرانی می‌شود: این همان چیزی است که پیام «1 passé» به آن اشاره دارد. آن کاری را که باید انجام دهد، انجام می‌دهد و سپس به نوبه خود به مدت ۱۰۰ میلی‌ثانیه متوقف می‌شود. در این صورت، پردازنده برای یک نخ دیگر در دسترس قرار می‌گیرد، اما همه نخ‌ها در انتظار یک رویداد هستند: هیچ‌کدام از آن‌ها نمی‌توانند پردازنده را تصاحب کنند. پس از ۱۰۰ میلی‌ثانیه، نخ T1 پردازنده را دوباره به دست آورده و خاتمه می‌یابد: این معنای پیام «1 terminé» است. رشته‌های T1 تا T4 به همان شیوه‌ای که T1 رفتار می‌کند، عمل خواهند کرد: این معنای سه مجموعه پیام «passed» و «completed» است.