Опубликован: 11.12.2003 | Уровень: специалист | Доступ: платный
Лекция 12:

Потоки выполнения. Синхронизация

< Лекция 11 || Лекция 12: 1234 || Лекция 13 >

Базовые классы для работы с потоками

Класс Thread

Поток выполнения в Java представляется экземпляром класса Thread. Для того, чтобы написать свой поток исполнения, необходимо наследоваться от этого класса и переопределить метод run(). Например,

public class MyThread extends Thread {
   public void run() {
      // некоторое долгое действие, вычисление
      long sum=0;
      for (int i=0; i<1000; i++) {
         sum+=i;
      }
      System.out.println(sum);
   }
}

Метод run() содержит действия, которые должны выполняться в новом потоке исполнения. Чтобы запустить его, необходимо создать экземпляр класса-наследника и вызвать унаследованный метод start(), который сообщает виртуальной машине, что требуется запустить новый поток исполнения и начать выполнять в нем метод run().

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

В результате чего на консоли появится результат:

499500

Когда метод run() завершен (в частности, встретилось выражение return ), поток выполнения останавливается. Однако ничто не препятствует записи бесконечного цикла в этом методе. В результате поток не прервет своего исполнения и будет остановлен только при завершении работы всего приложения.

Интерфейс Runnable

Описанный подход имеет один недостаток. Поскольку в Java множественное наследование отсутствует, требование наследоваться от Thread может привести к конфликту. Если еще раз посмотреть на приведенный выше пример, станет понятно, что наследование производилось только с целью переопределения метода run(). Поэтому предлагается более простой способ создать свой поток исполнения. Достаточно реализовать интерфейс Runnable, в котором объявлен только один метод – уже знакомый void run(). Запишем пример, приведенный выше, с помощью этого интерфейса:

public class MyRunnable implements Runnable {
   public void run() {
      // некоторое долгое действие, вычисление
      long sum=0;
      for (int i=0; i<1000; i++) {
         sum+=i;
      }
      System.out.println(sum);
   }
}

Также незначительно меняется процедура запуска потока:

Runnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();

Если раньше объект, представляющий сам поток выполнения, и объект с методом run(), реализующим необходимую функциональность, были объединены в одном экземпляре класса MyThread, то теперь они разделены. Какой из двух подходов удобней, решается в каждом конкретном случае.

Подчеркнем, что Runnable не является полной заменой классу Thread, поскольку создание и запуск самого потока исполнения возможно только через метод Thread.start().

Работа с приоритетами

Рассмотрим, как в Java можно назначать потокам приоритеты. Для этого в классе Thread существуют методы getPriority() и setPriority(), а также объявлены три константы:

MIN_PRIORITY
MAX_PRIORITY
NORM_PRIORITY

Из названия понятно, что их значения описывают минимальное, максимальное и нормальное (по умолчанию) значения приоритета.

Рассмотрим следующий пример:

public class ThreadTest implements Runnable {
  public void run() {
    double calc;
    for (int i=0; i<50000; i++) {
      calc=Math.sin(i*i);
      if (i%10000==0) {
        System.out.println(getName()+
          " counts " + i/10000);
      }
    }
  }

  public String getName() {
     return Thread.currentThread().getName();
  }

  public static void main(String s[]) {
    // Подготовка потоков
    Thread t[] = new Thread[3];
    for (int i=0; i<t.length; i++) {
      t[i]=new Thread(new ThreadTest(), 
                     "Thread "+i);
    }
    // Запуск потоков
    for (int i=0; i<t.length; i++) {
      t[i].start();
      System.out.println(t[i].getName()+
                         " started");
    }
  }
}

В примере используется несколько новых методов класса Thread:

  • getName()

    Обратите внимание, что конструктору класса Thread передается два параметра. К реализации Runnable добавляется строка. Это имя потока, которое используется только для упрощения его идентификации. Имена нескольких потоков могут совпадать. Если его не задать, то Java генерирует простую строку вида "Thread-" и номер потока (вычисляется простым счетчиком). Именно это имя возвращается методом getName(). Его можно сменить с помощью метода setName().

  • currentThread()

    Этот статический метод позволяет в любом месте кода получить ссылку на объект класса Thread, представляющий текущий поток исполнения.

Результат работы такой программы будет иметь следующий вид:

Thread 0 started
Thread 1 started
Thread 2 started
Thread 0 counts 0
Thread 1 counts 0
Thread 2 counts 0
Thread 0 counts 1
Thread 1 counts 1
Thread 2 counts 1
Thread 0 counts 2
Thread 2 counts 2
Thread 1 counts 2
Thread 2 counts 3
Thread 0 counts 3
Thread 1 counts 3
Thread 2 counts 4
Thread 0 counts 4
Thread 1 counts 4

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

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

public static void main(String s[]) {
// Подготовка потоков
   Thread t[] = new Thread[3];
   for (int i=0; i<t.length; i++) {
      t[i]=new Thread(new ThreadTest(), 
                      "Thread "+i);
      t[i].setPriority(Thread.MIN_PRIORITY + 
      (Thread.MAX_PRIORITY - 
      Thread.MIN_PRIORITY)/t.length*i);
   }

// Запуск потоков
   for (int i=0; i<t.length; i++) {
      t[i].start();
      System.out.println(t[i].getName()+
	                     " started");
   }
}

Формула вычисления приоритетов позволяет равномерно распределить все допустимые значения для всех запускаемых потоков. На самом деле, константа минимального приоритета имеет значение 1, максимального 10, нормального 5. Так что в простых программах можно явно пользоваться этими величинами и указывать в качестве, например, пониженного приоритета значение 3.

Результатом работы будет:

Thread 0 started
Thread 1 started
Thread 2 started
Thread 2 counts 0
Thread 2 counts 1
Thread 2 counts 2
Thread 2 counts 3
Thread 2 counts 4
Thread 0 counts 0
Thread 1 counts 0
Thread 1 counts 1
Thread 1 counts 2
Thread 1 counts 3
Thread 1 counts 4
Thread 0 counts 1
Thread 0 counts 2
Thread 0 counts 3
Thread 0 counts 4

Потоки, как и раньше, стартуют последовательно. Но затем мы видим, что чем выше приоритет, тем быстрее отрабатывает поток. Тем не менее, весьма показательно, что поток с минимальным приоритетом ( Thread 0 ) все же получил возможность выполнить одно действие раньше, чем отработал поток с более высоким приоритетом ( Thread 1 ). Это говорит о том, что приоритеты не делают систему однопоточной, выполняющей единовременно лишь один поток с наивысшим приоритетом. Напротив, приоритеты позволяют одновременно работать над несколькими задачами с учетом их важности.

Если увеличить параметры метода (выполнять 500000 вычислений, а не 50000, и выводить сообщение каждое 1000-е вычисление, а не 10000-е), то можно будет наглядно увидеть, что все три потока имеют возможность выполнять свои действия одновременно, просто более высокий приоритет позволяет выполнять их чаще.

Демон-потоки

Демон -потоки позволяют описывать фоновые процессы, которые нужны только для обслуживания основных потоков выполнения и не могут существовать без них. Для работы с этим свойством существуют методы setDaemon() и isDaemon().

Рассмотрим следующий пример:

public class ThreadTest implements Runnable {

   // Отдельная группа, в которой будут
   // находиться все потоки ThreadTest
   public final static ThreadGroup GROUP = new ThreadGroup("Daemon demo");

   // Стартовое значение, указывается при создании объекта
   private int start;

   public ThreadTest(int s) {
      start = (s%2==0)? s: s+1;
      new Thread(GROUP, this, "Thread "+ start).start();
   }

public void run() {
      // Начинаем обратный отсчет
      for (int i=start; i>0; i--) {
         try {
            Thread.sleep(300);
         } catch (InterruptedException e) {}
         // По достижении середины порождаем
         // новый поток с половинным начальным
         // значением
         if (start>2 && i==start/2)
		 {
           new ThreadTest(i);
         }
      }
   }

public static void main(String s[]) {
   new ThreadTest(16);
   new DaemonDemo();
   }
}
public class DaemonDemo extends Thread {
   public DaemonDemo() {
      super("Daemon demo thread");
      setDaemon(true);
      start();
   }

   public void run() {
      Thread threads[]=new Thread[10];
      while (true) {
         // Получаем набор всех потоков из
         // тестовой группы
         int count=ThreadTest.GROUP.activeCount();
         if (threads.length<count) threads = new Thread[count+10];
         count=ThreadTest.GROUP.enumerate(threads);

         // Распечатываем имя каждого потока
         for (int i=0; i<count; i++) {
            System.out.print(threads[i].getName()+", ");
         }
         System.out.println();
         try {
            Thread.sleep(300);
         } catch (InterruptedException e) {}
      }
   }
}
Пример 12.1.

В этом примере происходит следующее. Потоки ThreadTest имеют некоторое стартовое значение, передаваемое им при создании. В методе run() это значение последовательно уменьшается. При достижении половины от начальной величины порождается новый поток с вдвое меньшим начальным значением. По исчерпании счетчика поток останавливается. Метод main() порождает первый поток со стартовым значением 16. В ходе программы будут дополнительно порождены потоки со значениями 8, 4, 2.

За этим процессом наблюдает демон -поток DaemonDemo. Этот поток регулярно получает список всех существующих потоков ThreadTest и распечатывает их имена для удобства наблюдения.

Результатом программы будет:

Thread 16, 
Thread 16, 
Thread 16, 
Thread 16, 
Thread 16, 
Thread 16, 
Thread 16, 
Thread 16, 
Thread 16, 
Thread 16, Thread 8, 
Thread 16, Thread 8, 
Thread 16, Thread 8, 
Thread 16, Thread 8, 
Thread 16, Thread 8, 
Thread 16, Thread 8, Thread 4, 
Thread 16, Thread 8, Thread 4, 
Thread 8, Thread 4, 
Thread 4, Thread 2, 
Thread 2,
Пример 12.2.

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

В примере использовалось несколько дополнительных классов и методов, которые еще не были рассмотрены:

  • класс ThreadGroup

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

    Методы activeCount() и enumerate() возвращают количество и полный список, соответственно, всех потоков в группе.

  • sleep()

    Этот статический метод класса Thread приостанавливает выполнение текущего потока на указанное количество миллисекунд. Обратите внимание, что метод требует обработки исключения InterruptedException. Он связан с возможностью активизировать метод, который приостановил свою работу. Например, если поток занят выполнением метода sleep(), то есть бездействует на протяжении указанного периода времени, его можно вывести из этого состояния, вызвав метод interrupt() из другого потока выполнения. В результате метод sleep() прервется исключением InterruptedException.

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

< Лекция 11 || Лекция 12: 1234 || Лекция 13 >
Вадим Кудаев
Вадим Кудаев

Добрый день! Начал проходить курс "Программирование на Java". Как я понимаю,курс создавался приблизительно в 2015 году. Не потерял ли данный курс свою актуальность? Стоит ли проходить его в 2023 году, или же лучше найти что-то более новое?

Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Даниил Вирченко
Даниил Вирченко
Россия, Краснодар