Читаем Многопоточное программирование в Java полностью

Многопоточное программирование в Java

В многопроцессорных системах многопоточность решает проблему параллельного выполнения кода с наименьшими затратами. Поэтому мно…

Тимур Сергеевич Машнин

Компьютеры и Интернет / Зарубежная компьютерная, околокомпьютерная литература 18+

Этот закон гласит следующее — В случае, когда задача разделяется на несколько частей, суммарное время её выполнения на параллельной системе не может быть меньше времени выполнения самого длинного фрагмента.

Согласно этому закону, ускорение выполнения программы за счёт распараллеливания её инструкций на множестве вычислителей, ограничено временем, необходимым для выполнения её последовательных инструкций.

Потоки имеют собственный стек вызовов, но также могут обращаться к общим данным. Поэтому у вас есть две основные проблемы, проблемы с видимостью и доступом.

Проблема видимости возникает, если поток A читает общие данные, которые позже изменяются потоком B, а поток A не знает об этом изменении.

Проблема доступа может возникнуть, если несколько потоков получают доступ и изменяют одновременно одни и те же общие данные.

Проблема видимости и доступа может привести к сбою в работе — программа перестанет реагировать и войдет в ступор или взаимную блокировку из-за одновременного доступа к данным, или может быть сбой безопасности — программа создаст неверные данные.

Как решаются эти проблемы мы обсудим позже.

Таким образом, каждое приложение имеет хотя бы один поток — или несколько, если учитывать «системные» потоки, которые выполняют такие функции, как управление памятью и обработка событий.

Но с точки зрения программиста, вы начинаете с одного потока, называемого основным потоком.

Этот поток имеет возможность создавать дополнительные потоки.

Вопрос в том, как мы можем создать, запустить и выполнить поток?

В Java каждый поток представлен экземпляром класса Thread.

Создать поток, или экземпляр Thread, можно двумя способами.

Первый способ, это сначала создать объект Runnable.

Интерфейс Runnable определяет один метод run, предназначенный для того, чтобы содержать код, выполняемый в потоке.

После создания, объект Runnable передается конструктору класса Thread.

И поток запускается методом start.

Второй способ, это создать подкласс класса Thread.

Сам класс Thread реализует интерфейс Runnable, и при этом его метод run пустой.

Поэтому нужно создать подкласс класса Thread и предоставить собственную реализацию метода run.

Таким образом, первая ключевая операция — это создание потоков.

Но ключевой момент здесь — вам нужно указать вычисление, которое должно быть выполнено в потоке.

Затем после создания потока, он фактически не начинает выполнение.

Поэтому, следующее, что вам нужно сделать, это вызвать метод start.

Теперь, ваша основная программа сама по себе является потоком.

И у нас есть основной поток, который создает и запускает другой поток.

В другом потоке выполняется свой код.

Теперь основной поток после запуска другого потока может выполнить свой код.

В этом случае у нас параллельно выполняются два куска кода на двух разных ядрах.

Класс Thread содержит метод join.

Метод join может быть использован для того, чтобы приостановить выполнение текущего потока до тех пор, пока другой поток не закончит свое выполнение.

Как правило, мы используем более одного потока.

В этом случае, планировщик потоков планирует потоки, что не гарантирует порядок выполнения потоков.

В идеальном мире все потоки всех программ работают на отдельных процессорах.

Но в реальности, потоки должны разделяться между одним или несколькими процессорами.

Либо JVM, либо операционная система базовой платформы определяют, как распределять ресурс процессора среди потоков — задача, известная как планирование потоков.

Эта часть JVM или операционной системы, которая выполняет планирование потоков, является планировщиком потоков.

Java не заставляет виртуальную машину планировать потоки определенным образом, поэтому планирование потоков зависит от конкретной платформы.

Предположим, у нас есть два потока t1 и t2.

Несмотря на то, что мы запустили потоки последовательно, планировщик потоков не запускает и не завершает их в указанном порядке.

Каждый раз, когда вы запускаете этот код, вы можете получить разные результаты.

А если поток t1 должен использовать вычисления потока t2, что нам делать?

Решить эту проблему мы можем с помощью метода join ().

Этот код запустит второй поток t2, только после завершения первого потока t1, так как метод join приостанавливает выполнение главного потока до тех пор, пока не завершится поток t1.

Если поток прерывается, бросается исключение InterruptedException.

Теперь, предположим, что мы передали в метод run класса MyClass основной поток и применили к нему метод join.

Тогда первый поток будет ждать, когда завершится основной поток, а основной поток будет ждать, когда завершится первый поток.

Возникнет дедлок deadlock или взаимная блокировка потоков.

Для отладки долгоиграющих операций, например, сетевых запросов, часто используется статический метод sleep класса Thread.

Вызов этого метода ставит выполнение текущего потока на паузу, при этом нужно указать количество миллисекунд паузы.

Здесь также нужно обрабатывать исключение InterruptedException.

Это исключение, которое метод бросает, когда другой поток прерывает текущий поток, при работающем методе.

Похожие книги