Полезное для программистов:

Фриланс
Новости
Статьи
   
Рубрики:


Многопоточное программирование

Поиск:
Спасибо большое Domestic Cat за предоставленную информацию

Синхронизация потоков
Если на вашем компьютере один процессор, то в данныи момент времени может выполняться может только один поток - все остальные потоки приостановены (suspended). Планировшик занимается переключением потоков и распределением времени их выполнения. Отсюда 2 вывода:

- Thread.sleep(int ms) не обязателно "спит" именно ms миллисекунд. Когда поток "проснется" зависит от планировшика.
- установка приоритета потока - всего лишь пожелание; это значит что скорее всего поток с приоритетом 9 получит больше времени на исполнение чем поток с приоритетом 8. Хотя для HotSpot наверное, так и будет, а вот для браузеров - вряд ли.

2. Перейдем собственно к синхронизации. Зачем она нужна? Посмотрим на такую программу:
Код
public class Test extends Thread
{
   private static A a = new A();
   
   public static void main(String [] args)
   {
       for (int i = 0; i < Integer.parseInt(args[0], 10); i++)
       {
           Test test = new Test();
           test.start();
       }
   }
   public void run()
   {
       doSomeWork();
   }
   
   public void doSomeWork()
   {
       a.doWork();
   }
}

class A
{
   private static int a1 = 0;
   private int a2 = 0;

   public void doWork()
   {
       int a3 = 0;
       a1++; a2++; a3++;
       try
       {
           Thread.sleep((int) (Math.random() * 100));
       }
       catch (InterruptedException ie)
       {
           System.out.println("Interrupted");
       }
       a1--; a2--; a3--;
       System.out.println("Thread:  " + Thread.currentThread().getName() +
                     " :   a1 = " + a1 + ", a2 = " + a2 + ", a3 = " + a3);
   }
}


Если запустить ее: java Test <N> то она запустит N потоков, передав каждому ссылку на объект класса А; ну а потоки вызывают метод которыи меняет статическую переменную а1, переменную объектa а2 и локальную переменную а3. Если запустить один поток, то мы увидим, естественно:
Код
$> Thread:  Thread-0 :   a1 = 0, a2 = 0, a3 = 0

A вот для 100 потоков результат будет инои:
Код

...
Thread:  Thread-81 :   a1 = 2, a2 = 2, a3 = 0
Thread:  Thread-79 :   a1 = 1, a2 = 1, a3 = 0
Thread:  Thread-84 :   a1 = 0, a2 = 0, a3 = 0
Thread:  Thread-95 :   a1 = 13, a2 = 13, a3 = 0
...

Получилось так потому, что а1 и а2 не thread-safe. Дело в том, что при создании каждыи поток получает свои участок памяти (он называется "рабочая память") на стеке; и соответственно, каждыи поток получает свою копию локальних переменных. Именно поэтому а3 всегда в результате равно 0 - каждыи поток оперирует своей а3. Локальные переменные всегда thread-safe.

Проблема возникает для переменных класса и объекта. Oбъект создается на хипе (heap - куча), потоки получают копию ссылки на объект, и поетому работают с одним и тем же объектом. Поскольку мы не знаем как планировшик распределит время выполнения между потоками, то и предсказать результат невозможно: попав внутрь метода doWork() поток изменяет а1, а2, и останавливается (sleep); в это время другие потоки изменяют а1, а2. В резyльтате для потока а1 и а2 до sleep бутут иными чем после sleep.

В целом проблема в следуюшем : поскольку потоки имеют доступ к не- thread-safe переменным, и действия планировшика неопредсказуемы, получается что программист ограничен работать только с локальными переменными в многопотоковом коде; иначе результаты предсказать нелзя.

3. Чтобы снять это ограничение, и введена синхронизация. Каждыи объект в Java получает "лок" (lock - замок); и этот лок только один. Встретив слово synchronized поток пытается забрать лок у объекта; если ему ето удается, он попадает внутрь (метода или блока). Если второи (третии итп) потоки попытаются теперь выполнить любой synchronized метод или блок на данном объекте, они не смогут получить лок и будут приостановлены. После того как поток покидает synchronized блок, он возврашяет лок объекту и ждушие потоки начинают "борьбу" за лок (это называется "concurrent access").
Заметим, что на не-synchronized методы/kод это не распространяется, даже если один из потоков взял лок, не-synchronized методы могут быть вызваны любым количеством потоков одновременно.
Переделаннаыа программа быдет иметь вид:
Код

public class Test extends Thread
{
   private static A a = new A();
   
   public static void main(String [] args)
   {
       for (int i = 0; i < Integer.parseInt(args[0], 10); i++)
       {
           Test test = new Test();
           test.start();
       }
   }
   public void run()
   {
       doSomeWork();
   }
   
   public void doSomeWork()
   {
       a.doWork();
   }
}

class A
{
   private static int a1 = 0;
   private int a2 = 0;

   public synchronized void doWork()
   {
       int a3 = 0;
       a1++; a2++; a3++;
       try
       {
           Thread.sleep((int) (Math.random() * 100));
       }
       catch (InterruptedException ie)
       {
           System.out.println("Interrupted");
       }
       a1--; a2--; a3--;
       System.out.println("Thread:  " + Thread.currentThread().getName() +
                     " :   a1 = " + a1 + ", a2 = " + a2 + ", a3 = " + a3);
   }
}

Результат будет такои:
Код

...
Thread:  Thread-93 :   a1 = 0, a2 = 0, a3 = 0
Thread:  Thread-94 :   a1 = 0, a2 = 0, a3 = 0
Thread:  Thread-95 :   a1 = 0, a2 = 0, a3 = 0
Thread:  Thread-96 :   a1 = 0, a2 = 0, a3 = 0
...

Чем меньшии блок вы синхронизируете, тем лучше. Запустив этот код вы увидите что скорость по сравнению с первои программои значительно снизилась. Если раньше потоки выполняли метод одновременно, то теперь - по очереди, что и снижает скорость. Понятно что для нескольких потоков код :
Код

int methodA()
{
    ...
    synchronized (this)
    {
         doSomeStuff();
    }
    ...
}

будет всегда выгоднее чем:
Код

synchronized int methodA()
{
    ...
    doSomeStuff();
    ...
}

Синхронизируя метод, мы синхронизируем на объекте "this" (то есть поток будет забирать лок у самого объекта ); но никто не мешает синхронизировать на любом другом объекте:
Код

synchronized (MyClass.class) {...}
synchronized (object) {...}
..

4. Теперь посмотрим на такую программу:
Код

public class Test
{

   public static void main(String [] args)
   {
       A a = new A();
       Thread1 p1 = new Thread1(a);
       Thread2 p2 = new Thread2(a);
       p1.start();
       p2.start();
   }
}

class A
{
   private int result;
   public int getResult()
   {
       return result;
   }
   public void doWork()
   {
       for (int i = 0; i < 100; i++, result++);
   }
}

class Thread1 extends Thread
{
   private A a;
   Thread1 (A a)
   {
       this.a = a;
   }
   public void run()
   {
       a.doWork();
   }
}
class Thread2 extends Thread
{
   private A a;
   Thread2 (A a)
   {
       this.a = a;
   }
   public void run()
   {
       System.out.println(a.getResult());
   }
}

Задумка такая : один поток считает, второй "подбирает" результат. Все бы хорошо, но опять проблема: результат будет зависеть от порядка старта потоков: для p1.start(); p2.start(); мы получим 100, а для p2.start(); p1.start() - 0.

Дело в том, что потоки понятия не имеют друг о друге; во втором случае поток 2 (p2) получает значение переменнои раньше чем p1 успевает его посчитать. Да и то что в первом случае результат будет всегда 100 при повторных запусках никто не гарантирует - неизвестно успеет ли p1 закончить вычисления до того как p2 затребует результат.

Для коммуникатции потоков используыутся методы wait(), notify() и notifyAll().

wait заставяет поток остановиться и выпустить лок; так что другои поток (ждушии "снаружи" synchronized) сможет его затребовать и воити в блок/метод.
notifyAll сообшает всем ждущим на wait потокам, что данныи поток собирается выпустить лок (ну а notify - одному из ждуших потоков, первом попавшим на wait). Как это работает мы посмотрим на исправленном варианте кода:
Код

public class Test
{

   public static void main(String [] args)
   {
       A a = new A();
       Thread1 p1 = new Thread1(a);
       Thread2 p2 = new Thread2(a);
       p1.start();
       p2.start();
   }
}

class A
{
   private int result;
   private boolean isReady = false;
   
   public synchronized int getResult()
   {
       try
       {
           while (!isReady) wait();
       }
       catch (InterruptedException ie)
       {}
       isReady = false;
       return result;
   }
   public synchronized void doWork()
   {
       for (int i = 0; i < 100; i++, result++);
       isReady = true;
       notify();
   }
}

class Thread1 extends Thread
{
   A a;
   Thread1(A a)
   {
       this.a = a;
   }
   public void run()
   {
       a.doWork();
   }
}
class Thread2 extends Thread
{
   A a;
   Thread2(A a)
   {
       this.a = a;
   }
   public void run()
   {
       System.out.println(a.getResult());
   }
}

Возможны 2 варианта:
a. p2 пытаетса выполнить getResult, и получает лок от объекта a. Поскольку isReady равен false, p2 попадает в цикл, выполняет wait и отпускает лок. p1 попадает в doWork, считает, изменяет isReady и уведомляет (notify) p2 что все готово. p2 выходит из цикла и получает результат.
б. p1 первым попадает в doWork; он получает лок, изменяет isReady и выполняет notify которыи ничего не делает, посколку p2 попросту не может попасть внутрь getResult. p1 отдает лок, p2 его получает, благополучно пропускает цикл и получает результат.
Здесь результат уже от порядка вызова методов зависеть не будет и всегда равен 100.

Заметим, что без wait/notify это работать не будет!
Если wait убрать, поток p2 будет крутить цикл "вхолостую", отнимая ценные ресурсы; но это не главное.
Проблема возникнет если p2 первым получит лок - он навсегда останетса в цикле, в то время как p1 не сможет попасть в doWork. Такая ситуация называетса deadlock. Вот более красивыи пример:
Код

class A
{
    private B b;
    A (B b)
    {
         this.b = b;
    }
   
    synchronized void methodA()
    {
          b.methodB();
    }
}
class B
{
    private A a;
    B (A a)
    {
         this.a = a;
    }
   
    synchronized void methodB()
    {
          a.methodA();
    }
}

Пусть p1 получил лок на объекте a, а p2 - на объекте b. Для продолжения работы p1 нужно получить лок от b - но этот лок у p2; соответственно, p2 нужен лок от b, но он у p1!

5. Наконец, последнее. На самом деле поток может кэшировать переменных (хотя он и имеет ссылку на соответствующий объект на хипе). Рассмотрим этот вопрос на примере переменных а1 и а2 из первой программы.
Java хранит значения переменных а1 и а2 в так называемой "главной памяти" ("main memory"). Тем не менее поток может сохранить значение этих переменных "для себя", чтобы не обновлять их каждый раз. В результате значения переменных у различных потоков будут несогласованными. Если вы хотите, чтобы поток каждый раз "освежал" значение переменной, получая его из main памяти, используйте volatile, например:
Код

class A
{
      private static volatile int a1;
      private volatile int a2;
      .....

Проверить действие volatile можно с помошю модификации первого примера:
Код

public class Test extends Thread
{
   public static A a = new A();
   
   public static void main(String [] args)
   {
       for (int i = 0; i < Integer.parseInt(args[0], 10); i++)
       {
           Test test = new Test();
           test.start();
       }
   }
   public void run()
   {
       doSomeWork();
   }
   
   public void doSomeWork()
   {
       for (int j = 0; j < 100; j++) a.doWork();
   }
}

class A
{
   volatile int a1 = 0;
   int a2 = 0;

   public void doWork()
   {
       int a3 = 0;
       a1++; a2++; a3++;                
       try
       {
           Thread.sleep((int) (Math.random() * 100));
       }
       catch (InterruptedException ie)
       {
           System.out.println("Interrupted");
       }
       a1--; a2--; a3--;

       if (a1 != a2) System.out.println("a1 = " + a1 + "   a2  = " + a2 );
   }
}

Поскольку а1 синхронизируется с главной памятью, а а2 - нет, то при запуске java Test 1000 вы получите что-то вроде:
Код

...
a1 = 53   a2  = 391
a1 = 52   a2  = 390
a1 = 51   a2  = 389
a1 = 50   a2  = 388
a1 = 49   a2  = 387
...

Как видим, потоки закэшировали а2, в результате чего каждый поток изменяет свою копию а2, в то время как а1 синхронизирована на всех потоках.
Автор: AntonSaburov
Сайт: http://






Просмотров: 6163

 

 

Новые статьи:


Популярные:
  1. Как сделать цикличным проигрывание MIDI-файла?
  2. Создание AVI файла из рисунков
  3. Как устройство "отключить в данной конфигурации"?
  4. Kто в данный момент присоединен через Сеть?
  5. Как узнать количество доступной памяти?
  6. Как реализовать в RichEdit разноцветный текст?
  7. Как скрыть свое приложение от ProcessViewer
  8. Как программно нажать/скрыть/показ кнопку "Start"?
  9. Модуль работы с ресурсами в PE файлах
10. Функции вызова диалоговых окон выбора
11. Проверка граматики средствами Word'а из Delphi.
12. Модуль для упрощенного вызова сообщений
13. Функции для записи и чтение своих данных в, ЕХЕ- файле
14. Рекурсивный просмотр директорий
15. Network Traffic Monitor
16. Разные модули
17. Универсальная функция для обращения к любым экспортируем функциям DLL
18. Библиотека от VladS
19. Протектор для UPX'а
20. Еще об ICQ, сообщения по контакт листу?
21. Использование открытых интерфейсов
22. Теория и практика использования RTTI
23. Работа с TApplication
24. Примеры использования Drag and Drop для различных визуальных компонентов
25. Что такое порт? Правила для работы с портами
26. Симфония на клавиатуре
27. Загрузка DLL
28. Исправление автоинкремента
29. Взаимодействие с чужими окнами
30. Проверить дубляжи в столбце


 

 

 
 
На главную