воскресенье, 19 февраля 2012 г.

Многопоточность в Java. Часть 1


Я думаю, что ни для кого не секрет, что программы бывают, как однопоточные, там и многопоточные. В чем же их отличия и для чего используется многопоточность при разработке программы на java. Давайте попробуем разобраться.


Для начала разберем небольшой пример однопоточной программы, посмотрим, в чем ее недостаток, а затем выясним, как этот недостаток можно исправить с помощью многопоточности.
И так, рассмотрим простенький пример, в котором программа будет отрабатывать цикл и выводить сообщение:

package my.onethread;

class OneThread {
      
       public OneThread() {
            
             System.out.println("Запускаем счетчик.");
            
             Counter();
            
             System.out.println("Пока выполняется цикл счетчика
Выведем это сообщение.");
             System.out.println("Ну и наверно посчитаем значение Pi в квадрате: " +
Math.PI * Math.PI);
}

       private void Counter() {

             long num = 0;
            
             while(num < 999999999) {
                    num++;
             }
            
             System.out.println("Результат работы счетчика: " + num);
            
       }

}

public class Main {

       public static void main(String args[]) {
             OneThread ot = new OneThread();
       }
}/*результат:
Запускаем счетчик.
Результат работы счетчика: 999999999
Пока выполняется цикл счетчика - выведем это сообщение.
Ну и наверно посчитаем значение Pi в квадрате: 9.869604401089358
*/

Как можно увидеть из результата выполнения программы, мы получили не то, что хотели.  То есть при использовании одного потока программа выполняется последовательно, а это не всегда подходит.
Теперь давайте перепишем нашу программу, используя многопоточность, так, чтобы выполнение цикла не препятствовало дальнейшей работе программы.

package my.onethread;

class NewThread implements Runnable {
      
       public NewThread() {
            
       }
      
       public void run() {

             long num = 0;
            
             while(num < 999999999) {
                    num++;
             }
            
             System.out.println("Результат работы счетчика: " + num);
            
       }
}

class OneThread {
      
       public OneThread() {
            
             System.out.println("Запускаемсчетчик.");
            
             Runnable r = new NewThread();
             Thread t = new Thread(r);
             t.start();
            
             System.out.println("Пока выполняется циклсчетчика
Выведем это сообщение.");
             System.out.println("Ну и наверно посчитаем значение Pi в квадрате: " +
Math.PI * Math.PI);
       }
}

public class Main {

       public static void main(String args[]) {
             OneThread ot = new OneThread();
       }
}/*результат:
Запускаем счетчик.
Пока выполняется цикл счетчика - выведем это сообщение.
Ну и наверно посчитаем значение Pi в квадрате: 9.869604401089358
Результат работы счетчика: 999999999
*/

Как вы можете видеть, использование второго потока решило нашу проблему.
Дело в том, что при создании потока, его выполнение осуществляется «параллельно» основному потоку программы, тем самым не препятствуя дальнейшему выполнению кода основного потока.
Однако, многопоточное выполнение программы понятие абстрактное. На однопроцессорном компьютере, на самом деле одновременно может выполняться только один поток, а эффект многопоточности создается за счет того, что потоки уступают «место» друг другу. На компьютерах с двуядерным процессором одновременно могут выполняться два потока, не мешая друг другу.

Создание потока
Давайте рассмотрим способы создания поток. Существует двумя способами:
  • ·         реализацией интерфейса Runnable;
  • ·         наследованием класса Thread.


Реализация интерфейса Runnable
Самый простой способ создания потока заключается в определении класса, который реализует интерфейс Runnable. Runnable определяет всего один абстрактный метод – run().
В теле метода  run() реализуется работа потока (вызываются классы, методы, определяются переменные). При выходе из метода run() поток завершает свое действие.
Класс, реализующий интерфейс Runnable выглядит так:

class MyClass implements Runnable {
                                public void run() {
// тело метода run
}
}

Для создания потока создаем экземпляр класса реализующего интерфейс Runnable.

Runnabler  = new MyClass();

На основе объекта Runnable создаем объект Thread.

Thread t = new Thread(r);

Для того чтобы запустить поток вызываем метод start() класса Thread, для данного потока. Данный метод запускает метод run().
Пример реализации интерфейса Runnable:

package my.onethread;

class NewThread implements Runnable {
      
       public NewThread() {
            
       }
      
       public void run() {

             System.out.println("Тело метода run(), созданного потока.");
            
       }
}

public class Main {

       public static void main(String args[]) {
             System.out.println("Основной поток.");
             Runnable r = new NewThread();
             Thread t = new Thread(r);
             t.start();
       }
}/* результат:
Основной поток.
Тело метода run(), созданного потока.
*/

Наследование класса Thread
Для создания потока необходимо расширить класс Thread и переопределить метод run(). Как и в случае с реализацией интерфейса в теле метода run() реализуется работа потока, и при выходе из метода поток прекращает свою работу.
Класс, расширяющий Thread,выглядит так:

class MyClass extends Thread {
public void run() {
                               // тело метода run()
}
}

Приведем пример расширения класса Thread.

package my.thread;

class AppThread extends Thread {
      
       public AppThread() {
            
       }
      
       public void run() {
            
             System.out.println("Дочерний поток.");
             for(int i = 1; i <= 5; i++) {
                    System.out.println("Значение цикла дочернего потока - " + i);
             }
             System.out.println("Работа дочернего потока завершена.");
       }
}
public class App {
      
       public static void main(String[] args) {
             System.out.println("Родительский поток.");
             Thread t = new Thread(new AppThread());
             t.start();
       }

}/*результат:
Родительский поток.
Дочерний поток.
Значение цикла дочернего потока - 1
Значение цикла дочернего потока - 2
Значение цикла дочернего потока - 3
Значение цикла дочернего потока - 4
Значение цикла дочернего потока - 5
Работа дочернего потока завершена.
 */

Какую реализацию выбрать
Вы можете спросить, зачем нужно два вида реализации, и какую из них, когда использовать. Ответ просто.
 Реализация интерфейса Runnable используется в случаях, когда класс уже наследует какой-либо родительский класс, и тем самым не позволяет расширить класс Thread. Да и вообще реализация интерфейсов считается хорошим тоном программирования в java. Это связано с тем, что в java может наследоваться только один родительский класс, таким образом, унаследовав класс Thread,вы не сможете наследовать, какой-либо другой класс.
 Расширение класса Thread целесообразно используется, когда необходимо переопределить другие методы класса Thread, помимо метода run(). Но это используется довольно редко.

Класс Thread
В классе Thread определены семь конструкторов, большое количество методов, предназначенных для работы с потоками и три константы (приоритеты выполнения потока).
Давайте рассмотрим все это богатство.

Конструкторы класса Thread
Класс Thread имеет семь перегруженных конструкторов:
Thread();
Thread(Runnable target);
Thread(Runnable target, String name);
Thread(String name);
Thread(ThreadGroup group, Runnable target);
Thread(ThreadGroup group, Runnable target, String name);
Thread(ThreadGroup group, String name);
где:
·         target – экземпляр класса реализующего интерфейс Runnable;
·         name – имя создаваемого потока;
·         group – группа к которой относится данный поток.
Например, создадим поток, который будет входить в группу, реализовывать интерфейс Runnable и иметь свое уникальное название:

            Runnable r  = new MyClassRunnable(); // в данном классе создается поток
ThreadGroup tg = new ThreadGroup(); // создаем группу потоков
Thread t = new Thread(tg, r, “myThread”);  // создаем экземпляр класса потока

Группы потоков удобно использовать, когда необходимо одинаково управлять несколькими потоками. Например, есть потоки, которые выводят данные  на печать, и необходимо прервать печать всех документов поставленных в очередь. В этом случае удобно применить команду к группе потока, а не к каждому потоку отдельно. Но это можно сделать, если потоки отнесены к одной группе.

Запуск потока
Для того чтобы запустить поток необходимо вызвать метод start().

Thread t = new Thread();
t.start();

Если вы вместо метода start() выполните метод run(), то run() выполнит свой код, но только в том же потоке, в котором и был вызван. Отдельный поток при этом не создастся.

Задержка, приостановка и прерывание потока

Задержка
Для того чтобы приостановить выполнение текущего потока необходимо выполнить статический метод sleep().
Данный метод задерживает поток на заданное время в миллисекундах и наносекундах, и имеет две перегруженные реализации:

sleep(long millis); - задает задержку в миллисекундах;
sleep(long millis, int nanos) – задает задержку в миллисекундах и наносекундах.

Данный метод может выбрасывать исключение InterruptedException.
Пример:

       public void run() {
             for(int i = 1; i <= 5; i++) {
                    System.out.println("Значение цикла дочернего потока - " + i);
                    try {
                           Thread.sleep(1000);
                    } catch (InterruptedException e) {
                           System.out.println(e);
                    }
             }
       }

Учтите, что данный метод не передает управление другому потоку, а только приостанавливает текущий на заданное время.

Приостановка
Приостановка потока, с передачей управления другому потоку производится статическим методом yield().
Метод не выбрасывает никаких исключений.
Пример:

package my.thread;

class AppThread extends Thread {
      
       public AppThread() {
            
       }
      
       public void run() {
            
             Thread ct = Thread.currentThread();
             System.out.println("Дочернийпоток - " + ct.getName());
             for(int i = 1; i <= 5; i++) {
                    System.out.println("Значение цикла дочернего потока " +
ct.getName() +" - " + i);        }
             System.out.println("Работа дочернего потока завершена - " +
 ct.getName());
       }
}
public class App {
      
       public static void main(String[] args) {
             System.out.println("Родительский поток.");
             for(int i = 1; i <= 10; i++){
                    Thread t = new Thread(new AppThread());
                    t.start();
                    Thread.yield();
             }
       }

}/* результат:
Родительский поток.
Дочерний поток - Thread-1
Значение цикла дочернего потока Thread-1 - 1
Дочерний поток - Thread-3
Значение цикла дочернего потока Thread-3 - 1
Значение цикла дочернего потока Thread-3 - 2
Значение цикла дочернего потока Thread-3 - 3
Значение цикла дочернего потока Thread-3 - 4
Значение цикла дочернего потока Thread-3 - 5
Работа дочернего потока завершена - Thread-3
Значение цикла дочернего потока Thread-1 - 2
Значение цикла дочернего потока Thread-1 - 3
Значение цикла дочернего потока Thread-1 - 4
Дочерний поток - Thread-7
Значение цикла дочернего потока Thread-1 - 5
. . . . . . . . . . ..
Дочерний поток - Thread-17
Значение цикла дочернего потока Thread-17 - 1
Значение цикла дочернего потока Thread-17 - 2
Значение цикла дочернего потока Thread-17 - 3
Значение цикла дочернего потока Thread-17 - 4
Значение цикла дочернего потока Thread-17 - 5
Работа дочернего потока завершена - Thread-17
Дочерний поток - Thread-19
Значение цикла дочернего потока Thread-19 - 1
Значение цикла дочернего потока Thread-19 - 2
Значение цикла дочернего потока Thread-19 - 3
Значение цикла дочернего потока Thread-19 - 4
Значение цикла дочернего потока Thread-19 - 5
Работа дочернего потока завершена - Thread-19
*/

Как мы видим, управление передается от одного потока другому.  У вас может быть другой результат выполнения программы.
В данном примере так же использовались метода:
·         Thread.currentThread() – получает объект Thread текущего потока;
·         getName() – получает имя потока. По умолчанию имя потока состоит из слова Thread и номера потока.

Прерывание
Прервать работу выполняемого потока можно с помощью метода interrupt().
Данный метод отправляет запрос на прекращение работы потока.
В момент вызова метода interrupt(), устанавливается статус прерывания для потока. Это флаг типа Boolean, который обязательно присутствует в любом потоке. В процессе работа поток периодически проверяет, следует ли ему прекратить выполнение.
Вы также  можете проверить, является ли поток прерванным с помощью методов isInterrupted()  и interrupted().
Отличия данных методов в том, что interrupted() является статическим методом и может применяться только к текущему потоку. Так же метод interrupted() сбрасывает статус прерывания потока. В то время, как метод  isInterrupted() является методом экземпляра, и позволяет проверить прерван ли любой поток. Так же метод isInterrupted() не изменяет статус прерывания.
В случае, когда для заблокированного потока (методами sleep() или wait()) вызывается метод interrupt(), работа потока прерывается и управление передается обработчику исключения InterruptedException().
При генерации исключения InterruptedException метод sleep() также сбрасывает статус прерывания.

Приоритеты потоков
При создании потока, по умолчанию, задается приоритет родительского потока. Но вы всегда можете изменить заданный параметр приоритета с помощью метода setPriority().
Например:

Runnable r = new MyRunnable();
Thread t = new Thread(r);
t.setPriority(8);

В Java можно задать приоритет в диапазоне от 1 до 10.
Так же приоритет потока можно задать через определенные, в классе константы:
  • Thread.MIN_PRIORITY – равняется самому низкому приоритету 1;
  • Thread.NORM_PRIORITY – равняется среднему приоритету 5 (данный приоритет задан по умолчанию);
  • Thread.MAX_PRIORITY – равняется самому высокому приоритету 10.

Узнать приоритет потока можно с помощью метода getPriority().
Устанавливая приоритеты потока, вы всегда должны помнить, что указание высокого или низкого приоритета не гарантирует, что поток, в очереди, будет выполнен раньше или позже. В данном случае планировщик потоков сам решает, какому потоку дать более высокий приоритет. Многое зависит от реализации потоков в операционной системе.

Определение состояния потока
Чтобы определить состояние потока используется метод isAlive().
Thread t = new Thread ();
. . . . . . . . . . .
t.isAlive();
или
Thread ct = Thread.currentThread();
ct.isAlive();
Если поток запушен или заблокирован, то возвращается значение true, е если поток является созданным (еще не запущенным) или остановленным, то возвращается значение false.
Определить, запущен поток или заблокирован – невозможно. Так же невозможно понять  создан поток или остановлен.

Присоединение к потоку
Иногда, для выполнения потока необходимо дождаться завершения другого потока. В этих случаях вам поможет метод join().
Если поток вызывает метод join(), для другого потока, то вызывающий поток приостанавливается до тех пор, пока вызываемый поток не завершит свою работу.
Данный метод имеет две перегруженные реализации:
  • join() – ожидает пока вызываемый поток не завершит свою реализации;
  • join(long milis) – ожидает завершения вызываемого потока указанное время, после чего передает управление вызывающему потоку.

Вызов метода join() может быть прерван вызовом метода interrupt() для вызывающего потока, поэтому метод join() размещают в блоке try-catch.
Пример:
package my.jt;
class Sleeper extends Thread {
      
       private int duration;
       public Sleeper(String name, int sleepTime) {
             super(name);
             duration = sleepTime;
             start();
       }
      
       public void run() {
             try {
                    sleep(duration);
             } catch (InterruptedException e) {
                    System.out.println(getName() + " прерван");
                    return;
             }
             System.out.println(getName() + " активизировался.");
       }
}

class Joiner implements Runnable {
       private Sleeper sleeper;
       private Thread t;
       public Joiner(String name, Sleeper sleeper) {
             this.sleeper = sleeper;
             t = new Thread(this);
             t.setName(name);
             t.start();
            
       }
      
       public void run() {
             try {
                    sleeper.join();
                    System.out.println(t.getName() + " завершен.");
             } catch (InterruptedException e) {
                    System.out.println(t.getName() + " прерван.");
             }
       }
      
       public Thread getThread() {
            
             return t;
       }
}
public class App {

       public static void main(String[] args) {
             Sleeper sleepy1 = new Sleeper("Sleepy 1", 1500),
                           sleepy2 = new Sleeper("Sleepy 2", 2000);
             Joiner joiner1 = new Joiner("Joiner 1", sleepy1),
                           joiner2 = new Joiner("Joiner 2", sleepy2);
             sleepy1.interrupt();
             joiner2.getThread().interrupt();
       }
}/* результат:
Joiner2 прерван.
Sleepy1 прерван
Joiner1 завершен.
Sleepy2 активизировался.
*/

Как видно из данного примера при прерывании  метода join() для потока joiner2 было выброшено исключение InterruptedException. Данное исключение привело к тому, что поток joiner2 прервался, не дождавшись завершения потока sleepy2.
В случае прерывания потока sleepy1, управление было передано потоку joiner1, который ожидал завершения потока sleepy1.

Потоки-демоны
Любой поток, кроме main (главный) можно сделать потоком-демоном. Для этого необходимо перед запуском потока вызвать метод setDaemon(), с аргументом true.
           Thread t = new Thread();
t.setDaemon(true);
t.start();
Основным назначением потока-демона является выполнение какой-то работы (например, таймер, проверка входящих сообщений) в фоновом режиме во время выполнения программы. При этом данный поток не является неотъемлемой частью программы. А это значит, что в случае завершения работы всех потоков, не являющихся демонами программа, завершает свою работу, не дожидаясь завершения работы потоков-демонов.
Чтобы узнать, является ли поток демоном, необходимо вызвать метод isDaemon(). Если поток является демоном, то все потоки, которые он производит, также будут демонами.
Также вы должны помнить, что потоки-демоны завершают свои методы run() без выполнения секции finally.
Ниже приведен пример создания потока-демона:
package my.jt;

import java.util.concurrent.TimeUnit;

class Daemon implements Runnable {
      
       public void run() {
             try {
                    Thread ct = Thread.currentThread();
                    while(true){
                           System.out.println("Запускаем поток-демон: " +
 ct.getName());
                           TimeUnit.MICROSECONDS.sleep(10);
                    }            }
             catch(InterruptedException e) {
                    System.out.println("Прерывание потока.");
             }
             finally {
                    System.out.println("Сюда, поток-демон, никогда не зайдет.");
             }
       }
}

public class App {

       public static void main(String[] args) {
             Runnable r = new Daemon();
             Thread t = new Thread(r, "daemon");
             t.setDaemon(true); // определяем поток, как демон
             t.start();
            
             for(int i = 1; i <= 100; i++) {
                    System.out.println("цикл - " + i);
             }
            
             System.out.println("Завершение программы.");
       }
}/* результат:
цикл - 1
цикл - 2
цикл - 3
цикл - 4
цикл - 5
Запускаем поток-демон: daemon
цикл - 6
. . . . . . . . .
цикл - 31
цикл - 32
Запускаем поток-демон: daemon
цикл - 33
. . . . . . . . .
цикл - 37
Запускаем поток-демон: daemon
цикл - 38
. . . . . . . . .
цикл - 41
Запускаем поток-демон: daemon
цикл - 42
Запускаем поток-демон: daemon
цикл - 43
. . . . . . . . .
цикл - 99
цикл - 100
Завершение программы.
Запускаем поток-демон: daemon
*/

Как мы можем видеть, при завершении программы не было выброшено исключения InterruptedException, а также секция finally не была выполнена. У вас может быть другой результат.

В следующей статье я хотел бы рассмотреть работу синхронизации потоков и пакет concurrent.

26 комментариев:

  1. спасбо, прочитал перед сном, ещё раз вспомнил основы.

    ОтветитьУдалить
  2. спасибо, не хватает только про wait и notify рассказать

    ОтветитьУдалить
    Ответы
    1. Рад, что статья помогает.
      Постараюсь в ближайшее время написать вторую часть статьи. Все никак руки не доходят. Но постараюсь исправиться :)

      Удалить
  3. Недавно начал изучать java. Многопоточность по Эккелю понимается гораздо труднее, а благодаря вашей статье разобрался. Большое спасибо! :))

    ОтветитьУдалить
    Ответы
    1. Очень приятно это слышать. Значит нужно продолжать начатое дело. Постараюсь, более шире раскрыть данную тему.

      Удалить
  4. Изучаю джава по Шилдту 8 издание, не мог норм понять многопотоки, спасибо вам, жду с нетерпением продолжения "синхронизации потоков и пакет concurrent."

    И еще вот никак не могу разобраться, почему после наследования Thread не надо обращаться к потоку напрямую(как ето делается Runnable) вот два разных вызова "ob1.t.join" - runnable, "ob1.join" наследование. Я понимаю что ето изза наследования, но вот конретно изза чего не могу понять.. Уже перечитывал раздел Наследование, но толку ноль. Клас как бы наследует Thread и все его переменные екземпляра, типа как "t". Но вот почему не надо на нее ссылаться, во время наследования остается для меня загадкой. Спасибо за любую помочь.

    ОтветитьУдалить
    Ответы
    1. Шилдт 8 издание - неплохая книга, но как по мне больше похожа на справочник. Хотя, каждому свой подход и изложение подходит.
      Как вариант можно обратить внимание на книги:
      - Java 2. Том 1. Основы. 8-е издание - Хорстманн, Корнелл
      - Java 2. Том 2. Тонкости программирования. 8-е издание - Хорстманн, Корнелл
      - Философия Java. Библиотека программиста. 4-е издание - Брюс Эккель.

      По поводу потоков:
      Runnable - это интерфейс потока, как бы метка говорящая, что этот класс реализует метод run() потока, но это не сам поток. И при реализации через интерфейс Runnable все равно нужно создавать поток (Thread) и передавать ему реализацию Runnable.
      При наследовании класса Thread вы просто расширяете уже готовый поток, и по-этому обращаетесь сразу к нему.

      То есть в первом случае obj1 - это экземпляр класса содержащего экземпляр потока, а во втором obj1 - это экземпляр класса расширяющего поток.

      Надеюсь получилось донести мысль сказанного :).

      Удалить
  5. Все понял, спасибо. Есть вопрос о deadlock. Там мне тоже сложно разобраться. В какое приблизительно время вам писать можно, чтоб вы по возможности отвечали, если есть желание помочь. Заранее спасибо.

    ОтветитьУдалить
    Ответы
    1. Deadlock возникает когда поток или потоки переходят в режим ожидания/блокировки и выйти из них не могут. В итоге поток(и) никогда не завершат своей работы. Постараюсь на этих выходных написать статейку по этому поводу. А то все как-то давно собираюсь, да руки не доходят.

      В какое время писать, даже не знаю. Иногда бываю днем, иногда вечером, как получается. Если смогу помочь, то с удовольствием помогу

      Удалить
  6. Молодец! Так держать! Отличные статьи, а главное - очень доступная манера изложения.

    ОтветитьУдалить
    Ответы
    1. Спасибо, постараюсь писать дальше. Но со временем немного сложновато)

      Удалить
  7. Этот комментарий был удален автором.

    ОтветитьУдалить
  8. Вот у меня, поток-демон, зашел в finally .После вызова у потока-демона метода t.interrupt(); ;) .
    Отличная статья ! Спасибо .

    ОтветитьУдалить
  9. доступно написано, спасибо большое автору!

    ОтветитьУдалить
  10. Спасибо , статья что надо , очень помогло !

    ОтветитьУдалить
  11. Спасибо , статья что надо , очень помогло !

    ОтветитьУдалить
  12. Благодарю за статью - все просто и понятно

    ОтветитьУдалить
  13. Спасибо за статью, когда будет следующая обещеная Вами статья?)))

    ОтветитьУдалить
  14. Отличная статья для начинающих) Спасибо!

    ОтветитьУдалить
  15. Этот комментарий был удален автором.

    ОтветитьУдалить
  16. Спасибо за статью! Не могу понять почему в последнем примере вывод начинается с результатов работы цикла фор, а не с фразы "Запускаем поток-демон: daemon". Ведь в мейне сперва запускается поток-демон, уже после начинается цикл фор. А в ране потока-демона цикл уайл начинается со строки "Запускаем поток-демон: daemon" и уже после её вывода запускается метод слип. Был бы признателен за пояснение.

    ОтветитьУдалить