Представь, что твоя семья переезжает из одного дома в другой. Важная часть переезда — собрать книги. Книг у вас накопилось много, и надо сложить их в коробки.
Сейчас свободен только ты. Мама готовит еду, брат собирает одежду, а сестра ушла в магазин. В одиночку ты худо-бедно справляешься, и, рано или поздно, даже сам выполнишь задание, но времени понадобится очень много. Впрочем, через 20 минут из магазина вернется твоя сестра, и других дел у нее нет. Так что она может присоединиться к тебе.
Задача не менялась: сложить книги в коробки. Только выполняется в два раза быстрее. Почему?
Потому что работа делается параллельно. Два разных «потока» (ты и твоя сестра) одновременно выполняют одну и ту же задачу и, если ничего не изменится, разница по времени будет очень большой по сравнению с ситуацией, в которой ты бы делал все один.
Если брат скоро справится со своей задачей, он может помочь вам, и дело пойдет еще быстрее.
Проблемы, которые решает многопоточность в Java
По сути, многопоточность Java была придумана, чтобы решить две главные задачи:Одновременно выполнять несколько действий.
В примере выше разные потоки (т.е. члены семьи) параллельно выполняли несколько действий: мыли посуду, ходили в магазин, складывали вещи.
Можно привести и более «программистский» пример. Представь, что у тебя есть программа с пользовательским интерфейсом. При нажатии кнопки «Продолжить» внутри программы должны произойти какие-то вычисления, а пользователь должен увидеть следующий экран интерфейса. Если эти действия осуществляются последовательно, после нажатия кнопки «Продолжить» программа просто зависнет. Пользователь будет видеть все тот же экран с кнопкой «Продолжить», пока все внутренние вычисления не будут выполнены, и программа не дойдет до части, где начнется отрисовка интерфейса.
Что ж, подождем пару минут!
![Многопоточность в Java: суть, «плюсы» и частые ловушки - 3]()
А еще мы можем переделать нашу программу, или, как говорят программисты, «распараллелить». Пусть нужные вычисления выполняются в одном потоке, а отрисовка интерфейса — в другом. У большинства компьютеров хватит на это ресурсов. В таком случае программа не будет «тупить», и пользователь будет спокойно переходить между экранами интерфейса не заботясь о том, что происходит внутри. Одно другому не мешает :)
Ускорить вычисления.
Тут все намного проще. Если наш процессор имеет несколько ядер, а большинство процессоров сейчас многоядерные, список наших задач могут параллельно решать несколько ядер. Очевидно, что если нам нужно решить 1000 задач и каждая из них решается за секунду, одно ядро справится со списком за 1000 секунд, два ядра — за 500 секунд, три — за 333 с небольшим секунды и так далее.
Thread. То есть чтобы создать и запустить выполнение 10 потоков, понадобится 10 объектов этого класса.
Напишем самый простой пример:
public class MyFirstThread extends Thread {
@Override
public void run() {
System.out.println("I'm Thread! My name is " + getName());
}
}
Чтобы формировать и запускать потоки, нам нужно создать класс, унаследовать его от класса java.lang.Thread и переопределить в нем метод run().
Последнее — очень важно. Именно в методе run() мы прописываем ту логику, которую наш поток должен выполнить.
Теперь, если мы создадим экземпляр MyFirstThread и запустим его, метод run() выведет в консоль строку с его именем: метод getName() выводит «системное» имя потока, которое присваивается автоматически.
Хотя, собственно, почему «если»? Давай создадим и проверим!
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MyFirstThread thread = new MyFirstThread();
thread.start();
}
}
}
Вывод в консоль:
I'm Thread! My name is Thread-2
I'm Thread! My name is Thread-1
I'm Thread! My name is Thread-0
I'm Thread! My name is Thread-3
I'm Thread! My name is Thread-6
I'm Thread! My name is Thread-7
I'm Thread! My name is Thread-4
I'm Thread! My name is Thread-5
I'm Thread! My name is Thread-9
I'm Thread! My name is Thread-8
Создаем 10 потоков (объектов) MyFirstThread, который наследуется от Thread и запускаем их, вызывая у объекта метод start(). После вызова метода start() начинает работу его метод run(), и выполняется та логика, которая была в нем написана.
Обрати внимание: имена потоков идут не по порядку. Это довольно странно, почему они не выполнялись по очереди: Thread-0, Thread-1, Thread-2 и так далее?
Это как раз пример того, когда стандартное, «последовательное» мышление не подойдет. Дело в том, что мы в данном случае только отдаем команды на создание и запуск 10 потоков. В каком порядке их запускать — решает планировщик потоков: особый механизм внутри операционной системы.
Как именно он устроен и по какому принципу принимает решения — тема очень сложная, и сейчас не будем в нее погружаться. Главное запомни, что последовательность выполнения потоков программист контролировать не может.
Чтобы осознать серьезность ситуации, попробуй запустить метод main() из примера выше еще пару раз.
Второй вывод в консоль:
I'm Thread! My name is Thread-0
I'm Thread! My name is Thread-4
I'm Thread! My name is Thread-3
I'm Thread! My name is Thread-2
I'm Thread! My name is Thread-1
I'm Thread! My name is Thread-5
I'm Thread! My name is Thread-6
I'm Thread! My name is Thread-8
I'm Thread! My name is Thread-9
I'm Thread! My name is Thread-7
Третий вывод в консоль:
I'm Thread! My name is Thread-0
I'm Thread! My name is Thread-3
I'm Thread! My name is Thread-1
I'm Thread! My name is Thread-2
I'm Thread! My name is Thread-6
I'm Thread! My name is Thread-4
I'm Thread! My name is Thread-9
I'm Thread! My name is Thread-5
I'm Thread! My name is Thread-7
I'm Thread! My name is Thread-8
Проблемы, которые создает многопоточность
На примере с книгами ты увидел, что многопоточность решает достаточно важные задачи, и ее использование ускоряет работу наших программ. Во многих случаях — в разы. Но многопоточность недаром считается сложной темой. Ведь при неправильном использовании она создает проблемы вместо того, чтобы решать их. Говоря «создавать проблемы» я не имею в виду что-то абстрактное. Есть две конкретные проблемы, которые может вызвать использование многопоточности — взаимная блокировка (deadlock) и состояние гонки (race condition). Deadlock — ситуация, при которой несколько потоков находятся в состоянии ожидания ресурсов, занятых друг другом, и ни один из них не может продолжать выполнение. Мы еще поговорим о нем в следующих лекциях, пока достаточно этого примера:
Представь, что поток-1 работает с каким-то Объектом-1, а поток-2 работает с Объектом-2. При этом программа написана так:
- Поток-1 перестанет работать с Объектом-1 и переключится на Объект-2, как только Поток-2 перестанет работать с Объектом 2 и переключится на Объект-1.
- Поток-2 перестанет работать с Объектом-2 и переключится на Объект-1, как только Поток-1 перестанет работать с Объектом 1 и переключится на Объект-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();
}
}
}
A теперь представь, что программа отвечает за работу робота, который готовит еду!
Поток-0 достает яйца из холодильника.
Поток-1 включает плиту.
Поток-2 достает сковородку и ставит на плиту.
Поток-3 зажигает огонь на плите.
Поток-4 выливает на сковороду масла.
Поток-5 разбивает яйца и выливает их на сковороду.
Поток-6 выбрасывает скорлупу в мусорное ведро.
Поток-7 снимает готовую яичницу с огня.
Поток-8 выкладывает яичницу в тарелку.
Поток-9 моет посуду.
Посмотри на результаты работы нашей программы:
Выполнен поток Thread-0
Выполнен поток Thread-2
Выполнен поток Thread-1
Выполнен поток Thread-4
Выполнен поток Thread-9
Выполнен поток Thread-5
Выполнен поток Thread-8
Выполнен поток Thread-7
Выполнен поток Thread-3
Выполнен поток Thread-6
Веселый получился сценарий? :) А все потому, что работа нашей программы зависит от порядка выполнения потоков.
При малейшем нарушении последовательности наша кухня превращается в ад, а сошедший с ума робот крушит все вокруг себя. Это тоже распространенная проблема в многопоточном программировании, о которой ты еще не раз услышишь.
В завершение лекции, хочу посоветовать тебе книгу, посвященную многопоточности.


ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ