Привет! Сегодня продолжаем говорить о многопоточности. Рассмотрим класс Thread и работу его нескольких методов. Раньше, когда мы изучали методы класса, чаще всего просто писали это так: «название метода» -> «что он делает». С методами Thread так не получится :) Их логика сложнее, и без нескольких примеров не разобраться. Если коротко: класс Thread управляет потоками через несколько ключевых методов. start() запускает поток (именно start(), а не run()), sleep(мс) усыпляет текущий поток на заданное время, join() заставляет текущий поток дождаться завершения другого, а interrupt() просит поток прерваться, isInterrupted() проверяет флаг прерывания. Устаревший stop() использовать нельзя: он останавливал поток небезопасно.

Кратко

  • start() запускает поток и вызывает его run() в новом потоке; прямой вызов run() выполняется в текущем потоке, без многопоточности.
  • sleep(мс) это статический метод: усыпляет текущий поток на заданное число миллисекунд и может быть прерван (InterruptedException).
  • Проснувшийся поток переходит в состояние runnable, но продолжит работу не мгновенно: момент решает планировщик.
  • join() приостанавливает текущий поток, пока не завершится указанный поток; помогает задать порядок выполнения.
  • interrupt() прерывает ожидание (InterruptedException) или ставит флаг interrupted; корректное завершение реализует сам программист через isInterrupted().
  • Метод stop() устарел (deprecated) и небезопасен: он обрывал поток без освобождения ресурсов.

Метод Thread.start()

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

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(). Метод sleep() усыпляет текущий поток на заданное времяМетод 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()

Метод join() заставляет поток дождаться завершения другого потокаМетод 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 действительно был и позволял прерывать работу потока. Но позже его объявили устаревшим (deprecated). Ты можешь найти его в документации 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.

Методы класса Thread: сводка

Все разобранные методы в одной таблице.
МетодЧто делаетСтатический?Ключевые нюансы
start()Запускает поток и вызывает его run() в новом потокенетЗапускать поток нужно им, а не run()
run()Содержит логику потоканетПрямой вызов выполняется в текущем потоке, без многопоточности
sleep(мс)Усыпляет текущий поток на заданное времядаМожет быть прерван, бросает InterruptedException
join()Ждет завершения другого потоканетЗадает порядок выполнения потоков
interrupt()Просит поток прерватьсянетПрерывает ожидание (InterruptedException) или ставит флаг interrupted
isInterrupted()Проверяет флаг прерываниянетВозвращает boolean; корректное завершение пишет сам программист
stop()Останавливал поток (устарел)даdeprecated и небезопасен, с Java 20 бросает UnsupportedOperationException
Чтобы закрепить знания, можешь посмотреть эту видеолекцию о многопоточности:
она послужит отличным дополнительным материалом! В конце, после обзора методов, в ней рассказывается как раз о том, что мы будем проходить дальше по курсу :) Успехов!

Вопросы и ответы

Чем метод start() отличается от run()?

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

Что делает метод Thread.sleep()?

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

Продолжит ли поток работу сразу после сна?

Нет. После пробуждения поток переходит в состояние runnable, но когда именно он продолжит работу, решает планировщик потоков. Может сначала поработать другой поток.

Для чего нужен метод join()?

join() приостанавливает текущий поток, пока не завершится другой поток. Он помогает гарантировать нужный порядок выполнения потоков.

Как правильно прерывать поток вместо stop()?

Методом interrupt(): он прерывает ожидание (InterruptedException) или ставит флаг interrupted. Проверку флага через isInterrupted() и корректное завершение работы программист реализует сам.

Почему нельзя использовать Thread.stop()?

Он устарел (deprecated) и небезопасен: обрывал поток посреди работы, не освобождая ресурсы и не завершая логику корректно. Начиная с Java 20 вызов stop() выбрасывает UnsupportedOperationException. Источник: Oracle: класс Thread.

Читайте также