JavaRush /Java блог /Java Developer /Многопоточность: что делают методы класса Thread
Автор
Pavlo Plynko
Java-разработчик в CodeGym

Многопоточность: что делают методы класса Thread

Статья из группы Java Developer
Привет! Сегодня продолжаем говорить о многопоточности. Рассмотрим класс Thread и работу его нескольких методов. Раньше, когда мы изучали методы класса, чаще всего просто писали это так: «название метода» -> «что он делает».
Многопоточность: что делают методы класса Thread - 1
С методами Thread так не получится :) Их логика сложнее, и без нескольких примеров не разобраться.

Метод Thread.start()

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

public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Выполнен поток " + getName());
   }
}


public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {
           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
Обрати внимание: чтобы запустить поток, необходимо вызвать специальный метод start(), а не метод run()! Эту ошибку легко допустить, особенно в начале изучения многопоточности. Если в нашем примере ты 10 раз вызовешь у объекта метод run() вместо start(), результат будет таким:

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {
           MyFirstThread thread = new MyFirstThread();
           thread.run();
       }
   }
}
Выполнен поток Thread-0 Выполнен поток Thread-1 Выполнен поток Thread-2 Выполнен поток Thread-3 Выполнен поток Thread-4 Выполнен поток Thread-5 Выполнен поток Thread-6 Выполнен поток Thread-7 Выполнен поток Thread-8 Выполнен поток Thread-9 Посмотри на последовательность вывода: все идет строго по порядку. Странно, да? Мы к такому не привыкли, ведь уже знаем, что порядок запуска и выполнения потоков определяет сверхразум внутри нашей операционной системы — планировщик потоков. Может, просто повезло? Конечно, дело не в везении. В этом можешь убедиться, запустив программу еще пару раз. Дело в том, что прямой вызов метода run() не имеет отношения к многопоточности. В этом случае программа будет выполнена в главном потоке — том, в котором выполняется метод main(). Он просто последовательно выведет 10 строк на консоль и все. Никакие 10 потоков не запустятся. Поэтому запомни на будущее и постоянно себя проверяй. Хочешь, чтобы выполнился run(), вызывай start(). Поехали дальше.

Метод Thread.sleep()

Для приостановки выполнения текущего потока на какое-то время, используем метод sleep(). Многопоточность: что делают методы класса Thread - 3Метод sleep() принимает в качестве параметра число миллисекунд, то есть то время, на которое необходимо «усыпить» поток.

public class Main {

   public static void main(String[] args) throws InterruptedException {

       long start = System.currentTimeMillis();

       Thread.sleep(3000);

       System.out.println(" - Сколько я проспал? \n - " + ((System.currentTimeMillis()-start)) / 1000 + " секунды");

   }
}
Вывод в консоль: - Сколько я проспал? - 3 секунды Обрати внимание: метод sleep() — статический: он усыпляет текущий поток. То есть тот, который работает в данный момент. Еще один важный нюанс: поток в состоянии сна можно прервать. В таком случае в программе возникнет исключение InterruptedException. Мы рассмотрим пример ниже. Кстати, а что произойдет после того, как поток «проснется»? Продолжит ли он сразу же свое выполнение с того места, где закончил? Нет. После того, как поток «просыпается» — когда заканчивается время, переданное в качестве аргумента в Thread.sleep(), — он переходит в состояние runnable, «работоспособный». Однако это не значит, что планировщик потоков запустит именно его. Вполне возможно, он отдаст предпочтение какому-то другому «неспящему» потоку, а наш «свежепроснувшийся» продолжит работу чуть позже. Обязательно запомни: «проснулся — не значит продолжил работать в ту же секунду»!

Метод Thread.join()

Многопоточность: что делают методы класса Thread - 4Метод join() приостанавливает выполнение текущего потока до тех пор, пока не завершится другой поток. Если у нас есть 2 потока, t1 и t2, и мы напишем —

t1.join()
t2 не начнет работу, пока t1 не завершит свою. Метод join() можно использовать, чтобы гарантировать последовательность выполнения потоков. Давай рассмотрим работу join() на примере:

public class ThreadExample extends Thread {

   @Override
   public void run() {

       System.out.println("Начало работы потока " + getName());

       try {
           Thread.sleep(5000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println("Поток " + getName() +  " завершил работу.");
   }
}


public class Main {

   public static void main(String[] args) throws InterruptedException {

       ThreadExample t1 = new ThreadExample();
       ThreadExample t2 = new ThreadExample();

       t1.start();

      
 /*Второй поток t2 начнет выполнение только после того, как будет завершен
       (или бросит исключение) первый поток - t1*/
       try {
           t1.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       t2.start();

       //Главный поток продолжит работу только после того, как t1 и t2 завершат работу
       try {
           t1.join();
           t2.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       System.out.println("Все потоки закончили работу, программа завершена");

   }
}
Мы создали простой класс ThreadExample. Его задача — вывести на экран сообщение о начале работы, потом уснуть на 5 секунд и в конце сообщить о завершении работы. Ничего сложного. Главная логика заключена в классе Main. Посмотри на комментарии: с помощью метода join() мы успешно управляем последовательностью выполнения потоков. Если ты вспомнишь начало темы, этим занимался планировщик потоков. Он запускал их на свое усмотрение: каждый раз по-разному. Здесь же мы с помощью метода гарантировали, что сначала будет запущен и выполнен поток t1, затем — t2, и только после них — главный поток выполнения программы. Идем дальше. В реальных программах тебе не раз встретятся ситуации, когда необходимо будет прервать выполнение какого-то потока. Например, наш поток выполняется, но при этом ждет определенного события или выполнения условия. Если это произошло, он останавливается. Было бы, наверное, логично, если бы существовал какой-то метод типа stop(). Однако все не так просто. Когда-то давно метод Thread.stop() в Java действительно был и позволял прерывать работу потока. Но позже его удалили из библиотеки Java. Ты можешь найти его в документации Oracle и увидеть, что он помечен как deprecated. Почему? Потому что он просто останавливал поток без какой-либо дополнительной работы. Например, поток мог работать с данными и что-то в них менять. Потом его резко вырубали методом stop() посреди работы — и все. Ни корректного завершения работы, ни освобождения ресурсов, ни хотя бы обработки ошибок — ничего этого не было. Метод stop(), если утрировать, просто крушил все на своем пути. Его работу можно сравнить с тем, как кто-то выдергивает вилку из розетки, чтобы выключить компьютер. Да, нужного результата добиться можно. Но все понимают, что через пару недель компьютер не скажет за это «спасибо». По этой причине логику прерывания потоков в Java изменили, и теперь используется специальный метод — interrupt().

Метод Thread.interrupt()

Что произойдет, если у потока вызвать метод interrupt()? Есть 2 варианта:
  1. Если объект находился в этот момент в состоянии ожидания, например, join или sleep, ожидание будет прервано, и программа выбросит InterruptedException.
  2. Если же поток в этот момент был в работоспособном состоянии, у объекта будет установлен boolean-флаг interrupted.
Но проверить объект на значение этого флага и корректно завершить работу мы должны будем самостоятельно! Для этого в классе Thread есть специальный метод — boolean isInterrupted(). Давай вернемся к примеру с часами, который был в лекции основного курса. Для удобства он немного упрощен:

public class Clock extends Thread {

   public static void main(String[] args) throws InterruptedException {
       Clock clock = new Clock();
       clock.start();

       Thread.sleep(10000);
       clock.interrupt();
   }

   public void run() {
       Thread current = Thread.currentThread();

       while (!current.isInterrupted())
       {
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               System.out.println("Работа потока была прервана");
               break;
           }
           System.out.println("Tik");
       }
   }
}
В нашем случае часы стартуют и начинают тикать каждую секунду. На 10-й секунде мы прерываем поток часов. Как ты уже знаешь, если поток, который мы пытаемся прервать, находится в одном из состояний ожидания, это приводит к InterruptedException. Данный вид исключения — проверяемый, поэтому его можно легко перехватить и выполнить нашу логику завершения программы. Что мы и сделали. Вот наш результат: Tik Tik Tik Tik Tik Tik Tik Tik Tik Работа потока была прервана На этом мы заканчиваем знакомство с основными методами класса Thread. Чтобы закрепить знания, можешь посмотреть эту видеолекцию о многопоточности:
она послужит отличным дополнительным материалом! В конце, после обзора методов, в ней рассказывается как раз о том, что мы будем проходить дальше по курсу :) Успехов!
Комментарии (223)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Novikova Natalia Уровень 28
9 февраля 2024
по закону Амдала , если машина одноядерная, как приводит пример автор видео, ускорение будет равно единице , при любом количестве вычислений , которые нужно распараллелить, значит , график не может идти стремительно вниз, или я что то не точно поняла
Novikova Natalia Уровень 28
31 января 2024
Обязательно запомни: «проснулся — не значит продолжил работать в ту же секунду»!😁 Скажите это ребятам, кто просыпается за минуту до дейли митинга
Anatoly Уровень 30
26 января 2024
оке
Dmitriy Kvitka Уровень 29
13 января 2024
Метод run() в последнем примере написан не сосем корректно:

   public void run() {
       Thread current = Thread.currentThread();

       while (!current.isInterrupted())
       {
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               System.out.println("Работа потока была прервана");
               break;
           }
           System.out.println("Tik");
       }
   }
Если работа потока будет прервана не в процессе сна, то фраза "Работа потока была прервана" выведена не будет. И становится не понятно, либо работа была прервана, либо - завершена.
10 января 2024
Жесть, и это реально нам понадобится в программировании?
StasKlg Уровень 23 Expert
23 ноября 2023
стало понятнее..
Максим Li Уровень 36
19 ноября 2023
Неплохо
Андрей Уровень 40
2 ноября 2023
В самом начале не совсем верно написано: Дело в том, что прямой вызов метода run() не имеет отношения к многопоточности. В этом случае программа будет выполнена в главном потоке — том, в котором выполняется метод main(). Он просто последовательно выведет 10 строк на консоль и все. Никакие 10 потоков не запустятся. Но в примере как раз видно, что запускаются десять потоков (Thread-0, Thread-1...). Просто последовательно, а не параллельно.
Afonya Уровень 22
17 октября 2023
как в закладки помещать такие полезные статьи, я имею ввиду непосредственно в своём кабинете?
Sma1l Уровень 30
8 июля 2023


public static void main(String[] args) throws InterruptedException {

       ThreadExample t1 = new ThreadExample();
       ThreadExample t2 = new ThreadExample();

       t1.start();


 /*Второй поток t2 начнет выполнение только после того, как будет завершен
       (или бросит исключение) первый поток - t1*/
       try {
           t1.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       t2.start();

       //Главный поток продолжит работу только после того, как t1 и t2 завершат работу
       try {
           t1.join();
           t2.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
почему мы еще раз проверяем отработала ли первая нить , если вторая должна запуститься после завершения первой нити ?