The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
Я затронул эту проблему в одном из моих последних вебинаров, теперь пришло время объяснить ее в письменной форме. Потокобезопасность - это важное качество классов в языках/платформах, таких как Java, где мы часто используем объекты между потоками. Проблемы, вызванные отсутствием потокобезопасности, очень сложны для отладки, так как они случайны и практически невозможно воспроизвести намеренно. Как вы тестируете свои объекты, чтобы убедиться, что они потокобезопасны? Вот как я это делаю.
Предположим, у нас есть простая книжная полка в памяти:
Сначала мы помещаем книгу туда, и книжная полка возвращает ее идентификатор. Затем мы можем прочитать название книги по ее идентификатору.
Класс, кажется, является потокобезопасным, поскольку мы используем потокобезопасный ConcurrentHashMap
вместо более простого и непотокобезопасного HashMap
, верно? Давайте попробуем протестировать это:
Тест проходит, но это только однопоточный тест. Давайте попробуем выполнить ту же манипуляцию из нескольких параллельных потоков (я использую Hamcrest).
Сначала я создаю пул потоков с помощью Executors
. Затем я отправляю десять объектов типа Callable
с помощью submit()
. Каждый из них добавит новую уникальную книгу на полку. Все они будут выполнены в некотором непредсказуемом порядке некоторыми из этих десяти потоков из пула.
Затем я получаю результаты их выполнения через список объектов типа Future
. Наконец, я вычисляю количество созданных уникальных идентификаторов книг. Если число равно 10, то конфликтов не было. Я использую коллекцию Set
, чтобы убедиться, что список идентификаторов содержит только уникальные элементы.
Тест проходит на моем ноутбуке. Однако он не сильно надежен. Проблема здесь заключается в том, что он не проверяет Books
из нескольких параллельных потоков. Время, проходящее между вызовами submit()
, достаточно велико, чтобы завершить выполнение books.add()
. Поэтому на самом деле будет выполняться только один поток одновременно. Мы можем проверить это, немного изменив код:
С помощью этого кода я пытаюсь узнать, насколько часто потоки перекрываются друг с другом и выполняют что-то параллельно. Это никогда не происходит, и overlaps
равно нулю. Таким образом, наш тест на самом деле ничего не проверяет. Он просто добавляет десять книг на книжную полку по одной. Если я увеличу количество потоков до 1000, иногда они начинают перекрываться. Но мы хотим, чтобы они перекрывались даже при небольшом их количестве. Чтобы решить эту проблему, нам нужно использовать CountDownLatch
:
Теперь каждый поток перед тем, как коснуться книг, ожидает разрешения, предоставленного latch
. Когда мы отправляем их все через submit()
, они остаются на удержании и ожидают. Затем мы освобождаем замок с помощью countDown()
, и они все начинают двигаться одновременно. Теперь, на моем ноутбуке, overlaps
равно 3-5, даже когда threads
равно 10.
И этот последний assertThat()
теперь выдает ошибку! Я не получаю 10 идентификаторов книг, как раньше. Это 7-9, но никогда не 10. Класс, видимо, не является потокобезопасным!
Но прежде чем мы исправим класс, давайте упростим наш тест. Давайте используем RunInThreads
(переименовано просто в Threads
в последней версии) из Cactoos, который делает точно то же самое, что и мы сделали выше, но под капотом.
Первый аргумент assertThat()
- это экземпляр Func
(функциональный интерфейс), принимающий AtomicInteger
(первый аргумент RunsInThreads
) и возвращающий Boolean
. Эта функция будет выполнена на 10 параллельных потоках с использованием того же подхода на основе замков, что и показано выше.
Этот RunInThreads
кажется компактным и удобным, я уже использую его в нескольких проектах.
Кстати, чтобы сделать Books
потокобезопасным, мы просто должны добавить synchronized
к его методу add()
. Или может быть у вас есть лучшее решение?
P.S. Я узнал все это из книги Java Concurrency in Practice авторов Гойтц и др.
Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 04:44