Нить, которую разбудит HotSpot
Предположим, что одна нить вашей программы только что закончила работать внутри секции synchronized, а другие нити уже тут как тут: ждут, не дождутся, пока сами в synchronized к тому же объекту смогут попасть.
Что должен сделать HotSpot в такой ситуации? —
- Разбудить всех ожидающих или
- выбрать и разбудить одного
Очевидно, что первый вариант хорош для блокировок типа читатели/писатели (read/write или shared/exclusive)—там где вы создаете, скажем, запись в кэше в режиме exclusive и потом уведомляете всех, что запись готова и можно ее читать. Но в Java блокировки взаимоисключающие. Если мы разбудим все нити, победа достанется одной, а все другие тронутся и тут же заглохнут на блокировке. Эта ситуация называется futile wakeup (см. раздел 3.3 в этой статье).
Получается, что надо выбрать одного. И снова выбор: кого?
- Того, кто встал в очередь первым (с учетом приоритета) или
- последнего?
Если бы разработчики планировщиков были благородными людьми, то первый вариант был единственно возможным. Но зачем же тогда потребовался второй вариант?
А нужен он, чтобы кэш процессора "не остыл". Это значит, что скорее всего кэши данных, команд и TLB процессора, на котором выполнялась нить содержат данные этой нити и выбор ее позволит избежать перезагрузки кэшей. Это скажется на быстродействии самой нити, трафике межпроцессорных шин и шины памяти. Планировщик пытается найти здоровый баланс между распределением времени между всеми нитями и оптимальностью использования кэшей. Если же нить выполнялась достаточно давно, то можно не заботится о кэшах и помещать нить на наиболее свободный процессор.
Однако, HotSpot знает в этой ситуации больше, чем планировщик. Как правило, блокировками защищаются наборы данных и можно быть уверенным в том, что процесс, перехватывающий блокировку у другого, будет работать с теми же областями памяти, что и его предшественник. Таким образом, планировщику можно дать "подсказку" о том, на каком процессоре лучше разбудить нить. Исключается не только перезагрузка кэшей, но и "переход" модифицированных строк кэшей к другому процессору. Таким образом, Магомет, как процесс идет к горе данных, а не наоборот. Но и здесь не стоит забывать о разумном балансе нитей в очередях к процессорам.
Этот метод может особенно улучшать быстродействие на NUMA-архитектурах и совершенно не менять быстродействие на процессорах типа Niagara, имеющих общий L2$1 для всех ядер.
В данный момент реализация данной подсказки в HotSpot только планируется.
Надо сказать, что в HotSpot реализовано несколько дисциплин выбора следующей нити. Переключать их можно следующей опцией:
$ java -XX:SyncKnobs=QMode=x …
Где x—это номер дисциплины. "Честные" дисциплины—1 и 3. Их реализацию можно посмотреть в исходниках.
А убедиться, что это работает на практике можно с помощью следующей программы:
import java.util.concurrent.CyclicBarrier;
public class qMode {
public static final int N = 5;
private static class T extends Thread {
private int _n;
private CyclicBarrier _barrier;
public T(int n, CyclicBarrier b) {
_n = n;
_barrier = b;
}
public void run() {
try {
_barrier.await();
Thread.sleep(_n * 10);
while ( true ) {
synchronized ( T.class ) {
System.out.print(_n);
Thread.sleep(100);
}
}
} catch ( Exception e ) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(N + 1);
for ( int i = 0; < N; i++ ) {
new T(i, barrier).start();
}
try {
barrier.await();
} catch ( Exception e ) {
e.printStackTrace();
}
}
}
Кирилл Широков
1Игра слов: $ ⇒ cash ⇒ cache
опубликовал vmrobot ( дек 11 2007, 03:21:24 PM MSK ) Permalink Комментарии [3]

Офигенная статья. Желаю "не сдуться" и продолжать в том же духе
опубликовал chand0s Декабрь 12, 2007 at 01:43 PM MSK #
Первый выбор (между побудкой одного и всех) был предложен для умственно отсталых, я так полагаю.
А второй выбор, между FIFO и LIFO, не так очевиден. Благородство оно и в африке оказывается благородством. Да, может последний процесс, ставший в очередь, ещё лежит в кэше, и выполнится быстрее. Но какова вероятность этого? Вероятность застопориться на коротких операциях достаточно мала - именно поэтому монитор так успешно заменятся спинлоком, и это даёт столь заметное ускорение синхронизации в яве. Если же несколько тредов застряли на мониторе - в подавляющем большинстве случаев это длинная операция. И поскольку это длинная операция, и длинное ожидание - в кэше от данных/кода ждущих тредов мало что останется, не зависимо от очерёдности их остановки. Что же мы имеем в результате? Мы имеем увеличение производительности на какие-то проценты (и бодрый рапорт, что теперь наша машинка ещё на миллиметр быстрее конкурента). Но в качестве побочного эффекта этого подхода мы имеем ухудшение обслуживания клиентов в случае большой нагрузки на сервер. При FIFO очереди все задачи будут медленно, но двигаться. При LIFO очереди мы будем иметь постепенно накапливающиеся ждущие треды. Одни треды будут выполнятся быстро, а остальные будут просто стоять. Вместо тянучки получится мёртвая пробка.
В общем, умение держать критическую нагрузку (фактически показатель качества продукта) опять принесено в жертву маркетингу (пылью в глаза в веде бенчмарков). Как обычно.
опубликовал Maxim Kizub Декабрь 12, 2007 at 11:33 PM MSK #
Не совсем тогда понятно, зачем в java реализовано notifyAll(), все-равно реально-то блокирует только один. Кстати, говоря об оптимизациях, интересно все-же знать, оценку прироста производительности.
опубликовал null Декабрь 17, 2007 at 06:44 PM MSK #