C, PHP, VB, .NET

Дневникът на Филип Петров


* Синхронизация на нишки

Публикувано на 17 октомври 2009 в раздел ПИК3 Java.

Когато имаме многонишково приложение, ние много често работим и със споделени ресурси. Нека демонстрираме с един пример - имаме масив с наредени числа. Имаме две нишки - такава, която променя числата с произволни и такава, която сортира числата по големина:

public class myfirstprogram{
 public static void main(String[] args){
   ArrayClass arr = new ArrayClass();
   ChangeThread c = new ChangeThread("Change Thread", arr);
   SortThread s = new SortThread("Sort Thread", arr);
   c.T.start();
   s.T.start();
   try{
     c.T.join();
     s.T.join();
   }
   catch(java.lang.InterruptedException e){}
   arr.showArray();
 }
}

class ArrayClass{
  int[] arr;
  public ArrayClass(){
    this.arr = new int[200];
    for(int i=this.arr.length-1; i>=0; i--){
      this.arr[i] = i;
    }
  }
  public void changeArray(){
    for(int i=0; i<this.arr.length; i++){
      this.arr[i] = (int)Math.round(Math.random()*100);
    }
    System.out.println("Change finished");
  }
  public void sortArray(){
    java.util.Arrays.sort(this.arr);
    System.out.println("Sort finished");
  }
  public void showArray(){
    for(int i: this.arr){
      System.out.print(i+" ");
    }
  }
}

class SortThread implements Runnable {
  private String threadName;
  ArrayClass arr;
  Thread T;
  public SortThread(String threadName, ArrayClass arr){
    this.threadName = threadName;
    this.T = new Thread(this, this.threadName);
    this.arr = arr;
  }
  public void run(){
    arr.sortArray();
  }
}

class ChangeThread implements Runnable {
  private String threadName;
  ArrayClass arr;
  Thread T;
  public ChangeThread(String threadName, ArrayClass arr){
    this.threadName = threadName;
    this.arr = arr;
    this.T = new Thread(this, this.threadName);
  }
  public void run() {
    arr.changeArray();
  }
}

При изполнение на програмата ще забележите, че нито числата са подредени произволно, нито масива е сортиран напълно. Ще има няколко поредици от сортирани числа - нещо, което определено нито една от нишките не е пожелала.

За да поправим този проблем, когато една нишка "бърка" в данните на друга, използваме т.нар. "синхронизирани методи". Въвежда се функционалност подобна на тази при трансакциите при бази от данни - достъпваните данни се "заключват", променят се и се "отключват" за достъп от други:

public class myfirstprogram{
 public static void main(String[] args){
   ArrayClass arr = new ArrayClass();
   ChangeThread c = new ChangeThread("Change Thread", arr);
   SortThread s = new SortThread("Sort Thread", arr);
   c.T.start();
   s.T.start();
   try{
     c.T.join();
     s.T.join();
   }
   catch(java.lang.InterruptedException e){}
   arr.showArray();
 }
}

class ArrayClass{
  int[] arr;
  public ArrayClass(){
    this.arr = new int[200];
    for(int i=this.arr.length-1; i>=0; i--){
      this.arr[i] = i;
    }
  }
 public synchronized void changeArray(){
    for(int i=0; i<this.arr.length; i++){
      this.arr[i] = (int)Math.round(Math.random()*100);
    }
    System.out.println("Change finished");
  }
 public synchronized void sortArray(){
    java.util.Arrays.sort(this.arr);
    System.out.println("Sort finished");
  }
  public void showArray(){
    for(int i: this.arr){
      System.out.print(i+" ");
    }
  }
}

class SortThread implements Runnable {
  private String threadName;
  ArrayClass arr;
  Thread T;
  public SortThread(String threadName, ArrayClass arr){
    this.threadName = threadName;
    this.T = new Thread(this, this.threadName);
    this.arr = arr;
  }
  public void run(){
    arr.sortArray();
  }
}

class ChangeThread implements Runnable {
  private String threadName;
  ArrayClass arr;
  Thread T;
  public ChangeThread(String threadName, ArrayClass arr){
    this.threadName = threadName;
    this.arr = arr;
    this.T = new Thread(this, this.threadName);
  }
  public void run() {
    arr.changeArray();
  }
}

Когато една нишка извика "синхронизиран" метод от един обект, всички други нишки, които в същия момент извикат същия или друг синхронизиран метод от същия обект "заспиват" и изчакват изпълнението си в опашка. В този смисъл ако първо се стартира ChangeThread, той ще извика метод changeArray(). Ако стартираме в същия момент SortThread, той ще извика функцията sortArray(). Тъй като и двете извикани функции са синхронизирани (с ключова дума synchronized), SortThread ще "заспи" и ще изчака ChangeThread да си свърши работата. Едва след това метод sortArray() ще бъде стартиран.

Единствените методи, които не могат да бъдат синхронизирани са конструкторите. При тях естествено такава операция е напълно безсмислена - не е възможно две нишки да "създават" един и същи обект едновременно.

В предишни статии ние споменахме за два обекта, които се различаваха по това как са реализирани техните методи. Това бяха StringBuilder (притежаващ не-синхронизирани методи) и StringBuffer (чийто методи са синхронизирани). Когато пишем подобни класове е хубаво винаги да преценяваме дали те ще бъдат използвани в многонишкови приложения или не. Синхронизирането трябва да се изпълнява много внимателно - в противен случай е напълно възможно при да се появят трудни за локализиране "бъгове" в последствие при реална експлоатация на софтуера.

Понякога обаче ни се налага да не правим синхронизация на абсолютно целия метод, а само на фрагмент от него. Така на практика оптимизираме програмите, като заключваме нишките за по-кратко време. Синтаксисът е следният:

Оператори 1;
synchronized(<име на обект>){
   Оператори 2;
}
Оператори 3;

По този начин по време на изпълнението на групата "Оператори 2" обектът дефиниран в блока ще бъде заключен за другите нишки. Чак след излизане от блока те ще бъдат отключени. От предишния пример бихме могли да постигнем аналогична функционалност ако променим методите changeArray и sortArray по следния начин:

public void changeArray(){
 synchronized(this){ 
    for(int i=0; i<this.arr.length; i++){ 
      this.arr[i] = (int)Math.round(Math.random()*100); 
    }
    System.out.println("Change finished");
 }
public void sortArray(){
 synchronized(this){ 
    java.util.Arrays.sort(this.arr); 
 }
 System.out.println("Sort finished");
}

Виждаме, че вече сме оградили само и единствено блокът, където се прави промяна на данните. Извикването на System.out.println() не променя никакви данни и поради тази причина няма нужда да бъде изчакван от другите нишки.

Още повече - не е задължително блоковете да заключват всички обекти от текущия клас (в горния пример заключихме за синхронизация this). Можем да заключваме само и единствено обектът, с който работим, а именно - arr.this:

  public void changeArray(){
    synchronized(this.arr){
      for(int i=0; i<this.arr.length; i++){
        this.arr[i] = (int)Math.round(Math.random()*100);
      }
    }
    System.out.println("Change finished");
  }
  public void sortArray(){
    synchronized(this.arr){
      java.util.Arrays.sort(this.arr);
    }
    System.out.println("Sort finished");
  }

По този начин е съвсем възможно друга нишка да достъпва и променя други "член-обекти" от текущия клас.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*