Здравствуйте, я криворукий.
Недавно один читатель рассказал мне, что когда интервью было про пулы потоков, они отлично поговорили и в основном ответили на все из них, но один из вопросов прямо ввел его в ступор.
Интервьюер спросил его: Расскажите мне о блокировках в пуле потоков.
В результате его знания о пуле потоков действительно были замечены в различных блогах или фейсбуках, сам он не читал исходный код, поэтому не обращал внимания на наличие блокировок в пуле потоков.
Еще он жаловался мне:
Когда он сказал это, я также почувствовал, что, когда все говорили о пуле потоков, они мало говорили об используемых в нем блокировках.
Это действительно очень низкое присутствие.
Мне это устроить?
mainLock
На самом деле, в пуле потоков все еще есть много мест, где используются блокировки.
Например, как я уже говорил, в пуле потоков есть переменная worker, и то, что она хранит, можно понимать как потоки в пуле потоков.
А структура данных этого объекта — HashSet.
Вы знаете, что HashSet не потокобезопасный класс коллекции?
Итак, посмотрите, что говорят комментарии к нему:
Доступ к нему возможен только при удерживании mainLock.
Даже если я его не ввожу, вы чувствуете это из названия: если нет догадок, то mainLock должен быть замком.
Так ли это, и если да, то что это за замок?
В исходном коде переменная mainLock находится чуть выше воркеров:
Оказывается, его настоящее тело — это ReentrantLock.
Нет ничего плохого в защите HashSet с помощью ReentrantLock.
Так как же ReentrantLock и рабочие процессы работают вместе?
В качестве примера возьмем наиболее важный метод addWorker:
Когда используется замок, должно быть что-то, что нужно монополизировать.
Посмотрите еще раз, вы блокируете и монополизируете общий ресурс, что вы хотите сделать?
В большинстве случаев вы должны захотеть изменить его и что-то в него запихнуть, верно?
Вот вы анализируете по этой идее, какой именно код заворачивается в блокировку в эксклюзиве addWorker?
На самом деле анализировать не нужно, здесь всего два общих данных. Оба должны быть записаны, и два имеют общие данные, один является рабочим объектом, а другой является переменной LargePoolSize.
worker Как мы уже говорили ранее, его структура данных представляет собой небезопасный для потоков HashSet.
Что такое наибольший размер пула и почему он заблокирован?
Это поле используется для записи максимального количества потоков, которые когда-либо появлялись в пуле потоков.
Блокировка mianLock также добавляется при чтении этого значения:
На самом деле, лично я считаю, что в этом месте используется volatile для изменения переменной largePoolSize, что может сохранить операцию блокировки mainLock.
Также потокобезопасный.
Я не знаю, чувствуешь ли ты то же самое?
Если вы тоже так думаете, извините, вы ошибаетесь.
Многие другие поля в пуле потоков используют volatile:
Почему не используется наибольший размер пула?
Посмотрите еще раз на то место, где метод getLargestPoolSize ранее получает значение.
Если он изменен на volatile и не заблокирован, на одну операцию mainLock.lock() меньше.
Удаление этой операции может привести к тому, что одной блокирующей операцией ожидания станет меньше.
Предполагая, что у метода addWorkers не было времени для изменения значения параметра LargePoolSize, поток вызывает метод getLargestPoolSize.
Поскольку блокировки нет, значение, полученное напрямую, является только наибольшим размером пула в данный момент, не обязательно после выполнения метода addWorker.
При блокировке программа может воспринимать, что величина LargePoolSize может изменяться, поэтому полученный результат должен быть наибольшим размером пула после выполнения метода addWorker.
Я так понимаю, что блокировка должна обеспечить точность этого параметра в наибольшей степени.
В дополнение к местам, упомянутым выше, есть еще много мест, где используется mainLock:
Я не буду вводить их по одному, вы должны посмотреть сами, эти вещи не очень интересно вводить, это весь код, который можно понять с первого взгляда.
Скажи что-нибудь интересное.
Вы когда-нибудь задумывались, почему г-н Дуг Ли использовал небезопасные для потоков HashSet и ReentrantLock для обеспечения безопасности потоков?
Почему бы просто не создать потокобезопасную коллекцию Set, такую как Collections.synchronizedSet?
Ответ на самом деле появился раньше, но я специально его не говорил, и все этого не заметили.
Просто в комментарии mainLock говорится:
Я подберу ключевые моменты, чтобы рассказать вам.
Сначала посмотрите на это предложение:
While we could use a concurrent set of some sort, it turns out to be generally preferable to use a lock.
Это предложение - перевернутое предложение, новых слов быть не должно, это всем известно.
Один из них оказывается, могу представить, это фраза, которая часто встречается в диалогах в американских сериалах.
Перевод состоит из четырех слов «доказательство фактов».
Итак, вся фраза выше такова:Хотя мы могли бы использовать некоторую безопасную для параллелизма коллекцию наборов, оказывается, что в целом лучше использовать блокировки.
Далее старик объяснит, почему лучше использовать замок.
Что я имею в виду, переводя это предложение, так это то, что я не сказал ничего глупого, и все это вполне обоснованно, потому что именно поэтому старик сам объяснил, почему он не использовал потокобезопасные коллекции Set.
Первая причина такова:
Among the reasons is that this serializes interruptIdleWorkers, which avoids unnecessary interrupt storms, especially during shutdown. Otherwise exiting threads would concurrently interrupt those that have not yet interrupted.
Английский Да, я перевел это на китайский язык, плюс мое собственное понимание таково.
Во-первых, в первом предложении есть «сериализует прерываниеIdleWorkers» Комбинация этих двух слов все еще сбивает с толку.
сериализует здесь, не относится к операции сериализации в нашей Java, но должен быть переведен как «сериализация».
interruptIdleWorkers, это вообще не слово, это метод в пуле потоков:
Первым делом в этом методе нужно взять блокировку mainLock и попытаться прервать поток.
Из-за существования mainLock.lock несколько потоков, вызывающих этот метод, сериализуются с помощью сериализации.
В чем преимущество сериализации?
Вот что я скажу позже: избегайте ненужных бурь прерываний, особенно при вызове метода выключения, чтобы избежать прерывания выходящими потоками тех потоков, которые еще не были прерваны.
Почему именно здесь упоминается метод выключения?
Поскольку метод выключения вызывает interruptIdleWorkers:
Так что же означает вышеизложенное?
Здесь будут использованы контрдоказательства.
Предположим, мы используем безопасную для параллелизма коллекцию Set без mainLock.
В это время 5 потоков вызывают метод shutdown.Так как mainLock не используется, блокировки нет, поэтому каждый поток будет запускать interruptIdleWorkers.
Поэтому будет казаться, что первый поток инициирует прерывание, в результате чего возникает worker , то есть поток прерывается. Второй поток пришел, чтобы снова инициировать прерывание, поэтому он еще раз инициировал прерывание для прерывания, которое было прервано.
Ну, это что-то вроде скороговорки.
Итак, я собираюсь повторить это: для прерывания, которое прерывается, инициируйте прерывание.
Поэтому здесь используются блокировки, чтобы избежать риска штормов прерываний.
В параллельном режиме только один поток может инициировать прерванную операцию, поэтому необходимы блокировки. С основной предпосылкой блокировки коллекция Set в любом случае будет заблокирована, поэтому нет необходимости в одновременно безопасном Set.
Итак, я понимаю, что mainLock используется здесь для обеспечения сериализации, при этом гарантируя, что коллекция Set не будет доступна одновременно.
Поскольку операция этого набора гарантированно будет обернута блокировкой, нет необходимости в защищенной от параллелизма коллекции Set.
То есть то, что написано в примечании: Доступ только под mainLock.
Помните, вы можете пройти тестирование.
Затем, вторая причина, по которой старик сказал:
It also simplifies some of the associated statistics bookkeeping of largestPoolSize etc.
В этом предложении речь идет о блокировке и поддержании параметра наибольшего размера пула, поэтому я не буду его повторять.
О, кстати, есть и т. д., что означает «что угодно».
Это и т. д. относится к этому параметру completeTaskCount, причина та же:
другой замок
В дополнение к mainLock, упомянутому выше, на самом деле существует блокировка в пуле потоков, которую часто все игнорируют.
Это рабочий объект.
Вы можете видеть, что Worker наследуется от объектов AQS, и многие его методы также связаны с блокировками.
В то же время он также реализует метод Runnable, поэтому в конечном итоге это инкапсулированный поток, используемый для запуска задач, отправленных в пул потоков.Когда задач нет, он переходит в очередь, чтобы взять или опросить и подождите. был переработан.
Давайте посмотрим на место, где он заблокирован, в очень важном методе runWorker:
java.util.concurrent.ThreadPoolExecutor#runWorker
Тогда возникает вопрос:
Вот поток в пуле потоков, где выполняется логика отправленного задания, зачем его блокировать?
Почему вы сами создали блокировку вместо того, чтобы использовать существующий ReentrantLock, то есть mainLock?
Ответ до сих пор написан в комментариях:
Я знаю, что ты мгновенно теряешь интерес к такому большому куску английского языка.
Но не паникуйте, я возьму вас, чтобы жевать его медленно.
Первая фраза говорила прямо в точку:
Class Worker mainly maintains interrupt control state for threads running tasks.
Основной смысл существования рабочего класса заключается в поддержании прерванного состояния потока.
Поддерживаемый поток — это не обычный поток, а поток запущенных задач, то есть работающий поток.
Как понимать это «поддержание прерванного состояния потока»?
Если вы посмотрите на методы lock и tryLock класса Worker, то увидите, что каждый из них можно вызвать только в одном месте.
Метод блокировки, как мы говорили ранее, вызывается в методе runWorker.
Здесь вызывается метод tryLock:
Этот метод тоже наш старый друг, как я только что упомянул, он используется для прерывания потоков.
Какой тип потока прерывается?
Это поток, ожидающий задачи, то есть поток, ожидающий здесь:
java.util.concurrent.ThreadPoolExecutor#getTask
другими словами:Потоки, выполняющие задачи, не должны прерываться.
Как пул потоков узнает, какая задача выполняется и не должна быть прервана?
Давайте посмотрим на условия суждения:
Ключевым условием на самом деле является метод w.tryLock().
Итак, давайте взглянем на основную логику метода tryLock:
Основная логика — это операция CAS по обновлению состояния с 0 до 1. В случае успеха выполняется попытка tryLock.
Что такое "0" и "1" соответственно?
Обратите внимание, ответ все еще в примечании:
Итак, основная логика в tryLockcompareAndSetState(0, 1)
, что является блокирующей операцией.
Если tryLock не работает, в чем может быть причина?
Должно быть состояние в это время уже равно 1.
Итак, когда состояние становится 1?
В одном случае выполняется метод блокировки, который также вызывает метод tryAcquire.
Когда запирается замок?
В методе runWorker, когда задача получена и готова к выполнению.
Другими словами, рабочий процесс со статусом 1 должен быть потоком, выполняющим задачу, и его нельзя прервать.
Кроме того, начальное значение состояния установлено равным -1.
Мы можем написать простой код для проверки трех вышеуказанных состояний:
Сначала мы определяем пул потоков, а затем вызываем метод prestartAllCoreThreads, чтобы разогреть все потоки и держать их в состоянии ожидания получения задач.
Каковы статусы трех рабочих в это время?
Это должно быть 0 , разблокированное состояние.
Конечно, вы также можете увидеть такую ситуацию:
Откуда взялся -1?
Не паникуйте, я вам потом скажу, давайте сначала посмотрим, где 1?
Согласно предыдущему анализу нам нужно только отправить задачу в пул потоков:
В это время, если мы вызовем выключение, что произойдет?
Конечно, это прерывает простаивающие потоки.
А как насчет потока, выполняющего задачу?
Поскольку это цикл while, после выполнения задачи снова будет вызван метод getTask:
Метод getTask сначала оценивает состояние пула потоков.В это время вы можете определить, что пул потоков закрыт, вернуть значение null, и рабочий процесс автоматически завершится.
Ну, так много было сказано раньше, вам просто нужно запомнить одну важную предпосылку:Основная предпосылка пользовательского рабочего класса — поддерживать состояние прерывания, поскольку поток, выполняющий задачу, не должен прерываться.
Тогда прочитайте примечания ниже:
We implement a simple non-reentrant mutual exclusion lock rather than use ReentrantLock because we do not want worker tasks to be able to reacquire the lock when they invoke pool control methods like setCorePoolSize.
Вот почему старик решил сам создать рабочий класс вместо ReentrantLock.
Потому что ему нужен нереентерабельный мьютекс, а ReentrantLock реентерабельный.
Из проанализированного выше метода также видно, что это нереентерабельный метод:
Передаваемые параметры вообще не используются, и в коде отсутствует логика накопления.
Если вы не поняли, что происходит, позвольте мне показать вам логику повторного входа в ReentrantLock:
Видите ли, идет кумулятивный процесс.
Когда блокировка снимается, происходит соответствующий процесс декремента, когда он уменьшается до 0, текущий поток успешно снимает блокировку:
Вышеупомянутая логика накопления и уменьшения недоступна в рабочем классе.
Итак, снова возникает вопрос: что произойдет, если он реентерабельный?
Цель все та же, что и раньше: не нужно прерывать поток, выполняющий задачу.
В то же время в комментариях упоминается метод: setCorePoolSize.
Вы сказали, что это совпадение, что я сосредоточился на этом методе, когда писал динамическую настройку пула потоков:
Жаль, что я в основном говорил о логике в delta>0.
Теперь давайте посмотрим, где я кадрировал.
workerCountOf(ctl.get()) > corePoolSize
Что значит правда?
Это означает, что текущее количество воркеров больше, чем corePoolSize, который я хочу сбросить, и его нужно немного уменьшить.
Как его уменьшить?
Вызовите метод interruptIdleWorkers.
Мы только что проанализировали этот метод раньше, и я возьму его и взгляну на него вместе:
В нем есть tryLock, если он реентерабельный, что произойдет?
Можно ли прервать исполняющий воркер?
Это уместно?
Ну и последнее предложение на заметку:
Additionally, to suppress interrupts until the thread actually starts running tasks, we initialize lock state to a negative value, and clear it upon start (in runWorker).
Это предложение говорит о том, что для подавления прерывания до того, как поток фактически начнет выполнять задачу. Поэтому инициализируйте рабочее состояние отрицательным числом (-1).
Всем стоит обратить на это внимание: и очищать при запуске (в runWorker).
Очистите его при запуске, и это состояние с отрицательным значением.
Старик очень тактичен и указал вам методы: в runWorker.
Итак, если вы перейдете к runWorker, вы узнаете, почему сначала выполняется операция разблокировки, за которой следует комментарий разрешения прерываний:
Потому что в этом месте статус воркера все еще может быть -1, поэтому сначала разблокируйте, а статус очистите до 0.
Это также объясняет, откуда взялся -1, который я не объяснил ранее:
Разобрался, откуда взялся -1?
Именно в процессе запуска должен выполняться метод worker.add, но рабочие объекты, не успевшие выполнить метод runWorker, имеют статус -1.
Последнее слово
Ладно, смотри здесь, ставь лайк и устраивай. Написание статей утомительно и нуждается в небольшой положительной обратной связи.
Вот один для всех читателей и друзей:
Эта статья была включена в мой личный блог, играть может каждый.