GrabDuck

Многопоточность в 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 завершен .

Sleepy 2 активизировался.

*/

Как видно из данного примера при прерывании  метода 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.