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. Чтобы закрепить знания, можешь посмотреть эту видеолекцию о многопоточности:
она послужит отличным дополнительным материалом! В конце, после обзора методов, в ней рассказывается как раз о том, что мы будем проходить дальше по курсу :) Успехов!
Комментарии (210)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
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();
       }
почему мы еще раз проверяем отработала ли первая нить , если вторая должна запуститься после завершения первой нити ?
No Name Уровень 32
30 июня 2023
+ статья в копилке
Ислам Уровень 32
22 июня 2023
Nice
PHANTOM Уровень 40 Expert
15 февраля 2023
Как посмотреть все статьи данного автора? А то написано "Давай вспомним пример из предыдущей лекции" а где эту лекцию найти хз
Быкова Регина Уровень 31
6 февраля 2023
Зачем проверка isInterrupted в while (!current.isInterrupted()) ? Если использовать while (true), то в Thread.sleep все равно будет проверка и выброс InterruptedException.
partiec Уровень 28
17 января 2023
Цитата: "Давай вернемся к примеру с часами, который был в лекции основного курса." Что за "основной курс"?
Lafaed Уровень 36
30 декабря 2022
Конечно я и сам не оратор, но смотреть этот ролик невозможно : ээ, эмм, вот, так вот, вот - тот еще мямля. Простите но это просто не качественный контент разжиженый водичкой . Еще с красным маркером , едва видным, по белому холсту, ужас.
Андрей Уровень 28
1 декабря 2022
Дело в том, что прямой вызов метода run() не имеет отношения к многопоточности. В этом случае программа будет выполнена в главном потоке — том, в котором выполняется метод main(). Он просто последовательно выведет 10 строк на консоль и все. Никакие 10 потоков не запустятся. Почему же тогда метод getName() выдает 10 разных значений? Откуда 10 разных имён потоков, если всё выполняется в главном потоке? Кто-то может объяснить? Попробовал переписать код с имплементацией Runnable вместо наследования Thread:

public class MainRunnable {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new mulithreading.MyFirstThread2());
            thread.run();
        }
    }
}

class MyFirstThread2 implements Runnable {

    @Override
    public void run() {
        System.out.println("Выполнен поток " + Thread.currentThread().getName());
    }
}
Вот тут да, вывод ожидаемый:

Выполнен поток main
Выполнен поток main
Выполнен поток main
Выполнен поток main
Выполнен поток main
Выполнен поток main
Выполнен поток main
Выполнен поток main
Выполнен поток main
Выполнен поток main
27 ноября 2022
ВНИМАНИЕ, ВОПРОС! Зачем в методе run в цикле while с условием !current.isInterrupted() при выкидывании InterruptedException мы в блоке catch дополнительно завершаем цикл с помощью break, если в условии цикла у нас уже есть проверка на isInterrupted? По-моему что-то одно следует убрать, либо условие из цикла и оставить только while (true), либо break при обработке исключения.


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");
       }
   }
}
Grock Уровень 42
11 октября 2022
Интересно, если есть 4 потока, можно ли сделать так, чтобы: - 1-й поток ждал пока не заверлится 3-й; - 2-й поток ждал пока не заверлится 4-й; и все это параллельно. - 3-й потом работал параллельно 4-му; - после завершение 3-го и 4-го потоков 1-й и 2-й потоки работали также параллельно (при условии что 3-й и 4-й потоки завершились одновременно). Как будет выглядеть код? Метод join(); как я понял, регулирует очередность потоков, но не позволяет "создавать несколько параллельных очередностей" или я не прав? Поясните, пожалуйста, знатоки :)