Содержание

четверг, 14 апреля 2011 г.

Блокировки в java

Одно из самых полезных вещей на конференциях вроде javaone это стенды, на которых можно пообщаться с инженерами Oracle, непосредственно работающими над JVM. Самый интересный стенд (вернее люди, которые там находились) на только что прошедшей конференции javaone в Москве был "Java Performance". Там мне удалось услышать краткий обзор стратегий блокировки потоков применяемой в HotSpot на данный момент. Дабы не забыть эту весьма интересную и малоосвещенную в литературе информацию, я решил написать данный топик.

Виды блокировок

На данный момент есть три различных видов блокировок: biаsed, thin and fat. Начнем в историческом порядке их появления.
1. Fat блокировка
Самый очевидный и простой вариант, основанный на использовании мютекса операционной системы. Применим в любых условиях, включая случай состязательные блокировок в нескольких потоках.
2. Thin блокировка
Если же состязательной блокировки практически нет, то можно придумать кое-что побыстрее мьютекса, так как будить в этом случае потоки не нужно и можно обойтись без скедульера операционной системы. Так были придуманы thin блокировки использующие CAS операции для гарантирования эксклюзивного доступа.
3. Biased блокировки
На некоторых архитектурах, особенно очень многопроцессорных, CAS операции не очень-то дешевы, поэтому для случая работы с объектом в одного потоке были придуманы biased блокировки, которые не требуют никаких CAS операций. На самом требуется одна CAS операция для выставления типа блокировки при первой попытке её захвата.

Таким образом для состязательных блокировок со многими потоками могут использоваться только fat блокировки, для не состязательных блокировок многими потоками выгоднее использовать thin локи и для работы с одним потоком выгоднее использовать biased блокировки.

Порядок выбора блокировок HotSpot

При работе JVM переход от одного вида блокировки к другому происходит в обратном описанному порядке. Т.е. стратегия на данный момент оптимистическая. При первом захвате блокировки, JVM надеется, что данная блокировка будет использоваться только одним потоком, и использует biased блокировку. Если же данную блокировку пытается захватить другой поток, то biased блокировка конвертируется в fat блокировку в случае состязательного захвата или в thin блокировку в случае не состязательного. Если блокировка была сконвертирована в thin и потом наступил состязательный захват из другого потока, то возможно он немного покрутиться в CAS и если не захватит блокировку через совсем короткий промежуток времени, то блокировка сконвертируется в fat, чтобы можно было пользоваться возможностью скедьюлинга потоков операционной системой.

Применение данного сакрального знания

Казалось бы, какая разница программисту пишущему прикладной код от того, какую блокировку выберет JVM? Например, такой случай. В вашем приложении работа с каким-то объектом выполняется без состязания то одним, то другим потоком. Вы поставили синхронизацию и забыли об этом. Однако если вы знаете о типах блокировок, то вы, возможно, попытаетесь сделать так, чтобы работа с данным объектом практически всегда выполнялась одним и тем же потоком, тем самым JVM сможет задействовать biased блокировку вместо thin. Конечно же я не призываю на этом заморачиваться, но если во время реализации любой из подходов занимает одинаковые усилия, то почему бы сразу не сделать так, чтобы JVM могла использовать более эффективную блокировку.

Второй случай, когда об этом полезно знать, был приведем на одном из докладов javaone. Проводился микро бэнчмарк сравнения производительности synchronized метода и метда с тем же содержимым, все тело которого заключено в synchronozed блок на this.
Сам тест был в виде метода main(), внутри которого сначала вызывался N-ое количество раз метод m1(), затем метод m2(). Результат - различие производительности в несколько раз. Объяснение же этому весьма простое - biased locking оптимизация включается не сразу, а после некоторой задержки, как стартовала JVM. Если тест запускать с параметром -XX:BiasedLockingStartupDelay=0, то время работы методов примерно совпадает.

15 комментариев:

  1. У меня другой вопрос, который меня мучает уже несколько лет =)

    Вот ты пишешь мьютексы системные в fat блокировках. А где джава столько сьютексов берет? Это же ограниченный ресур для ОС, их там десяток тысяч, сотня, но не сильно больше ...

    ОтветитьУдалить
  2. Чувах рассказывал про какую-то таблицу мьютексов, что создает еще дополнительные расходы. Может она из себя некий пул представляет...

    ОтветитьУдалить
  3. А я тебя обманул, похоже: http://linux.die.net/man/3/pthread_mutex_init нет лимита. Странно, я был уверен, что есть.

    ОтветитьУдалить
  4. >А где джава столько сьютексов берет?

    А сколько -- "столько"? В обычных джава-приложениях не особо много блокировок используется. Плюс разные оптимизации JVM - устранение блокировок в результате esacape analyze, внутри-JVM-ные блокировки, вроде описанных выше -- реальных-то fat-блокировок, конвертируемых в мьютексы -- мало.

    ОтветитьУдалить
  5. ну вот положим, у нас много объектов, с методами, которые помечены synchronized - если нет biased & thin locking, то, как я понимаю, для каждого из объектов нужно у ОС попросить свой мьютекс

    ОтветитьУдалить
  6. В реальности синхронизация на объектах -- явление не частое, поэтому мьютексов создаётся мало.

    ОтветитьУдалить
  7. не совсем понятно как делается Biased блокировка, если там даже CAS не используется в обычном режиме, как тогда определяется вмешательство другого потока? на пальцах еси можно )

    ОтветитьУдалить
  8. На самлм деле одна-то CAS инструкция используется, как я нарисал в посте, а именно, используется она чтобы в самом начале когда первый поток пытается захватить монитор в заголовок обьекта монитора записать ID потока. Именно здесь единственный CAS гарантирует, что запишет туда свой ID только один поток. Второй поток увидит, что в заголовке монитора уже пропмсан ID другого потока и блокировка будет преобразована в более сильную.
    Еще мне тут умные люди подсказывают, что это будет работать только при условии, что у обьекта монитора не вызывается нативный хешкод, так как он сохраняется в то же место, что и ID, таким образом, в данном случае будет исползоваться толькоблокировка на мьютексах.

    ОтветитьУдалить
  9. т.е. нативный хешкод хранится в заголовке объекта? и интересно, зачем его перетирает id потока, писать чтоль больше некуда...
    Где бы в спеках почитать это не в курсе?

    ОтветитьУдалить
  10. а вот, у Руслана это есть: http://cheremin.blogspot.com/2011/04/how-caching-affects-hashing.html

    ОтветитьУдалить
  11. Илья, ты не на тот пост Руслана ссылку даешь.

    Конечная ссылка - это блог сана http://blogs.sun.com/dave/entry/biased_locking_in_hotspot. Я правда сам об этом не читал. Мне рассказал Илья, прочитав об этом на блоге Руслана (http://cheremin.blogspot.com/2011_04_13_archive.html). Вот такая вот цепочка :)

    ОтветитьУдалить
  12. >т.е. нативный хешкод хранится в заголовке объекта? и интересно, зачем его перетирает id потока, писать чтоль больше некуда...

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

    А поскольку редко так бывает, чтобы у объекта использовался _и_ identity hash code, _и_ этот объект выступал монитором синхронизации -- есть смысл использовать это же место и для хранения признака привязки.

    ОтветитьУдалить
  13. Решил проверить и получил почти одинаковые результаты для вызовов m1 и m2.
    Вот код:
    http://snipt.org/uJp4

    ОтветитьУдалить
  14. Андрей, простите, что Вы пытались проверить?

    Идея данного примера была в том, что запуск бенчмарка двух методов m1 и m2 нельзя запускать последовательно. Вы же в своем примере, на сколько я понял, запускаете эти бенчмарки отдельно.

    ОтветитьУдалить
  15. Я тут на досуге почитал http://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf. После чего хочу сделать уточнение: если вы посчитали identity hash code у объекта, то перестает работать только biased оптимизация, thin блокировки все еще работают, так как они хедер объекта копируют в объект лока.

    Еще там немного другая терминология thin лок называется lightweight, а fat лок - inflated.

    ОтветитьУдалить