Проблемы, которые решает многопоточность в Java
По сути, многопоточность Java была придумана, чтобы решить две главные задачи:Одновременно выполнять несколько действий.
В примере выше разные потоки (т.е. члены семьи) параллельно выполняли несколько действий: мыли посуду, ходили в магазин, складывали вещи.
Можно привести и более «программистский» пример. Представь, что у тебя есть программа с пользовательским интерфейсом. При нажатии кнопки «Продолжить» внутри программы должны произойти какие-то вычисления, а пользователь должен увидеть следующий экран интерфейса. Если эти действия осуществляются последовательно, после нажатия кнопки «Продолжить» программа просто зависнет. Пользователь будет видеть все тот же экран с кнопкой «Продолжить», пока все внутренние вычисления не будут выполнены, и программа не дойдет до части, где начнется отрисовка интерфейса.
Что ж, подождем пару минут!
А еще мы можем переделать нашу программу, или, как говорят программисты, «распараллелить». Пусть нужные вычисления выполняются в одном потоке, а отрисовка интерфейса — в другом. У большинства компьютеров хватит на это ресурсов. В таком случае программа не будет «тупить», и пользователь будет спокойно переходить между экранами интерфейса не заботясь о том, что происходит внутри. Одно другому не мешает :)
Ускорить вычисления.
Тут все намного проще. Если наш процессор имеет несколько ядер, а большинство процессоров сейчас многоядерные, список наших задач могут параллельно решать несколько ядер. Очевидно, что если нам нужно решить 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
Веселый получился сценарий? :) А все потому, что работа нашей программы зависит от порядка выполнения потоков.
При малейшем нарушении последовательности наша кухня превращается в ад, а сошедший с ума робот крушит все вокруг себя. Это тоже распространенная проблема в многопоточном программировании, о которой ты еще не раз услышишь.
В завершение лекции, хочу посоветовать тебе книгу, посвященную многопоточности.