Каковы некоторые из наименее известных фактов и вариантов использования для потоков Java?
Некоторые люди любят альпинизм, другие занимаются скайдайвингом. Я люблю Java. Мне нравится в этом то, что ты никогда не перестаешь учиться. Инструменты, которые вы используете ежедневно, часто могут открыть им совершенно новую сторону, с методами и интересными сценариями использования, которые вы еще не видели. Как темы например. Актуальные темы. Или, лучше сказать, сам класс Thread. Параллельное программирование никогда не прекращает создавать проблемы, когда мы имеем дело с системами с высокой масштабируемостью, но теперь мы поговорим о чем-то немного другом.
В этом посте вы увидите некоторые из менее известных, но полезных техник и методов, которые поддерживают потоки. Если вы новичок, опытный пользователь или опытный Java-разработчик, попробуйте узнать, что из этого вы уже знаете, а что новшество для вас. Есть ли что-то еще о темах, о которых вы считаете нужным упомянуть? Я хотел бы услышать об этом в комментариях ниже. Давайте начнем.
начинающий
1. Названия тем
У каждого потока в вашем приложении есть имя, простая строка Java, которая генерируется для него при создании потока. Значения имен по умолчанию изменяются от «Thread-0» до «Thread-1», «Thread-2» и так далее. Теперь перейдем к более интересной части: потоки предоставляют два способа задания имен:
1. Конструкторы потоков, вот самый простой:
1
2
3
4
5
6
7
8
9
|
class SuchThread extends Thread { Public void run() { System.out.println ( "Hi Mom! " + getName()); } } SuchThread wow = new SuchThread( "much-name" ); |
2. Установщик имени потока:
1
|
wow.setName(“Just another thread name”); |
Да, имена потоков изменчивы. Таким образом, кроме установки собственного имени при создании его экземпляра, мы можем изменить его во время выполнения. Само поле имени задается как простой объект String. Это означает, что он может содержать до 2 ¹-1 символов (Integer.MAX_VALUE). Более чем достаточно, я бы сказал. Обратите внимание, что это имя не действует как уникальный идентификатор, поэтому потоки могут иметь одно и то же имя. Кроме того, не пытайтесь передать null в качестве имени, если только вы не хотите, чтобы было выдано исключение (хотя «null» — это нормально, я не осуждаю!).
Использование имен потоков для отладки
Так что теперь, когда у вас есть доступ к именам потоков, следование некоторым собственным соглашениям об именах может значительно облегчить вашу жизнь, когда происходит что-то плохое. «Тема 6» звучит немного бессердечно, я уверен, что вы можете придумать лучшее имя. Соедините это с самостоятельно назначенным идентификатором транзакции при обработке пользовательских запросов, добавьте его к имени потока, и вы значительно сократите время устранения ошибок.
Хорошей практикой здесь является то, что вы должны генерировать UUID в каждой точке входа потока в ваше приложение и поддерживать его согласованным, когда запрос проходит между вашими узлами, процессами и потоками. Давайте посмотрим на этот пример, один из рабочих потоков в определенном пуле потоков зависает слишком долго. Вы запускаете jstack, чтобы посмотреть поближе, и затем вы видите это:
1
2
|
“pool-1-thread-1″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800 nid=0x6d03 in Object.wait() [0x000000013ebcc000] |
Хорошо, «пул-1-нить-1», почему так серьезно? Давайте узнаем вас поближе и подумаем о более подходящем имени:
1
|
Thread.currentThread().setName(Context + TID + Params + current Time, ...); |
Теперь, когда мы снова запустим jstack, все выглядит намного ярче:
1
2
3
4
|
”Queue Processing Thread, MessageID: AB5CAD, type : AnalyzeGraph, queue: ACTIVE_PROD, Transaction_ID: 5678956, Start Time: 30 /12/2014 17:37″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800 nid=0x6d03 in Object.wait() [0x000000013ebcc000] |
Мы знаем, что делает поток, когда он застрял, и у нас также есть идентификатор транзакции, который запустил все это. Вы можете проследить ваши шаги, воспроизвести ошибку, изолировать и устранить ее. Чтобы узнать больше о крутых способах использования jstack, вы можете проверить этот пост прямо здесь .
2. Приоритеты потоков
Еще одно интересное поле темы имеет приоритет. Приоритет потока — это значение от 1 (MIN_PRIORITY) до 10 (MAX_PRIORITY), а значением по умолчанию для вашего основного потока является 5 (NORM_PRIORITY). Каждый новый поток получает приоритет своего родителя, поэтому, если вы не играете с ним вручную, все ваши приоритеты потока, вероятно, установлены на 5. Это также часто пропускаемое поле класса Thread, и мы можем обращаться к нему и манипулировать им. через методы getPriority () и setPriority () . Нет способа установить это в конструкторе потока.
Кому нужны приоритеты?
Конечно, не все потоки созданы равными, некоторые требуют немедленного внимания вашего процессора, в то время как другие — просто фоновые задачи. Приоритеты используются, чтобы сигнализировать об этом планировщику потока ОС. В Takipi, где мы разрабатываем инструмент отслеживания и анализа ошибок, поток, который обрабатывает новые исключения для наших пользователей, получает MAX_PRIORITY, в то время как потоки, которые обрабатывают такие задачи, как отчетность о новых развертываниях, имеют более низкий приоритет. Можно ожидать, что потоки с более высоким приоритетом получают больше времени от планировщика потоков, работающего с вашей JVM. Ну, это не всегда так.
Каждый поток Java открывает новый собственный поток на уровне ОС, а установленные вами приоритеты Java по-разному переводятся в собственные приоритеты для каждой платформы. В Linux вам также нужно будет включить флаг «-XX: + UseThreadPriorities» при запуске приложения, чтобы они учитывались. С учетом вышесказанного приоритеты потоков — это всего лишь рекомендации, которые вы предоставляете. По сравнению с собственными приоритетами Linux, они даже не охватывают весь спектр значений (1..99, а эффекты потоков, которые колеблются между -20..20). Основным выводом является важность сохранения вашей собственной логики, которая обеспечит отражение ваших приоритетов во времени ЦП, которое получает каждый поток, но не рекомендуется полагаться исключительно на приоритеты.
продвинутый
3. Поток локального хранилища
Это немного отличается от других существ, о которых мы говорили здесь. ThreadLocal — это концепция, реализованная вне класса Thread ( java.lang.ThreadLocal ), но хранящая уникальные данные для каждого потока. Как сказано в олове, он предоставляет вам локальное хранилище потоков, то есть вы можете создавать переменные, которые являются уникальными для каждого экземпляра потока. Подобно тому, как у вас будет имя или приоритет потока, вы можете создавать настраиваемые поля, которые действуют так, как если бы они были членами класса Thread. Разве это не круто? Но давайте не будем слишком взволнованы , впереди есть несколько предостережений.
Рекомендуется создать ThreadLocal одним из двух способов: либо как статическая переменная, либо как часть синглтона, где он не должен быть статическим. Обратите внимание, что он живет в глобальной области, но действует локально для каждого потока, который может получить к нему доступ. Вот пример переменной ThreadLocal, содержащей нашу собственную структуру данных для легкого доступа:
1
2
3
4
5
6
7
8
|
public static class CriticalData { public int transactionId; public int username; } public static final ThreadLocal<CriticalData> globalData = new ThreadLocal<CriticalData>(); |
Получив ThreadLocal в наших руках, мы можем получить к нему доступ с помощью globalData.set () и globalData.get () .
Глобальный? Должно быть зло
Не обязательно. Переменная ThreadLocal может хранить идентификатор транзакции. Это может пригодиться, когда у вас есть неперехваченное исключение, переполняющее ваш код. Хорошей практикой является наличие UncaughtExceptionHandler , который мы также получаем с классом Thread, но должны реализовать сами. Как только мы достигаем этой стадии, не так много намеков на то, что на самом деле привело нас туда. Мы остались с объектом Thread и не можем получить доступ ни к одной из переменных, которые отправляют нас туда, когда кадры стека закрываются. В нашем UncaughtExceptionHandler, когда поток делает последние вдохи, ThreadLocal является в значительной степени единственной вещью, которую мы оставили.
Мы можем сделать что-то в духе:
1
|
System.err.println( "Transaction ID " + globalData.get().transactionId); |
И просто так мы добавили ценный контекст к ошибке. Одним из наиболее творческих способов использования ThreadLocal является выделение выделенного фрагмента памяти, который снова и снова используется рабочим потоком в качестве буфера. Это может стать полезным, в зависимости от того, на какой стороне вы находитесь в памяти, и, конечно, на компромиссы ЦП. Тем не менее, на что нужно обратить внимание, так это на злоупотребление пространством нашей памяти. ThreadLocal существует для определенного потока, пока он жив и не будет собирать мусор, пока вы не освободите его или поток не прекратит работу. Так что лучше будьте осторожны, когда используете его, и сохраняйте его простым.
4. Пользовательские потоки и потоки демонов
Вернуться к нашему классу Thread. Каждая нить в нашем приложении получает статус пользователя или демона. Другими словами, передний план или фоновый поток. По умолчанию основной поток является потоком пользователя, и каждый новый поток получает статус потока, который его создал. Таким образом, если вы установите поток как Daemon, все потоки, которые он создает, также будут помечены как daemon. Когда единственные потоки, запущенные в вашем приложении, имеют статус Daemon, процесс закрывается. Чтобы поиграть, проверить и изменить состояние потоков, у нас есть методы Boolean .setDaemon (true) и .isDaemon () .
Когда бы вы установили нить Daemon?
Вы должны изменить статус потока на Daemon, когда не важно, чтобы он завершился, чтобы процесс мог закрыться. Это избавляет от необходимости правильно закрывать поток, останавливая все сразу и позволяя быстро закончить. С другой стороны, когда есть поток, который выполняет операцию, которая должна завершиться должным образом, иначе произойдет что-то плохое, убедитесь, что он установлен как поток пользователя. Важной транзакцией может быть, например, запись в базе данных или завершение обновления, которое не может быть прервано.
эксперт
5. Сходство Java-процессоров
Эта часть приближает нас к аппаратному обеспечению, где код соответствует металлу. Привязка к процессору позволяет привязывать потоки или процессы к конкретным ядрам процессора. Это означает, что всякий раз, когда этот конкретный поток выполняется, он будет работать исключительно на одном определенном ядре. Обычно случается так, что планировщик потоков ОС будет выполнять эту роль в соответствии со своей собственной логикой, возможно, с учетом приоритетов потоков, которые мы упоминали ранее.
Козырем здесь является кеш процессоров. Если поток будет работать только на одном конкретном ядре, более вероятно, что он получит удовольствие от того, что все его данные готовы для него в кеше. Когда данные уже есть, их не нужно перезагружать. Можно сэкономить микросекунды, которые вы сэкономите, и код будет фактически работать в это время, лучше используя выделенное им время процессора. Хотя некоторые оптимизации существуют на уровне ОС, и архитектура оборудования также играет важную роль, использование сходства может исключить вероятность переключения потоков между ядрами.
Поскольку здесь задействованы многие факторы, лучший способ определить, как сродство процессора повлияет на вашу пропускную способность, — это принять привычку тестирования. Хотя это не всегда может быть значительно лучше, одним из преимуществ, которые вы можете получить, является стабильная пропускная способность. Стратегии сродства могут опуститься до хирургического уровня, в зависимости от того, что можно получить. Индустрия высокочастотной торговли была бы одним из тех мест, где подобные вещи наиболее важны.
Тестирование сродства процессора
Java не имеет встроенной поддержки сходства процессоров, но это, конечно, не конец истории. В Linux мы можем установить сходство процессов с помощью команды taskset . Допустим, у нас запущен Java-процесс, и мы хотим привязать его к определенному процессору:
1
|
taskset -c 1 “java AboutToBePinned” |
Или, если он уже запущен:
1
|
taskset -c 1 <PID> |
Теперь, чтобы перейти к уровню потока, нам нужно вставить новый код. К счастью, есть библиотека с открытым исходным кодом, которая поможет нам сделать это: Java-Thread-Affinity . Написанный Питером Лоури из OpenHFT, это, вероятно, самый простой способ сделать это. Давайте посмотрим на быстрый пример закрепления потока, больше этого доступно в репозитории библиотеки GitHub:
1
|
AffinityLock al = AffinityLock.acquireLock(); |
Вот и все. На GitHub доступны более продвинутые варианты получения блокировки — с учетом разных стратегий выбора конкретного ядра.
Вывод
Мы видели 5 способов посмотреть на потоки: имена потоков, локальное хранилище потоков, приоритеты, потоки демонов и сходство. Надеюсь, что это помогло пролить новый свет на вещи, с которыми вы сталкиваетесь ежедневно, и был бы рад услышать ваши комментарии! Какие другие методы обработки потоков могут вписаться?
Ссылка: | Волшебные трюки с нитями: 5 вещей, которые вы никогда не знали, что можете сделать с потоками Java от нашего партнера JCG Алекса Житницкого в блоге Takipi . |