Pull to refresh

java.util.concurrent. Часть первая: Зачем и почему?

Reading time 4 min
Views 28K
Часть первая, в которой множеством слов раскрывается смысл существования этого API
Эта статья, хоть и не является прямым переводом, основана на статье Брайана Гетца Concurrency in JDK 5.0


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

Однако потоки были реализованы современными операционными системами задолго до массового появления многоядерных процессоров. Зачем же нужны потоки на однопроцессорных системах?

Причин несколько

1. Отзывчивый графический интерфейс. Длительные операции могут быть выполнены в отдельном потоке, в то время как приложение способно обслуживать события от клавиатуры и мышки.
2. Возможность более эффективного использования ресурсов: процессора, памяти, жесткого диска и сети. В то время как один из потоков простаивает, ожидая завершения операции чтения файла, второй поток может в это время устанавливать сетевое соединение с клиентом, а третий — обрабатывать какой нибудь запрос.
3. Простота модели “поток-на-запрос” и идея фоновых системных процессов. Оставляя детали распределения ресурса процессора на совесть операционной системы, мы получаем возможность сконцентрировать программу на обработке самого запроса. Мы также можем выделить определённые задачи (например, сборку мусора или системный отладчик) в отдельные потоки, и таким образом “незаметно” добавить новые свойства программе.

Первые два пункта в основном понятны, третий распишу более подробно.

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

Однако алгоритм не создается для одного запроса. Если требуется обрабатывать большое количество входных данных, то возникает желание либо уменьшить общее время их обработки, либо увеличить скорость их обработки (и таким образом увеличить пропускную способность обработчика). Самый простой способ в данном случае — поставить больше однопоточных алгоритмов и заставить их работать параллельно. Возможно, придется как-то согласовывать доступ к общим ресурсам и координировать их выполнение, но это обычно значительно проще чем разработка нового алгоритма, который за раз может обработать больше чем один набор данных (такой алгоритм нужно не только разработать, но еще и доказать). Это и понимается под простотой модели “поток-на-запрос”.

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

Каждый убежденный программист при программировании многопоточного приложения использует общие для нескольких (возможно всех) потоков данные, размещенные в памяти. Одни данные играют ключевую роль в координации потоков, например условия завершения операции копирования большого файла, другие хранят общие для потоков сведения о состоянии объектов, обеспечивая разделяемый доступ к ним, например, значения электронной таблицы или список сотрудников компании, которые отображены на экране; вычисление третьих данных может быть целью выполнения множества потоков, например лента комментариев этого блога или счетчик посетителей сайта.

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

Поскольку в Java данные — это объекты, то к классам этих объектов, которые будут обрабатывать вызовы из нескольких потоков, предъявляется требование многопоточной безопасности (thread safety). Что это значит? Это. конечно же значит, что вызовы методов обьекта будут безопасными, что, конечно, правильно, но нисколько не помогает нам понять, что же такое потокобезопасность класса.

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

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

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

До версии 5.0 единственными средствами JDK для придания классам свойств потокобезопасности были синхронизация выполнения методов и блоков с помощью ключевого слова synchronized, а взаимодействие осуществлялось с помощью внутренних блокировок объектов и volatile-переменных. Этого оказалось недостаточно для миллионов современных программистов, поэтому наиболее инициативный из них, по имени Даг Ли написал книгу Concurrent Programming in Java. Многие описанные в книге идеи легли в основу JSR 166: Concurrency Utilities и в результате под общим зонтиком java.util.concurrent API, в JDK 5.0 было добавлено множество новых высокоуровневых средств для облегчения задач взаимодействия потоков: блокирующие структуры данных, различные средства синхронизации и много других интересных вещей, которые я рассмотрю в следующей части.
Tags:
Hubs:
+29
Comments 29
Comments Comments 29

Articles