Многопоточность в Java – руководство с примерами


Оглавление (нажмите, чтобы открыть):

Многопоточность Thread, Runnable

Многопоточное программирование позволяет разделить представление и обработку информации на несколько «легковесных» процессов (light-weight processes), имеющих общий доступ как к методам различных объектов приложения, так и к их полям. Многопоточность незаменима в тех случаях, когда графический интерфейс должен реагировать на действия пользователя при выполнении определенной обработки информации. Потоки могут взаимодействовать друг с другом через основной «родительский» поток, из которого они стартованы.

В качестве примера можно привести некоторый поток, отвечающий за представление информации в интерфейсе, который ожидает завершения работы другого потока, загружающего файл, и одновременно отображает некоторую анимацию или обновляет прогресс-бар. Кроме того этот поток может остановить загружающий файл поток при нажатии кнопки «Отмена».

Создатели Java предоставили две возможности создания потоков: реализация (implementing) интерфейса Runnable и расширение(extending) класса Thread. Расширение класса — это путь наследования методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса Thread. Данное ограничение внутри Java можно преодолеть реализацией интерфейса Runnable, который является наиболее распространённым способом создания потоков.

Преимущества потоков перед процессами

  • потоки намного легче процессов поскольку требуют меньше времени и ресурсов;
  • переключение контекста между потоками намного быстрее, чем между процессами;
  • намного проще добиться взаимодействия между потоками, чем между процессами.

Главный поток

Каждое java приложение имеет хотя бы один выполняющийся поток. Поток, с которого начинается выполнение программы, называется главным. После создания процесса, как правило, JVM начинает выполнение главного потока с метода main(). Затем, по мере необходимости, могут быть запущены дополнительные потоки. Многопоточность — это два и более потоков, выполняющихся одновременно в одной программе. Компьютер с одноядерным процессором может выполнять только один поток, разделяя процессорное время между различными процессами и потоками.

Класс Thread

В классе Thread определены семь перегруженных конструкторов, большое количество методов, предназначенных для работы с потоками, и три константы (приоритеты выполнения потока).

Конструкторы класса Thread

  • target – экземпляр класса реализующего интерфейс Runnable;
  • name – имя создаваемого потока;
  • group – группа к которой относится поток.

Пример создания потока, который входит в группу, реализует интерфейс Runnable и имеет свое уникальное название :

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

Несмотря на то, что главный поток создаётся автоматически, им можно управлять. Для этого необходимо создать объект класса Thread вызовом метода currentThread().

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

Наиболее часто используемые методы класса Thread для управления потоками :

  • long getId() — получение идентификатора потока;
  • String getName() — получение имени потока;
  • int getPriority() — получение приоритета потока;
  • State getState() — определение состояния потока;
  • void interrupt() — прерывание выполнения потока;
  • boolean isAlive() — проверка, выполняется ли поток;
  • boolean isDaemon() — проверка, является ли поток «daemon»;
  • void join() — ожидание завершения потока;
  • void join(millis) — ожидание millis милисекунд завершения потока;
  • void notify() — «пробуждение» отдельного потока, ожидающего «сигнала»;
  • void notifyAll() — «пробуждение» всех потоков, ожидающих «сигнала»;
  • void run() — запуск потока, если поток был создан с использованием интерфейса Runnable;
  • void setDaemon(bool) — определение «daemon» потока;
  • void setPriority(int) — определение приоритета потока;
  • void sleep(int) — приостановка потока на заданное время;
  • void start() — запуск потока.
  • void wait() — приостановка потока, пока другой поток не вызовет метод notify();
  • void wait(millis) — приостановка потока на millis милисекунд или пока другой поток не вызовет метод notify();

Жизненный цикл потока

При выполнении программы объект Thread может находиться в одном из четырех основных состояний: «новый», «работоспособный», «неработоспособный» и «пассивный». При создании потока он получает состояние «новый» (NEW) и не выполняется. Для перевода потока из состояния «новый» в «работоспособный» (RUNNABLE) следует выполнить метод start(), вызывающий метод run().

Поток может находиться в одном из состояний, соответствующих элементам статически вложенного перечисления Thread.State :

NEW — поток создан, но еще не запущен;
RUNNABLE — поток выполняется;
BLOCKED — поток блокирован;
WAITING — поток ждет окончания работы другого потока;
TIMED_WAITING — поток некоторое время ждет окончания другого потока;
TERMINATED — поток завершен.

Пример использования Thread

В примере ChickenEgg рассматривается параллельная работа двух потоков (главный поток и поток Egg), в которых идет спор, «что было раньше, яйцо или курица?». Каждый поток высказывает свое мнение после небольшой задержки, формируемой методом ChickenEgg.getTimeSleep(). Побеждает тот поток, который последним говорит свое слово.

При выполнении программы в консоль было выведено следующее сообщение.

Невозможно точно предсказать, какой поток закончит высказываться последним. При следующем запуске «победитель» может измениться. Это происходит вследствии так называемого «асинхронного выполнения кода». Асинхронность обеспечивает независимость выполнения потоков. Или, другими словами, параллельные потоки независимы друг от друга, за исключением случаев, когда бизнес-логика зависимости выполнения потоков определяется предусмотренными для этого средств языка.

Интерфейс Runnable

Интерфейс Runnable содержит только один метод run() :

Метод run() выполняется при запуске потока. После определения объекта Runnable он передается в один из конструкторов класса Thread.

Пример класса RunnableExample, реализующего интерфейс Runnable

При выполнении программы в консоль было выведено следующее сообщение.

Синхронизация потоков, synchronized

В процессе функционирования потоки часто используют общие ресурсы приложения, определенные вне потока. Если несколько потоков начнут одновременно вносить изменения в общий ресурс, то результаты выполнения программы могут быть непредсказуемыми. Рассмотрим следующий пример :

В примере определен общий ресурс в виде класса CommonObject, в котором имеется целочисленное поле counter. Данный ресурс используется внутренним классом, создающим поток CounterThread для увеличения в цикле значения counter на единицу. При старте потока полю counter присваивается значение 1. После завершения работы потока значение res.counter должно быть равно 4.

Две строчки кода класса CounterThread закомментированы. О них речь пойдет ниже.

В главном классе программы SynchronizedThread.main запускается пять потоков. То есть, каждый поток должен в цикле увеличить значение res.counter с единицы до четырех; и так пять раз. Но результат работы программы, отображаемый в консоли, будет иным :

То есть, с общим ресурсов res.counter работают все потоки одновременно, поочередно изменяя значение.

Чтобы избежать подобной ситуации, потоки необходимо синхронизировать. Одним из способов синхронизации потоков связан с использованием ключевого слова synchronized. Оператор synchronized позволяет определить блок кода или метод, который должен быть доступен только одному потоку. Можно использовать synchronized в своих классах определяя синхронизированные методы или блоки. Но нельзя использовать synchronized в переменных или атрибутах в определении класса.

Блокировка на уровне объекта

Блокировать общий ресурс можно на уровне объекта, но нельзя использовать для этих целей примитивные типы. В примере следует удалить строчные комментарии в классе CounterThread, после чего общий ресурс будет блокироваться как только его захватит один из потоков; остальные потоки будут ждать в очереди освобождения ресурса. Результат работы программы при синхронизации доступа к общему ресурсу резко изменится :

Следующий код демонстрирует порядок использования оператора synchronized для блокирования доступа к объекту.

Блокировка на уровне метода и класса

Блокировать доступ к ресурсам можно на уровне метода и класса. Следующий код показывает, что если во время выполнения программы имеется несколько экземпляров класса DemoClass, то только один поток может выполнить метод demoMethod(), для других потоков доступ к методу будет заблокирован. Это необходимо когда требуется сделать определенные ресурсы потокобезопасными.

Каждый объект в Java имеет ассоциированный с ним монитор, который представляет своего рода инструмент для управления доступа к объекту. Когда выполнение кода доходит до оператора synchronized, монитор объекта блокируется, предоставляя монопольный доступ к блоку кода только одному потоку, который произвел блокировку. После окончания работы блока кода, монитор объекта освобождается и он становится доступным для других потоков.

Некоторые важные замечания использования synchronized

  1. Синхронизация в Java гарантирует, что два потока не могут выполнить синхронизированный метод одновременно.
  2. Оператор synchronized можно использовать только с методами и блоками кода, которые могут быть как статическими, так и не статическими.
  3. Если один из потоков начинает выполнять синхронизированный метод или блок, то этот метод/блок блокируются. Когда поток выходит из синхронизированного метода или блока JVM снимает блокировку. Блокировка снимается, даже если поток покидает синхронизированный метод после завершения из-за каких-либо ошибок или исключений.
  4. Синхронизация в Java вызывает исключение NullPointerException, если объект, используемый в синхронизированном блоке, не определен, т.е. равен null.
  5. Синхронизированные методы в Java вносят дополнительные затраты на производительность приложения. Поэтому следует использовать синхронизацию, когда она абсолютно необходима.
  6. В соответствии со спецификацией языка нельзя использовать synchronized в конструкторе, т.к. приведет к ошибке компиляции.

Примечание : для синхронизации потоков можно использовать объекты синхронизации Synchroniser’s пакета java.util.concurrent.

Взаимная блокировка

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

Основные условия возникновения взаимоблокировок в многопотоковом приложении :

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

Взаимодействие между потоками в Java, wait и notify

При взаимодействии потоков часто возникает необходимость приостановки одних потоков и их последующего извещения о завершении определенных действий в других потоков. Так например, действия первого потока зависят от результата действий второго потока, и надо каким-то образом известить первый поток, что второй поток произвел/завершил определенную работу. Для подобных ситуаций используются методы :

  • wait() — освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод notify();
  • notify() — продолжает работу потока, у которого ранее был вызван метод wait();
  • notifyAll() — возобновляет работу всех потоков, у которых ранее был вызван метод wait().

Все эти методы вызываются только из синхронизированного контекста (синхронизированного блока или метода).

Рассмотрим пример «Производитель-Склад-Потребитель» (Producer-Store-Consumer). Пока производитель не поставит на склад продукт, потребитель не может его забрать. Допустим производитель должен поставить 5 единиц определенного товара. Соответственно потребитель должен весь товар получить. Но, при этом, одновременно на складе может находиться не более 3 единиц товара. При реализации данного примера используем методы wait() и notify().

Листинг класса Store

Класс Store содержит два синхронизированных метода для получения товара get() и для добавления товара put(). При получении товара выполняется проверка счетчика counter. Если на складе товара нет, то есть counter

Библиотека примеров приложений Java

Оглавление
Простейший пример
Создание двух потоков
Управление потоками
Спрайтовая анимация
Панели с двигающимся текстом
Бегущая строка с мерцанием
Устранение мерцания
Поток для записи в файл
Контроль за выводом в файл
Чтение с сервера Web

6.1. Простейший пример

Пример демонстрирует использование многопоточности для динамического изменения цвета текстовой строки.

Демонстрация
(ваш браузер должен уметь работать с аплетами Java JDK 1.1)

Немного теории

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

Почему так происходит?

Многопоточность просто необходима для выполнения в приложениях Java каких-либо периодических действий, связанных например, с анимацией. Работа с файлами или передача данных через сеть также обычно реализуется с применением многопоточности. Это позволяет исключить блокирование работы приложения на время выполнения операций ввода или вывода. Можно найти и другую полезную работу для многопоточности.

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

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

Например, для запуска потока на выполнение предусмотрен метод start, а для его остановки — метод stop. Метод sleep, имеющийся в классе Thread, позволяет задержать работу потока на заданный период времени. Полное описание этих методов вы найдете в документации JDK.

Описание примера

Наш первый пример многопоточного аплета очень прост. В своем окне этот аплет отображает текстовую строку, цвет которой периодически изменяется случайным образом (рис. 1).

Рис. 1. Строка периодически изменяет свой цвет

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

Как выглядит исходный текст аплета и как он работает?

Главный класс аплета

Главный класс аплета реализует интерфейс Runnable:

Помимо методов, обычных для однопоточных аплетов, в этом классе мы определили метод run, работающий как отдельный поток.

В главном классе нашего аплета предусмотрены следующие поля:

В поле thr класса Thread хранится ссылка на поток, в рамках которого выполняется метод run. Первоначально это поле инициализируется значением null.

Поля rColor, gColor, bColor хранят текущие значения компонент цвета текстовой строки, отображаемой в окне аплета.

Метод start

Метод start вызывается при активизации аплета. Его задачей является создание дополнительного потока и запуск этого потока на выполнение:

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

После вызова метода start наряду с главным потоком, в рамках которого выполняется основной код аплета, запускается дополнительный поток. Его код находится в определении метода run.

Метод stop

Если пользователь покидает страницу HTML с аплетом, происходит вызов метода stop.

Обычно в многопоточных аплетах этот метод останавливает работу всех запущенных потоков. Наш метод поступает таким же образом:

После остановки потока он записывает в поле thr значение null. Если аплет будет снова активизирован, поток снова запустится методом start, определенным в главном классе аплета.

Метод run

Как мы уже говорили, метод run работает как отдельный поток. Основной код аплета никогда напрямую не должен вызывать этот метод. Для запуска потока на выполнение в классе Thread предусмотрен метод start.

Наш метод run представляет собой бесконечный цикл:

Если этот цикл завершится, поток также прекратит свою работу.

Какие действия выполняются внутри цикла?

Прежде всего мы получаем три случайные компоненты цвета:

На следующем этапе метод run вызывает метод repaint:

В результате будет вызван метод paint, исходный текст которого мы приведем ниже. Этот метод нарисует текстовую строку новым случайным цветом.

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

Для этого мы вызываем метод sleep, предусмотренный в классе Thread. Регулируя задержку, вы можете влиять на скорость перерисовки текстовой строки.

При работе метода sleep возможно возникновение исключения InterruptedException (если задержка будет по каким-либо причинам прервана). В этом аварийном случае мы останавливаем работу потока.

Метод paint

Так как метод run, запущенный в отдельном потоке, вызывает каждую секунду метод repaint, с этой же периодичностью будет вызываться метод paint.

Получив управление, метод paint устанавливает в контексте отображения цвет, компоненты которого хранятся в полях rColor, gColor и bColor:

Напомним, что метод run периодически изменяет значения компонент цвета случайным образом.

Далее метод paint формирует текстовую строку, устанавливает шрифт Courier и затем рисует эту строку в окне аплета:

Заметим, что достигнутый в нашем аплете эффект не может быть получен без применения многопоточности. В частности, ни в коем случае нельзя организовывать бесконечный цикл внутри метода paint. Такая попытка приведет лишь к «зависанию» аплета.

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

Программирование › Учебник по Java › Многопоточное программирование на Java

В этой теме 0 ответов, 1 участник, последнее обновление Васильев Владимир Сергеевич 1 год назад.

— самодостаточная исполняемая среда. Другими
словами процессы работают независимо друг от друга. Более того работают
изолировано — нет прямого доступа к данным одного процесса из другого.
Ресурсы, пямять и процессорное время, процессу выделяются операционной
системой. Большинство приложений использует только один процесс.

Породить новый процесс можно с помощью ProcessBuilder.

— независимая часть процесса выполняющую какую-либо
задачу. Каждый процесс имеет по крайне мере один поток — главный или
основной поток. Поэтому создание нового потока требует намного меньше
ресурсов, чем создание нового процесса. Потоки одного процесса совместно
используют глобальные данные и адресное пространство. В java общие
данные кешируются потоками в собственной памяти. Поток в java
описывается классом Thread.

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

— действие, которое гарантировано будет
выполнено до конца. Например, операции чтения/записи ссылочных
переменных атомарны. А операция инкремента нет, т.е. она может быть
приостановлена переключением на другой поток. В пятой версии java
добавлен ряд атомарных классов, например AtomicInteger с атомарной
операцией инкремента.

— возможность предоставить потоку
эксклюзивный доступ к объекту. До 5 версии синхронизация обеспечивалась
только ключевым словом synchronized.

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

Ключевое слово volatile для переменных гарантирует атомарность
операций чтения/записи, включая для типов long и double.

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

С java 5 добавлен более высокий уровень по управлению потоками (пакет
java.util.concurrent), который будет полезен для приложений с большой
конкурентностью потоков, что актуально для современных многопроцессорных
и многоядерных систем:

  • блокировка — интерфейс Lock и соответствующие классы;
  • конкурентные и атомарные типы данных;
  • управление пулом потоков;

Среди плюсов многопоточности:

  • более эффективное использование ресурсов;
  • в некоторых случаях более простая структура программы. Например,
    использовать шаблон разработки производитель-потребитель и реализовать
    его в разных потоках;
  • приложение более живое, пользователю не нужно каждый раз ожидать
    завершения какой-то задачи.

Необходимо соблюдать баланс по колличеству потоков. Так как каждый
поток, кроме процессорного времени, запрашивает дополнительную память
для своих нужд. Во-вторых само переключение между потоками ресурсоемко.

Мастер Йода рекомендует:  Что не надо делать на собеседовании — отвечают эксперты

Присоединение к потоку

Присоединение означает, что один поток дожидается завершения одного и
более других потоков, и только после этого продолжает свою работу.

Ниже приведен пример двух тестов создающих 10 потоков и ожидающих их
завершения. И хотя они с виду похожи, первый «ошибочный», т.е.
работает не так, как обычно ожидает разработчик.

Синхронизация потоков


— возможность предоставить потоку
экслюзивный доступ к объекту. Объект синхронизации обычно называется
монитором. Система гарантирует что никакой поток не сможет выполнить
синхронный метод/блок кода с тем же монитором пока он используется
другим потоком. Другими словами, если выполнение синхронного кода
приостановлено для выполнения других потоков, то монитор все равно
считается занятым. И освободится он, когда захвативший его поток выйдет
из блока синхронизации.

Цена — снижение скорости, так как синхронизируемый участок
обертывается дополнительным кодом + отслеживание объекта синхронизации.

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

Монитором синхронного метода является объект метода. Монитором
статического синхронного метода является объект Class связанный с
классом, в котором метод определен.

В большинстве случаев блоки синхронизации предпочтительней синхронных
методов по скорости, так как ими можно ограничить только критичный
участок кода (где происходит чтение/запись объекта совместного
пользования).

В качестве монитора не может быть null объект, в этом случае
возникнет исключение.

В качестве монитора не желательно использовать не завершенные члены
класса, так как объект ссылки может измениться. Некоторые ide выдают
предупреждение по этому поводу. Также не желательно использовать строки,
так как они могут быть сохранены в пуле строк и неизвестно кто захочет
ими пользоваться. Вместо этого рекомендуется создать специальный объект
для мониторинга.

private static final Object MY_LOCK = new Object();

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

Неправильная реализация синхронизации может привести к тупиковой
ситуации (deadlock) и не согласованности памяти.

Взаимодействие потоков

Синхронное взаимодействие между потоками осуществляется через объект
монитора методами корневого класса Object:

  • wait() — переводит текущий поток в состояние ожидания пока
    монитор не вызовет метод notify()/notifyAll(). Так как во время
    ожидания поток не владеет монитором, объект становится доступным другим
    потокам;
  • wait(long timeout) — аналогично предыдущему, но с
    ограничением времени ожидания;
  • notify() — уведомляет первый поток ожидающий монитор, в
    результате поток возвращается в активное состояние и снова становится
    владельцем монитора;
  • notifyAll() — аналогично предыдущему, но уведомляются все
    потоки ожидающие данный объект;

Шаблон разработки «Производитель-Потребитель» отлично
демонстрирует возможности этих методов. Пусть один поток-производитель
порождает данные. Второй поток эти данные потребляет. Но как ему
отслеживать появление новых данных?

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

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

Третий способ — синхронизировать задачи методами wait/notify.

В java 5 добавлены готовые решения подобных задач в виде конкурентных
коллекций. Так в предыдущем примере можно заменить класс TaskData на
LinkedBlockingDeque или на подобную коллекцию. При этом нужно
использовать синхронные методы, т.е. вместо привычного poll метод poll(timeout,
timeunit) или take().

volatile переменные

Ключевое слово volatile применяется только к переменным и имеет
следующие эффекты в многопоточном программировании:

  • 1. переменная всегда считывается из основной памяти, и никогда не
    кэшируется в память потока, а значит всегда доступна любому потоку;
  • 2. при запросах на чтение и запись от нескольких потоков, системой
    гарантируется выполнение вначале запросов на запись;
  • 3. гарантируется атомарность операций чтения/записи, правда это
    актуально для переменных только типа long и double, для остальных типов
    эти действия и так атомарны. Для всех прочих операций, как ++,
    синхронизация делается внешним образом, либо используются атомарные
    типы как AtomicInteger из пакета java.util.concurrent.atomic;
  • 4. в результе предыдущих пунктов, потоки не блокируются в ожидании
    освобождения монитора;

Объектная переменная может равняться null.

Ниже приведен пример синглтона с отложенной инициализацией.
Предположим один поток создает экземпляр одиночки. Может возникнуть
ситуация, когда instance уже ссылается на созданный объект, но поток не
успел выйти из блока синхронизации, так как система передала управление
другим потокам. В этом случае без volatile для всех прочих потоков
instance все еще равно null. И если какие-то потоки тоже нуждаются в нем
они будут простаивать.

Другой пример, когда один поток должен завершиться по значению
переменной отслеживаемой в другом потоке. Возьмем игру с двумя потоками,
один gui поток, другой игровой цикл. Предположим при нажатии кнопки
некая переменная btExit устнавливается в true. Но без volatile игровой
поток может пропустить это изменение.

В следующем коде без volatile и без System.out.print(«») в
игровом потоке, данные можно вводить до бесконечности (проверял на Mac).
С volatile или с System.out.print(«») в игровом потоке,
приложение работает как задумано: после ентера завершаются оба потока.

Конкурентные коллекции

Конкурентные коллекции обладают встроенной поддержкой блокировки и ожидания потоков,
что позволяет не использовать напрямую wait/notify. Как и прочие
потокобезопасные коллекции, они не могут содержать значение null.

Ниже приведен пример из офф. док. реализации процесса
«производитель-потребитель». Реализация завершения потоков
зависит от конкретной ситуации. Одно из распространенных решений —
добавление производителем в очередь особого элемента, который
потребителем четко будет распознаваться как конец добавления.

очередь

BlockingQueue — расширенный интерфейс обычной очереди,
добавленные методы:

  • add(E e) — добавить элемент в очередь. Если места в очереди
    нет, то вызывается исключение IllegalStateException;
  • remove(Object o) — удалить объект из очереди;
  • contains(Object o) — определить, есть ли в очереди указанный
    объект;
  • drainTo(Collection c) — перенести все
    элементы очереди в указанную коллекцию;
  • drainTo(Collection c, int maxElements) —
    перенести максимальное число элементов из очереди в указанную
    коллекцию;
  • offer(E e) — добавить элемент в очередь. Если места нет, то
    возвращается значение false;
  • offer(E e, long timeout, TimeUnit unit) — добавить элемент в
    очередь, если необходимо ждать свободное место указанное время;
  • poll(long timeout, TimeUnit unit) — взять элемент из
    очереди, или ждать указанное время появления доступного элемента;
  • put(E e) — добавить элемент в очередь, если необходимо ждать
    свободного места;
  • remainingCapacity() — сколько элементов можно добавить в
    очередь без блокировки (Integer.MAX_VALUE означает сколько угодно);
  • take() — взять элемент из очереди, если необходимо ждать
    появления элемента в очереди;
  • ArrayBlockingQueue — реализация классического ограниченного
    буфера, т.е. максимальное число элементов фиксировано;
  • DelayQueue — бесконечная очередь для элементов с интерфейсом
    Delayed;
  • LinkedBlockingDeque — двунаправленный связной список, по
    желанию можно ограничить максимальное число элементов (java 7+);
  • LinkedBlockingQueue — двунаправленный связной список, по
    желанию можно ограничить максимальное число элементов. Первым элементом
    считается элементо дольше всех находящийся в очереди, соответственно
    последний элемент меньше всех;
  • LinkedTransferQueue — асинхронная очередь. Методы size,
    addAll, removeAll, retainAll, containsAll, equals и toArray не
    атомарны. Например, вы можете получить значение size(), которое будет
    уже не актуально;

BlockingDeque — расширение обычной двунаправленной очереди и
интерфейса BlockingQueue. В дополенение к BlockingQueue есть уточненные
методы для работы с началом очереди и с концом. Например, к методу
poll(long timeout, TimeUnit unit) добавлены методы pollFirst и pollLast.

Проблемные ситуации при работе с потоками

Мертвая блокировка

или deadlock — ситуация взаимного
ожидания потоков. Возникает когда одновременно внутри своих блоков
синхронизации два потока обращаются к мониторам друг друга.

В следующем коде проблема заключается в вызове bower.bowBack(this) в
методе bow. Для каждого потока объект bower недоступен, а следовательно
завершить метод bow не удается (помним, что монитором синхронного метода
является объект метода). Таким образом, приложение находится в тупиковой
ситуации.

активная блокировка

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

голодание

Голодание или starvation — ситуация захвата одним ‘жадным’
потоком совместных данных. В результате другие потоки не могут нормально
работать. Одно из решений усыплять поток вне блока синхронизации.

согласованность памяти

или memory consistency
errors — ситауция, когда потоки видят разные значения в совместных
данных. Возникает из-за кэширования данных потоком при входе в блок
синхронизации. volatile переменные и конкурентные коллекции помогают
избежать эту проблему.

Пул потоков

Для эффективного управления множеством потоков в джаве реализован
механизм пула потоков. Таким образом, есть гарантия не потерять поток и
сбалансировать приложение по колличеству потоков и частоте их создания.

  • Executor — упрощенный интерфейс пула, содержит один метод
    для передачи задачи на выполнение;
  • ExecutorService — расширенный интерфейс пула, с возможностью
    завершения всех потоков;
  • Executors — фабрика объектов связанных с пулом потоков, в
    том числе позволяет создать основные типы пулов;
  • AbstractExecutorService — базовый класс пула, реализующий
    интерфейс ExecutorService;
  • ThreadPoolExecutor — пул потоков с гибкой настройкой, может
    служить базовым классом для нестандартных пулов;
  • ForkJoinPool — пул для выполнения задач типа ForkJoinTask;

Методы Executors для создания пулов:

  • newCachedThreadPool() — если есть свободный поток, то задача
    выполняется в нем, иначе добавляется новый поток в пул. Потоки не
    используемые больше минуты завершаются и удалются и кэша. Размер пула
    неограничен. Предназначен для выполнения множество небольших
    асинхронных задач;
  • newCachedThreadPool(ThreadFactory threadFactory) —
    аналогично предыдущему, но с собственной фабрикой потоков;
  • newFixedThreadPool(int nThreads) — создает пул на указанное
    число потоков. Если новые задачи добавлены, когда все потоки активны то
    они будут сохранены в очереди для выполнения позже. Если один из потоко
    завершился из-за ошибки, на его место будет запущен другой поток.
    Потоки живут до тех пор, пока пул не будет закрыт явно методом
    shutdown.
  • newFixedThreadPool(int nThreads, ThreadFactory
    threadFactory) — аналогично предыдущему, но с собственной фабрикой
    потоков;
  • newSingleThreadScheduledExecutor() — однопотоковый пул с
    возможностью выполнять задачу через указанное время или выполнять
    периодически. Если поток был завершен из-за каких-либо ошибок, то для
    выполнения следующей задачи будет создан новый поток.
  • newSingleThreadScheduledExecutor(ThreadFactory
    threadFactory) — аналогично предыдущему, но с собственной фабрикой
    потоков;
  • newScheduledThreadPool(int corePoolSize) — пул для
    выполнения задач через указанное время или переодически;
  • newScheduledThreadPool(int corePoolSize, ThreadFactory
    threadFactory) — аналогично предыдущему, но с собственной фабрикой
    потоков;
  • unconfigurableExecutorService(ExecutorService executor) —
    обертка на пул, запрещающая изменять его конфигурацию;

Функциональная задача Java

Начиная с api 5+, в джаву добавлена реализация функциональной задачи:

  • Callable — интерфейс функциональной задачи, чтобы ее
    выполнить в отдельном потоке необходимо иметь пул потоков. Всего один
    метод — call;
  • Future — базовый интерфейс результата задачи;
  • RunnableFuture — результат задачи для Runnable (api
    6+);
  • Executors — вспомогательный класс с разными утилитами;

Методы интерфейса Future:

  • cancel(boolean mayInterruptIfRunning) — попытка остановить
    выполняемую задачу. Если она уже завершена, отменена или не может быть
    отменена возвращается false. При успешном результате, последующие
    вызовы методов isCancelled и isDone всегда возвращают true;
  • get() — получить результат задачи, при необходимости ожидать
    результат;
  • get(long timeout, TimeUnit unit) — получить результат
    задачи, при необходимости ожидать результат указанное время;
  • isCancelled() — true, если задача была отменена;
  • isDone() — true, если задача завершена;

Трубопровод данных (Pipeline) в Java

В джава реализовано построение трубопровода данных из одного потока в
другой на основе потоков ввода/вывода:

  • PipedInputStream/PipedOuputStream — потоки ввода/вывода для
    передачи байтовых данных;
  • PipedReader/PipedWriter — потоки ввода/вывода для передачи
    текстовой информации;

Эти классы используются всегда в паре, т.е. либо поток ввода должен
ссылаться на поток вывода, либо наоборот. Запись и чтение данных
производится в разных потоках. Использование трубы в одном потоке может
привести к мертвой блокировке.

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

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

Внутри процесса: многопоточность и пинг-понг mutex’ом

Какая тема вызывает больше всего вопросов и затруднений у начинающих? Когда я спросила об этом преподавателя и Java-программиста Александра Пряхина, он сразу ответил: «Многопоточность». Спасибо ему за идею и помощь в подготовке этой статьи!

Мы заглянем во внутренний мир приложения и его процессов, разберёмся, в чём суть многопоточности, когда она полезна и как её реализовать — на примере Java. Если учите другой язык ООП, не огорчайтесь: базовые принципы одни и те же.

О потоках и их истоках

Чтобы понять многопоточность, сначала вникнем, что такое процесс. Процесс – это часть виртуальной памяти и ресурсов, которую ОС выделяет для выполнения программы. Если открыть несколько экземпляров одного приложения, под каждый система выделит по процессу. В современных браузерах за каждую вкладку может отвечать отдельный процесс.

Вы наверняка сталкивались с «Диспетчером задач» Windows (в Linux это — «Системный монитор») и знаете, что лишние запущенные процессы грузят систему, а самые «тяжёлые» из них часто зависают, так что их приходится завершать принудительно.

Но пользователи любят многозадачность: хлебом не корми — дай открыть с десяток окон и попрыгать туда-сюда. Налицо дилемма: нужно обеспечить одновременную работу приложений и при этом снизить нагрузку на систему, чтобы она не тормозила. Допустим, «железу» не угнаться за потребностями владельцев — нужно решать вопрос на программном уровне.

Мы хотим, чтобы в единицу времени процессор успевал выполнить больше команд и обработать больше данных. То есть нам надо уместить в каждом кванте времени больше выполненного кода. Представьте единицу выполнения кода в виде объекта — это и есть поток.

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

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

Разница между потоками и процессами

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

Процессы работают каждый со своими данными — обмениваться чем-то они могут только через механизм межпроцессного взаимодействия. Потоки обращаются к данным и ресурсам друг друга напрямую: что изменил один — сразу доступно всем. Поток может контролировать «собратьев» по процессу, в то время как процесс контролирует исключительно своих «дочек». Поэтому переключаться между потоками быстрее и коммуникация между ними организована проще.

Какой отсюда вывод? Если вам нужно как можно быстрее обработать большой объём данных, разбейте его на куски, которые можно обрабатывать отдельными потоками, а затем соберите результат воедино. Это лучше, чем плодить жадные до ресурсов процессы.

Но почему такое популярное приложение как Firefox идёт по пути создания нескольких процессов? Потому что именно для браузера изолированная работа вкладок — это надёжно и гибко. Если с одним процессом что-то не так, не обязательно завершать программу целиком — есть возможность сохранить хотя бы часть данных.

Что такое многопоточность

Вот мы и подошли к главному. Многопоточность — это когда процесс приложения разбит на потоки, которые параллельно — в одну единицу времени — обрабатываются процессором.

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

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

Занятые люди жалуются, что у них всего две руки. Процессы и программы могут иметь столько рук, сколько нужно для скорейшего выполнения задачи.

Жди сигнала: синхронизация в многопоточных приложениях

Представьте, что несколько потоков пытаются одновременно изменить одну и ту же область данных. Чьи изменения будут в итоге приняты, а чьи — отменены? Чтобы работа с общими ресурсами не приводила к путанице, потокам нужно координировать свои действия. Для этого они обмениваются информацией с помощью сигналов. Каждый поток сообщает другим, что он сейчас делает и каких изменений ждать. Так данные всех потоков о текущем состоянии ресурсов синхронизируются.

В категориях объектно-ориентированного программирования сигналы — это объекты синхронизации. У каждого из них — своя роль во взаимодействии.

Основные средства синхронизации

Взаимоисключение (mutual exclusion, сокращённо — mutex) — «флажок», переходящий к потоку, который в данный момент имеет право работать с общими ресурсами. Исключает доступ остальных потоков к занятому участку памяти. Мьютексов в приложении может быть несколько, и они могут разделяться между процессами. Есть подвох: mutex заставляет приложение каждый раз обращаться к ядру операционной системы, что накладно.

Семафор — позволяет вам ограничить число потоков, имеющих доступ к ресурсу в конкретный момент. Так вы снизите нагрузку на процессор при выполнении кода, где есть узкие места. Проблема в том, что оптимальное число потоков зависит от машины пользователя.

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

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

Как реализовать многопоточность в Java

За работу с потоками в Java отвечает класс Thread. Создать новый поток для выполнения задачи — значит создать экземпляр класса Thread и связать его с нужным кодом. Сделать это можно двумя путями:

образовать от Thread подкласс;

имплементировать в своём классе интерфейс Runnable, после чего передавать экземпляры класса в конструктор Thread.

Пока мы не будем затрагивать тему тупиковых ситуаций (deadlock’ов), когда потоки блокируют работу друг друга и зависают — оставим это для следующей статьи. А сейчас перейдём к практике.

Пример многопоточности в Java: пинг-понг мьютексами

Если вы думаете, что сейчас будет что-то страшное — выдохните. Работу с объектами синхронизации мы рассмотрим почти в игровой форме: два потока будут перебрасываться mutex’ом. Но по сути вы увидите реальное приложение, где в один момент времени только один поток может обрабатывать общедоступные данные.

Сначала создадим класс, наследующий свойства уже известного нам Thread, и напишем метод «удара по мячу» (kickBall):

Теперь позаботимся о мячике. Будет он у нас не простой, а памятливый: чтоб мог рассказать, кто по нему ударил, с какой стороны и сколько раз. Для этого используем mutex: он будет собирать информацию о работе каждого из потоков — это позволит изолированным потокам общаться друг с другом. После 15-го удара выведем мяч из игры, чтоб его сильно не травмировать.

А теперь на сцену выходят два потока-игрока. Назовём их, не мудрствуя лукаво, Пинг и Понг:

Мастер Йода рекомендует:  Функции базы данных MySQL

«Полный стадион народа — время начинать матч». Объявим об открытии встречи официально — в главном классе приложения:

Как видите, ничего зубодробительного здесь нет. Это пока только введение в многопоточность, но вы уже представляете, как это работает, и можете экспериментировать — ограничивать длительность игры не числом ударов, а по времени, например. Мы ещё вернёмся к теме многопоточности — рассмотрим пакет java.util.concurrent, библиотеку Akka и механизм volatile. А еще поговорим о реализации многопоточности на Python.

Какая тема вызывает больше всего вопросов и затруднений у начинающих? Когда я спросила об этом преподавателя и Java-программиста Александра Пряхина, он сразу ответил: «Многопоточность». Спасибо ему за идею и помощь в подготовке этой статьи!

Мы заглянем во внутренний мир приложения и его процессов, разберёмся, в чём суть многопоточности, когда она полезна и как её реализовать — на примере Java. Если учите другой язык ООП, не огорчайтесь: базовые принципы одни и те же.

О потоках и их истоках

Чтобы понять многопоточность, сначала вникнем, что такое процесс. Процесс – это часть виртуальной памяти и ресурсов, которую ОС выделяет для выполнения программы. Если открыть несколько экземпляров одного приложения, под каждый система выделит по процессу. В современных браузерах за каждую вкладку может отвечать отдельный процесс.

Вы наверняка сталкивались с «Диспетчером задач» Windows (в Linux это — «Системный монитор») и знаете, что лишние запущенные процессы грузят систему, а самые «тяжёлые» из них часто зависают, так что их приходится завершать принудительно.

Но пользователи любят многозадачность: хлебом не корми — дай открыть с десяток окон и попрыгать туда-сюда. Налицо дилемма: нужно обеспечить одновременную работу приложений и при этом снизить нагрузку на систему, чтобы она не тормозила. Допустим, «железу» не угнаться за потребностями владельцев — нужно решать вопрос на программном уровне.

Мы хотим, чтобы в единицу времени процессор успевал выполнить больше команд и обработать больше данных. То есть нам надо уместить в каждом кванте времени больше выполненного кода. Представьте единицу выполнения кода в виде объекта — это и есть поток.

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

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

Разница между потоками и процессами

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

Процессы работают каждый со своими данными — обмениваться чем-то они могут только через механизм межпроцессного взаимодействия. Потоки обращаются к данным и ресурсам друг друга напрямую: что изменил один — сразу доступно всем. Поток может контролировать «собратьев» по процессу, в то время как процесс контролирует исключительно своих «дочек». Поэтому переключаться между потоками быстрее и коммуникация между ними организована проще.

Какой отсюда вывод? Если вам нужно как можно быстрее обработать большой объём данных, разбейте его на куски, которые можно обрабатывать отдельными потоками, а затем соберите результат воедино. Это лучше, чем плодить жадные до ресурсов процессы.

Но почему такое популярное приложение как Firefox идёт по пути создания нескольких процессов? Потому что именно для браузера изолированная работа вкладок — это надёжно и гибко. Если с одним процессом что-то не так, не обязательно завершать программу целиком — есть возможность сохранить хотя бы часть данных.

Что такое многопоточность

Вот мы и подошли к главному. Многопоточность — это когда процесс приложения разбит на потоки, которые параллельно — в одну единицу времени — обрабатываются процессором.

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

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

Занятые люди жалуются, что у них всего две руки. Процессы и программы могут иметь столько рук, сколько нужно для скорейшего выполнения задачи.

Жди сигнала: синхронизация в многопоточных приложениях

Представьте, что несколько потоков пытаются одновременно изменить одну и ту же область данных. Чьи изменения будут в итоге приняты, а чьи — отменены? Чтобы работа с общими ресурсами не приводила к путанице, потокам нужно координировать свои действия. Для этого они обмениваются информацией с помощью сигналов. Каждый поток сообщает другим, что он сейчас делает и каких изменений ждать. Так данные всех потоков о текущем состоянии ресурсов синхронизируются.

В категориях объектно-ориентированного программирования сигналы — это объекты синхронизации. У каждого из них — своя роль во взаимодействии.

Основные средства синхронизации

Взаимоисключение (mutual exclusion, сокращённо — mutex) — «флажок», переходящий к потоку, который в данный момент имеет право работать с общими ресурсами. Исключает доступ остальных потоков к занятому участку памяти. Мьютексов в приложении может быть несколько, и они могут разделяться между процессами. Есть подвох: mutex заставляет приложение каждый раз обращаться к ядру операционной системы, что накладно.

Семафор — позволяет вам ограничить число потоков, имеющих доступ к ресурсу в конкретный момент. Так вы снизите нагрузку на процессор при выполнении кода, где есть узкие места. Проблема в том, что оптимальное число потоков зависит от машины пользователя.

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

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

Как реализовать многопоточность в Java

За работу с потоками в Java отвечает класс Thread. Создать новый поток для выполнения задачи — значит создать экземпляр класса Thread и связать его с нужным кодом. Сделать это можно двумя путями:

образовать от Thread подкласс;

имплементировать в своём классе интерфейс Runnable, после чего передавать экземпляры класса в конструктор Thread.

Пока мы не будем затрагивать тему тупиковых ситуаций (deadlock’ов), когда потоки блокируют работу друг друга и зависают — оставим это для следующей статьи. А сейчас перейдём к практике.

Пример многопоточности в Java: пинг-понг мьютексами

Если вы думаете, что сейчас будет что-то страшное — выдохните. Работу с объектами синхронизации мы рассмотрим почти в игровой форме: два потока будут перебрасываться mutex’ом. Но по сути вы увидите реальное приложение, где в один момент времени только один поток может обрабатывать общедоступные данные.

Сначала создадим класс, наследующий свойства уже известного нам Thread, и напишем метод «удара по мячу» (kickBall):


Теперь позаботимся о мячике. Будет он у нас не простой, а памятливый: чтоб мог рассказать, кто по нему ударил, с какой стороны и сколько раз. Для этого используем mutex: он будет собирать информацию о работе каждого из потоков — это позволит изолированным потокам общаться друг с другом. После 15-го удара выведем мяч из игры, чтоб его сильно не травмировать.

А теперь на сцену выходят два потока-игрока. Назовём их, не мудрствуя лукаво, Пинг и Понг:

«Полный стадион народа — время начинать матч». Объявим об открытии встречи официально — в главном классе приложения:

JLesson 35. Многопоточность в Java. Часть 1.

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

Java, в отличие от многих других языков программирования, изначально разрабатывался с поддержкой многопоточности. Многопоточность в Java существует на уровне самого языка. Хотя и на В цикле статей, посвященных данной теме, мы разберем основу создания приложений с поддержкой параллельных вычислений, модель памяти Java (JMM) и пакет java.util.concurrency, являющийся дополнительным инструментом для использования в многопоточных приложениях.

Процессы и потоки.

Любая операционная система неразрывно связана с понятием процесса, логической абстракции, представляющей собой выполняющуюся программу. Каждый процесс в операционной системе имеет в своем распоряжении свое адресное пространство памяти, в котором хранятся его код и данные. Но иногда может быть полезным разделить выполняющуюся программу на ряд отдельных подпроцессов. Здесь то и появляется понятия потока.

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

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

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

В конце концов сейчас мы живем в мире, когда существуют большие данные (Big Data), которые нужно не просто хранить, но их нужно и обрабатывать. Конечно, мощности современных устройств уже позволяют выполнять некоторую обработку даже на компьютерах пользователей, но создание параллельных вычислений может ускорить работу, распределив нагрузку между несколькими процессорами. А имея в своем распоряжении многоядерный процессор, почему бы не воспользоваться таким преимуществом?

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

Главный поток.

При запуске вашей программы на языке Java стартует главный поток приложения. Ссылку на него можно получить, вызывав статический метод currentThread() класса Thread.

Собеседование по Java — многопоточность (вопросы и ответы)

Вопросы и ответы для собеседования Java по теме — многопоточность.

К списку вопросов по всем темам

Вопросы

1. Дайте определение понятию “процесс”.
2. Дайте определение понятию “поток”.
3. Дайте определение понятию “синхронизация потоков”.
4. Как взаимодействуют программы, процессы и потоки?
5. В каких случаях целесообразно создавать несколько потоков?
6. Что может произойти если два потока будут выполнять один и тот же код в программе?
7. Что вы знаете о главном потоке программы?
8. Какие есть способы создания и запуска потоков?
9. Какой метод запускает поток на выполнение?
10. Какой метод описывает действие потока во время выполнения?
11. Когда поток завершает свое выполнение?
12. Как синхронизировать метод?
13. Как принудительно остановить поток?
14. Дайте определение понятию “поток-демон”.
15. Как создать поток-демон?
16. Как получить текущий поток?
17. Дайте определение понятию “монитор”.
18. Как приостановить выполнение потока?
19. В каких состояниях может пребывать поток?
20. Что является монитором при вызове нестатического и статического метода?
21. Что является монитором при выполнении участка кода метода?
22. Какие методы позволяют синхронизировать выполнение потоков?
23. Какой метод переводит поток в режим ожидания?
24. Какова функциональность методов notify и notifyAll?
25. Что позволяет сделать метод join?
26. Каковы условия вызова метода wait/notify?
27. Дайте определение понятию “взаимная блокировка”.
28. Чем отличаются методы interrupt, interrupted, isInterrupted?
29. В каком случае будет выброшено исключение InterruptedException, какие методы могут его выбросить?
30. Модификаторы volatile и метод yield().
31. Пакет java.util.concurrent
32. Есть некоторый метод, который исполняет операцию i++. Переменная i типа int. Предполагается, что код будет исполнятся в многопоточной среде. Следует ли синхронизировать блок?
33. Что используется в качестве mutex, если метод объявлен static synchronized? Можно ли создавать новые экземпляры класса, пока выполняется static synchronized метод?
34. Предположим в методе run возник RuntimeException, который не был пойман. Что случится с потоком? Есть ли способ узнать о том, что Exception произошел (не заключая все тело run в блок try-catch)? Есть ли способ восстановить работу потока после того как это произошло?
35. Какие стандартные инструменты Java вы бы использовали для реализации пула потоков?
36.Что такое ThreadGroup и зачем он нужен?
37.Что такое ThreadPool и зачем он нужен?
38.Что такое ThreadPoolExecutor и зачем он нужен?
39.Что такое «атомарные типы» в Java?
40.Зачем нужен класс ThreadLocal?
41.Что такое Executor?
42.Что такое ExecutorService?
43.Зачем нужен ScheduledExecutorService?

Ответы

1. Дайте определение понятию “процесс”.

Процесс — это совокупность кода и данных, разделяющих общее виртуальное адресное пространство. Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств). Для каждого процесса ОС создает так называемое «виртуальное адресное пространство», к которому процесс имеет прямой доступ. Это пространство принадлежит процессу, содержит только его данные и находится в полном его распоряжении. Операционная система же отвечает за то, как виртуальное пространство процесса проецируется на физическую память.

Многопоточность в Java: https://habrahabr.ru/post/164487/

2. Дайте определение понятию “поток”.

Один поток («нить» или «трэд») – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.

Thinking in Java.Параллельное выполнение. https://wikijava.it-cache.net/index.php@title=Glava_17_Thinking_in_Java_4th_edition.html

3. Дайте определение понятию “синхронизация потоков”.

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

Java поддерживает несколько потоков для выполнения. Это может привести к тому, что два или более потока получат доступ к одному и тому же полю или объекту. Синхронизация — это процесс, который позволяет выполнять все параллельные потоки в программе синхронно. Синхронизация позволяет избежать ошибок согласованности памяти, вызванных непоследовательным доступом к общей памяти.
Когда метод объявлен как синхронизированный — нить держит монитор для объекта, метод которого исполняется. Если другой поток выполняет синхронизированный метод, ваш поток заблокируется до тех пор, пока другой поток не отпустит монитор.
Синхронизация достигается в Java использованием зарезервированного слова synchronized. Вы можете использовать его в своих классах определяя синхронизированные методы или блоки. Вы не сможете использовать synchronized в переменных или атрибутах в определении класса.

Синхронизация потоков, блокировка объекта и блокировка класса info.javarush.ru: https://goo.gl/gW4ONp

4. Как взаимодействуют программы, процессы и потоки?

Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). В каждом процессе может быть создано множество потоков. Процессы разделены между собой (>программы), потоки в одном процессе могут взаимодействовать друг с другом (методы wait, notify, join и т.д.).

5. В каких случаях целесообразно создавать несколько потоков?

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

6. Что может произойти если два потока будут выполнять один и тот же код в программе?

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

7. Что вы знаете о главном потоке программы?

Маленькие программы на Java обычно состоят из одной нити, называемой «главной нитью» (main thread). Но программы побольше часто запускают дополнительные нити, их еще называют «дочерними нитями». Главная нить выполняет метод main и завершается. Аналогом такого метода main, для дочерних нитей служит метод run интерфейса Runnable. Много потоков — много методов main (run()).

8. Какие есть способы создания и запуска потоков?

Существует несколько способов создания и запуска потоков.

С помощью класса, реализующего Runnable

  • Создать объект класса Thread .
  • Создать объект класса, реализующего интерфейс Runnable
  • Вызвать у созданного объекта Thread метод start() (после этого запустится метод run() у переданного объекта, реализующего Runnable )

С помощью класса, расширяющего Thread

  • Создать объект класса ClassName extends Thread .
  • Переопределить run() в этом классе (смотрите пример ниже, где передается имя потока ‘Second’)

С помощью класса, реализующего java.util.concurrent.Callable

  • Создать объект класса, реализующего интерфейс Callable
  • Создать объект ExecutorService с указанием пула потоков.
  • Создать объект Future. Запуск происходит через метод submit() ; Сигнатура: Future submit(Callable task)

Java Thread

В Java Process и Thread являются двумя основными единицами исполнения в многопоточном приложении.

Процессы(Process)

Process представляет собой автономную среду исполнения, и его можно рассматривать как программу или приложение. Однако во время исполнения в среде Java программы могут иметь различные дочерние процессы.

Потоки( Thread)

Поток можно представить как облегчённую версию процесса. Для создания нового поток нужно меньшее количество ресурсов чем для создания нового процесса.

О потоках в Java

Каждое приложение написанное на Java имеет минимум один поток — main thread. Хотя так же в Java выполняются и системные, фоновые потоки типа: менеджера памяти, системного менеджера, обработчика сигналов и т.д. Но тем не менее в Java, точкой старта является статический метод main из которого уже можно запустить новые потоки.

Преимущества потоков(Thread) в Java

  1. Потоки в Java более легковесны по сравнению с процессами. Для создания потока требуется меньше времени и ресурсов.
  2. У потока есть доступ к данным и коду родительского процесса.
  3. Переключение между потоками более простая операция чем переключение между процессами.
  4. Обмен данными между потоками более простой чем между процессами.

Для создания нового потока в Java существуют два способа:

Многопоточность в Java

Продолжаем изучать стандартную библиотеку языка программирования Java. И сегодня речь пойдет о многопоточности:

  • что такое многопоточность;
  • как ее реализовать;
  • как создать о остановить потоки выполнения.

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

Многопоточность — это свойство системы выполнять несколько вычислений одновременно, тем самым ускоряя процесс этого вычисления. Например, когда Вы играете в компьютерные игры, вы видите, что Ваш персонаж выполняет определенное действие, другие персонажи, анимация, звук. Это все отдельные потоки если говорить примитивно.

В языке Java есть стандартный класс, который реализует многопоточность: Thread, который имплементирует Runable интерфейс. Для того, чтобы реализовать многопоточность в своей программе нужно унаследовать свой класс от Thread или имплементировать интерфейс Runable. Нечто похожее мы делали, когда создавали свои классы исключения в статье о исключениях. Но это еще не все. В классе Thread есть метод run() и start(), которые созданы чтобы делать вычисления и запускать выполнение кода соответственно. То есть в методе run() мы пишем, что хотим выполнить, а когда вызываем метод start(), он автоматически запускает наш код в run. Вот такая многоходовочка)). Все гораздо проще, когда смотришь на код.

Часть 2. Выполнение задач в многопоточном режиме

Серия контента:

Этот контент является частью # из серии # статей: Вселенная Java

Этот контент является частью серии: Вселенная Java

Следите за выходом новых статей этой серии.

При разработке ПО часто возникают ситуации, когда необходимо обеспечить одновременное выполнение нескольких задач, например, отправку и получение электронной почты. В корпоративных JEE-приложениях для этого предлагается использовать специальные инфраструктуры и библиотеки, такие как Quartz. Но для JSE-приложений такой подход может не окупиться, так как для внедрения инфраструктуры ресурсов потребуется больше, чем для реализации самих задач.

Мастер Йода рекомендует:  Убедитесь, что ваши колонки не «схлопываются» по горизонтали

Поэтому в данной статье рассматриваются два подхода к параллельному выполнению задач в JSE-приложениях: классический способ и новые возможности пакета java.util.concurrent.

Классический подход к запуску задач в многопоточном режиме

Классический подход к запуску задач в многопоточном режиме в JSE предполагает использование класса java.lang.Thread или интерфейса java.lang.Runnable. В первом случае программист создает потомка класса Thread и переопределяет в нем метод run, куда помещается функциональность, которую необходимо выполнить в многопоточном режиме, как показано в листинге 1.

Листинг 1. Запуск задач с помощью класса java.lang.Thread

Использование интерфейса Runnable основывается на другой парадигме. Сначала программист должен реализовать интерфейс Runnable в собственном классе, а затем поместить объект этого класса в объект типа Thread. Интересующая функциональность также помещается в метод run класса, реализующего интерфейс Runnable, и впоследствии вызывается объектом-контейнером, как показано в листинге 2.

Листинг 2. Запуск задач с помощью интерфейса java.lang.Runnable

Как видно в обоих примерах запуск задачи в многопоточном режиме выполняется через вызов метода start объекта типа Thread. Только в первом случае после вызова метода start класса Thread происходит вызов метода run, наследника этого класса (класса ThreadSample), в котором и находится код, относящейся к задаче. При выборе реализации на основе интерфейса Runnable сначала происходит вызов метода start класса Thread, затем обращение к методу run этого же класса, и уже из этого метода вызывается метод run реализации интерфейса Runnable (класса RunnableSample).

Ошибки при использовании классического подхода

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

Во-первых, часто вместо вызова метода start для запуска потока программист сразу вызывает метод run, что приводит к неправильному результату. Если сразу вызвать метод run на строке 8 в листинге 1 или листинге 2, то программа отработает без всяких видимых изменений. Ошибка заключается в том, что при непосредственном вызове метода run задача будет выполняться, но в однопоточном, а не в многопоточном режиме. Поэтому, если такая задача всего одна, программа отработает нормально, хотя и несколько медленнее (но «невооруженным» глазом это будет незаметно), а вот в случае с несколькими задачами падение производительности окажется фатальным.

Другая проблема связана с самим использованием наследования класса Thread вместо реализации интерфейса Runnable. Если при реализации интерфейса требуется обязательное соблюдение сигнатуры при переопределении метода (в данном случае метода run), то в наследовании такого ограничения не существует. Поэтому ошибка в сигнатуре метода run в листинге 1 автоматически меняет состояние этого метода с «переопределенный» на «перегруженный», при этом при компиляции не будет выведено никаких предупреждений или сообщений об ошибках. Однако запуск подобной программы опять приведет к возникновению непредусмотренного результата, а точнее, полному отсутствию такового. Это будет связано с тем, что при отсутствии переопределенной версии метода run будет вызвана реализация этого метода по умолчанию, которая расположена в классе Thread. Эта реализация по умолчанию не содержит никакой функциональности, соответственно поток запустится и тут же остановится, так как никакой работы для него нет.

Для решения этой проблемы на помощь приходит одна из возможностей, появившихся в JSE 5, — аннотация Override, как показано на строке 8 в листинге 1. Эта аннотация заставляет компилятор выполнить проверку, действительно ли объявленный метод переопределяет какой-нибудь из методов суперкласса. В случае, если метод, отмеченный этой аннотацией, не является переопределением метода из суперкласса, то компилятор выводит сообщение об ошибке. Вообще, аннотацию Override рекомендуется применять во всех случаях, когда выполняется переопределение методов, так как это повышает качество разрабатываемого кода.

Выбор между интерфейсом java.lang.Runnable и классом java.lang.Thread

Как было показано ранее, при необходимости обеспечить параллельное выполнение нескольких задач у программиста есть возможность выбрать, как именно реализовать эти задачи: с помощью класса Thread или интерфейса Runnable. У каждого подхода есть свои преимущества и недостатки.

В качестве основного преимущества при наследовании класса Thread заявляется полный доступ ко всем функциональным возможностям потока на платформе Java. Главным недостатком же считается как раз сам факт наследования, так как в силу того, что в Java применяется только одиночное наследование, то наследование классу Thread автоматически закрывает возможность наследовать другим классам. Для классов, отвечающих за доменную область или бизнес-логику, это может стать серьезной проблемой. Правда негативное воздействие, возникающее из-за невозможности наследования, можно ослабить, если вместо него применить прием делегирования или соответствующие шаблоны проектирования.

Использование интерфейса Runnable по умолчанию лишено этого недостатка, но если реализовать задачу таким способом, то придется потратить дополнительные усилия на ее запуск. Как было показано в листинге 2, для запуска Runnable-задачи все равно потребуется объект Thread, также в этом случае исчезнет возможность прямого управления потоком из задачи. Хотя последнее ограничение можно обойти с помощью статических методов класса Thread (например, метод currentThread() возвращает ссылку на текущий поток).

Поэтому сделать однозначный вывод о превосходстве какого-либо подхода довольно сложно, и чаще всего в приложениях одновременно используются оба варианта, но для решения задач различной направленности. Считается, что наследование класса Thread следует применять только тогда, когда действительно необходимо создать «новый вид потока, который должен дополнить функциональность класса java.lang.Thread», и подобное решение применяется при разработке системного ПО, например, серверов приложений или инфраструктур. Использование интерфейса Runnable показано в случаях, когда просто «необходимо одновременно выполнить несколько задач» и не требуется вносить изменений в сам механизм многопоточности, поэтому в бизнес-ориентированных приложениях в основном используется вариант с интерфейсом Runnable.

Ограничения классического подхода

Когда программист только начинает работать с многопоточными возможностями Java-платформы, то на первых порах он может даже впасть в состояние «эйфории», особенно, если у него уже был негативный опыт по созданию многопоточных приложений в других языках программирования. Действительно, система управления потоками в Java организованна крайне удачно, так как этот компонент платформы был детально проработан еще на стадии проектирования самых первых версий виртуальной Java-машины и языка программирования Java, и сейчас ему по-прежнему уделяется большое внимание. Однако со временем «розовые очки» спадают, и программист начинает замечать «раздражающие» моменты, которые усложняют организацию параллельного исполнения задач в Java.

Первым, что бросается в глаза, оказывается слияние низкоуровневого кода, отвечающего за многопоточное исполнение, и высокоуровневого кода, отвечающего за основную функциональность приложения (так называемый «спагетти-код»). В листинге 1 показано, что бизнес—код и поточный код вообще находятся в одном классе, но даже в более удачном варианте из листинга 2 для выполнения задачи все равно требуется создать объект Thread и запустить его. Подобное перемешивание снижает качество архитектуры приложения и может затруднить его последующее сопровождение.

Но даже если удалось отделить поточный код от основного, то возникает проблема, связанная уже с управлением самими потоками. Потоки в Java запускаются только путем вызова метода start и останавливаются после вызова соответствующих методов или самостоятельно после завершения работы метода run. Также после того, как поток остановился, его нельзя запустить второй раз, что и приводит к следующим негативным моментам:

  • поток занимает относительно много места в куче, так что после его завершения необходимо проследить, чтобы память, занимаемая им, была освобождена (например, присвоить ссылке на поток значение null);
  • для выполнения новой задачи потребуется запустить новый поток, что приведет к увеличению «накладных расходов» на виртуальную машину, так как запуск потока – это одна из самых требовательных к ресурсам операций.

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

Важно сказать, что «среднестатистический» программист будет отнюдь не первым, кто сталкивается с подобными проблемами в многозадачных приложениях. Все эти проблемы были давно проанализированы Java-сообществом и нашли решение в признанных шаблонах проектирования (например, ThreadPool (пул потоков) и WorkerThread (рабочий поток)). Но скорее всего, у рядового программиста, ограниченного временными рамками проекта, просто не будет времени или ресурсов, чтобы подготовить и самое главное протестировать полноценную реализацию данных шаблонов. А неточное или неполное внедрение этих шаблонов (да и вообще любых шаблонов проектирования) может в будущем негативно сказаться на этапе сопровождения продукта.

Новые возможности пакета java.uti.concurrent

Платформа Java постоянно развивается, и поэтому к существующей функциональности все время добавляются новые возможности. Иногда новая функциональность берется из уже существующих сторонних библиотек, при этом речь не идет о банальном копировании, а скорее о переосмыслении и доработке уже существующих решений. Подобным способом в версию Java 5 был добавлен пакет java.util.concurrent, включающий в себя множество уже проверенных и хорошо зарекомендовавших себя приемов для параллельного выполнения задач (этот пакет — только одно из множества важных нововведений, представленных в Java 5).

В рамках этой статьи интерес представляют уже готовые к использованию реализации шаблонов WorkerThread и ThreadPool, а также еще один способ реализации задач для параллельного выполнения, кроме упоминавшихся класса Thread и интерфейса Runnable. Ещё в пакете java.util.concurrent находятся два подпакета: java.util.concurrent.locks и java.util.concurrent.atomic, с которыми тоже стоит ознакомиться, так как они значительно упрощают организацию взаимодействия между потоками и параллельного доступа к данным.

Создание задачи с помощью интерфейса java.util.concurrent.Callable

Интерфейс Callable гораздо больше подходит для создания задач, предназначенных для параллельного выполнения, нежели интерфейс Runnable или тем более класс Thread. При этом стоит отметить, что возможность добавить подобный интерфейс появилась только начиная с версии Java 5, так как ключевая особенность интерфейса Callable – это использование параметризованных типов (generics), как показано в листинге 3.

Листинг 3. Создание задачи с помощью интерфейса Callable

Сразу необходимо обратить внимание на строку 2, где указано, что интерфейс Callable является параметризованным, и его конкретная реализация – класс CallableSample, зависит от типа String. На строке 3 приведена сигнатура основного метода call в уже параметризованном варианте, так как в качестве типа возвращаемого значения также указан тип String. Фактически это означает, что была создана задача, результатом выполнения которой будет объект типа String (см. строку 8). Точно также можно создать задачу, в результате работы которой в методе call будет создаваться и возвращаться объект любого требуемого типа. Такое решение значительно удобнее по сравнению с методом run в интерфейсе Runnable, который не возвращает ничего (его возвращаемый тип – void) и поэтому приходится изобретать обходные пути, чтобы извлечь результат работы задачи.

Еще одно преимущество интерфейса Callable – это возможность «выбрасывать» исключительные ситуации, не оказывая влияния на другие выполняющиеся задачи. На строке 3 указано, что из метода может быть «выброшена» исключительная ситуация типа Exception, что фактически означает любую исключительную ситуацию, так как все исключения являются потомками java.lang.Exception. На строке 5 эта возможность используется для создания контролируемой (checked) исключительной ситуации типа IOException. Метод run интерфейса Runnable вообще не допускал выбрасывания контролируемых исключительных ситуаций, а выброс неконтролируемой (runtime) исключительной ситуации приводил к остановке потока и всего приложения.

Запуск задач с помощью java.util.concurrent.ExecutorService

Облегчив с помощью интерфейса Callable создание задач для параллельного выполнения, пакет java.util.concurrent также берет на себя работу по запуску и остановке потоков. Вместо объекта Thread предлагается использовать объект типа ExecutorService, с помощью которого пользователь может просто поместить задачу в очередь на выполнение и ждать получения результата. Можно сказать, что ExecutorService – это значительно усовершенствованная реализация шаблона WorkerThread.

ExecutorService – это интерфейс, поэтому для выполнения задач используются его конкретные потомки, адаптированные под требования разрабатываемого приложения. Однако программисту нет необходимости создавать собственную реализацию ExecutorService, так как в пакете java.util.concurrent уже присутствуют различные варианты реализации ExecutorService. Доступ к ним можно получить через статические методы служебного класса Executors, метод которого newFixedThreadPool возвращает объект типа ExecutorService со встроенной поддержкой шаблона ThreadPool. Также в классе Executors есть и другие методы для создания объектов ExecutorService с различными свойствами.

Наибольший интерес в ExecutorService представляет метод submit, через который задача ставится в очередь на выполнение. На вход этот метод принимает объект типа Callable или Runnable, а возвращает некий параметризованный объект типа Future. Этот объект можно использовать для доступа к результату выполнения задачи, который будет возвращен из метода call соответствующего Callable-объекта. При этом через объект Future можно проверить, закончено ли уже выполнение задачи – с помощью метода isDone и через метод get получить доступ к результату или исключительной ситуации, если в процессе выполнения задачи произошла ошибка.

Таким образом, при запуске задач с помощью классов из пакета java.util.concurrent не требуется прибегать к низкоуровневой поточной функциональности класса Thread, достаточно создать объект типа ExecutorService с нужными свойствами и передать ему на исполнение задачу типа Callable. Впоследствии можно легко просмотреть результат выполнения этой задачи с помощью объекта Future, как показано в листинге 4.

Листинг 4. Запуск задачи с помощью классов пакета java.util.concurrent

Стоит обратить внимание на строку 18, где происходит остановка объекта ExecutorService с помощью метода shutdown. Дело в том, что потоки в объекте ExecutorService не останавливаются сами, как обычно, поэтому их необходимо явно остановить с помощью этого метода, при этом если в ExecutorService находятся невыполненные задачи, то потоки будут остановлены только, когда завершится последняя задача.

Многопоточность в Java Введение Hardware

Многопоточность в Java.ppt

Многопоточность в Java

Введение • Hardware • Software • Операционная система – Работа прикладного ПО – Один процессор, много программ – Многозадачность, планировщик – Программа и процесс 2

Процессы и потоки • Процесс • Поток, отличие от процесса Поток 1. 1 Поток 1. 2 Процесс 1 Поток 1. 3 Поток 2. 1 Поток 2. 2 Процесс 2 • Не путать с потоками данных (streams) 3

Многопоточность • Параллельное выполнение потоков • Переключение контекста 4

Практическое применение • Увеличение производительности • Параллельные алгоритмы – Альфа-бета отсечения – Сортировка слиянием • UI + вычисления – Компьютерная игра – Spell checker 5

Практическое применение • Упрощение программы (несколько различных логических функций, асинхронные действия) – Чтение + обработка – Текстовый редактор (асинхронные бэкапы) • Повторение последовательных алгоритмов – Веб-сервер (обслуживание мн-ва клиентов) – Обработка множества фотографий 6

Проблемы и трудности • Атомарные и неатомарные операции – Long, Double • Общие ресурсы – Одновременный доступ к общим данным (порча данных) • Состояние гонки – Зависимость порядка выполнения • Сложность отладки 7

Неатомарные операции • long и double – неатомарные типы • Только часть переменной м. б. обновлена за одну атомарную операцию • Между обновлениями могут «вклиниться» другие потоки и прочитать «недообновлённую» переменную 8

Общие ресурсы • Пример: «общий» связанный список • Первый поток обходит список, второй поток удаляет элемент • Может возникнуть ситуация, когда элемент удалён одним потоком, а второй поток пытается получить к нему доступ 9

Состояние гонки • Ситуация, когда порядок выполнения одного потока зависит от другого 1. if в потоке 2 проверяет x на чётность. 2. Оператор «x++» в потоке 1 увеличивает x на единицу. 3. Оператор вывода в потоке 2 выводит « 1» , хотя, казалось бы, переменная проверена на чётность. // Общая переменная int x = 0; // Поток 1: while (!stop) < x++; >// Поток 2: while (!stop) < if (x%2 == 0) System. out. println(x); >10

Решение проблем • Использование правильных типов данных (Atomic. Long) или volatile • Синхронизация – Мьютексы, критические секции и т. д. – События (wait, notify, sleep) • Рассмотрим на примерах 11

Классические задачи • Основные – Одновременный доступ к ресурсу – Производитель-потребитель • И другие – Не будут рассмотрены в рамках презентации 12

Потоки в Java • Класс Thread – поток – Позволяет создавать потоки и производить операции с ними • Интерфейс Runnable – сущность, которая может быть запущена – public void run(); • Thread реализует Runnable 13

Создание потока • main() – главный поток • Простейший способ – анонимный класс // Создание потока Thread t = new Thread(new Runnable() < public void run() < System. out. println("Hello"); >>); // Запуск потока t. start(); 14

Создание потока • Ещё один способ – Класс, реализующий Runnable – Реализовать метод run() public >

Создание потока • Ещё один способ – Класс, наследующийся от Thread – Реализовать метод run() public >

Одновременный доступ • • Атомарные типы данных и volatile Мьютекс (lock, unlock) Критическая секция (synhronized) Синхронизированный метод 17

Специальные атомарные типы • Atomic. Integer, Atomic. Boolean, Atomic. Long, Atomic. Integer. Array • Синхронизированы не только чтение и запись, но и операции, обычно не являющиеся атомарными (инкремент, декремент) 18

Ключевое слово volatile • Не сохраняются «внутрипоточные» копии • Синхронизация чтения и записи переменной volatile • Не синхронизируются неатомарные операции my. Volatile. Var++; аналогично: int temp = 0; synchronize(my. Volatile. Var) < temp = my. Volatile. Var; >temp++; synchronize(my. Volatile. Var) < my. Volatile. Var = temp; >19

Мьютекс • • Самый низкий уровень абстракции «Ручное» управление Плюсы: гибкость Минусы: неосторожность использования ведёт к трудностям и ошибкам 20

Пример мьютекса import java. util. concurrent. locks. *; Lock locker = new Reentrant. Lock(); locker. lock(); try < // действия >finally < locker. unlock(); >21

Критическая секция • «Синхронизированный» блок кода • Любой объект может служить объектом синхронизации • Снятие блокировки производится автоматически 22

Пример критической секции synchronized (object) < // действия 1 >// … synchronized (object) < // действия 2 >23

Синхронизированные методы • Каждый синхронизированный метод текущего класса – критическая секция • Объектом синхронизации выступает объект текущего класса • Если один поток выполняет какой-либо синхронизированный метод, остальные потоки его ждут 24

Пример синхронизации методов public class my. Class < public void a() < … >synchronized public void b() < … >synchronized public void c() < … >> 25

Производитель-потребитель • Один поток производит данные, второй их потребляет • Несколько потоков производят данные и несколько их потребляют • Данные могут храниться в очереди 26

Производитель-потребитель (1) • Класс данных class Data < private Object data; public void set(Object data) < … >public Object get() < … >> 27

Производитель-потребитель (2) • Установка значения public vo >

Производитель-потребитель • Данная модель неэффективна – Бесконечные циклы впустую тратят процессорное время • Использование sleep(time) не помогает – Маленькое время ожидания тратит процессорное время – Если «спать» долго, можно «проспать» – sleep() не снимает блокировку 30

Использование монитора • Любой объект может быть монитором • Для взаимодействия с монитором поток должен иметь блокировку на него • Методы монитора – wait(time? ) – ожидание монитора – notify() – извещение одного из ждущих потоков – notify. All() – извещение всех ждущих потоков 31

Производитель-потребитель (2) • Установка значения public synchronized vo >

Производитель-потребитель (3) • Получение значения public synchronized Object get() throws Interrupted. Exception < while (data == null) wait(); Object d = data; data = null; notify(); return d; >33

Ожидание окончания потока • Методы класса Thread – join() – ожидать до завершения – join(long millis) – ожидать до завершения или истечения millis миллисекунд – join(long millis, long nanos) – ожидать до завершения или истечения millis миллисекунд и nanos миллисекунд 34

Прерывание потока • Методы класса Thread – interrupt() – установить флаг прерывания – is. Interrupted() – проверить флаг прерывания – interrupted() – проверить и сбросить флаг прерывания 35

Дополнительные методы • Приостановка выполнения – sleep(time) – приостановить поток на время – yield() – позволить выполниться другим потокам • Получение текущего потока – current. Thread() • Завершение потока – destroy() 36

Новый класс ошибок • Deadlock (взаимная блокировка) • Пессимизация скорости из-за неправильного применения синхронизации – Одновременный просмотр списка 37

java. util. concurrent • Потоко-безопасные структуры данных 38

Добавить комментарий