JEP draft: Race exclusion for confined data
Исключение гонок для данных с ограниченным доступом
| Ответственный | John Rose |
| Тип | Feature |
| Область | JDK |
| Статус | Draft |
| Компонент | hotspot / runtime |
| Трудоёмкость | L |
| Длительность | L |
| Создан | 2017/05/19 07:49 |
| Обновлён | 2020/02/27 22:54 |
| Задача | 8180647 |
(ЧЕРНОВИК ЧЕРНОВИК ЧЕРНОВИК)
Summary
Механизм синхронизации Java требует, чтобы в заголовке каждого объекта в куче (включая массивы Java) был монитор. Монитор используется лишь небольшой долей всех объектов. Семантика, закреплённая в JVMS и JLS, слишком негибкая для некоторых сценариев использования, а во многих других случаях дополнительное слово заголовка просто тратит место. Для третьих сценариев монитор, наоборот, слишком слаб, так как JMM допускает гонки доступа к памяти, сводящие синхронизацию на нет.
Мы предлагаем использовать монитор объекта более гибко и, в зависимости от класса, разрешить либо расширять его, либо полностью убирать, чтобы поддержать более широкий круг сценариев использования. В частности, мы будем поддерживать сценарии ограничения одним потоком, постоянной неизменяемости и эпизодической неизменяемости, предоставив «точку подключения» для исключения гонок доступа к соответствующим образом настроенным объектам.
Description
Объект, находящийся под синхронизацией, принадлежит потоку, но его public-поля без final (а если это массив, то и его элементы) по-прежнему подвержены конкурирующим записям.
Точно так же, если объект служит дескриптором ресурсов памяти вне кучи (как DirectByteBuffer), он может предотвратить гонки доступа к самому себе, обеспечив однопоточный доступ, но не может предотвратить гонки доступа к своей памяти вне кучи без необычных мер, например захвата монитора при каждом обращении, — мер, которые часто отвергают как слишком дорогие. Из-за возможности конкурирующего доступа к освобождённой памяти мы не можем создать безопасный API для освобождения ресурсов памяти вне кучи, а это серьёзная и постоянно возникающая проблема для многих клиентов, работающих с приложениями, интенсивно использующими память.
Нам нужен новый вид блокировки со следующими характеристиками:
- Изменяемые переменные, как поля, так и элементы массивов, можно объявить блокируемыми. (Их можно рассматривать как более структурированные volatile-переменные.)
- Конкурирующие обращения к блокируемым переменным заблокированного объекта выбрасывают ошибку.
- По крайней мере у некоторых массивов (для каждого типа элементов) элементы будут блокируемыми. Поскольку при любом обращении к массиву должна выполняться проверка границ, представляется разумным наделить блокируемостью их все.
- Объект (или массив) можно заблокировать как замороженный: тогда любая запись считается гонкой, а чтение никогда не считается гонкой.
- Объект (или массив) можно заблокировать за потоком-владельцем: тогда любое обращение (чтение или запись) считается гонкой, кроме обращений из потока-владельца.
- Объект (или массив) можно заблокировать за терминалом-владельцем — объектом специального класса, который не является потоком: тогда любое обращение (чтение или запись) считается гонкой.
- Есть безопасный атомарный API передачи, позволяющий передать заблокированный объект от одного владельца (потока или терминала) другому, либо перевести его в замороженное или в разблокированное состояние.
- API передачи обеспечивает линейную цепочку владения для любого объекта с блокируемыми переменными.
- Блокируемое поле в разблокированном состоянии можно читать или записывать в любой момент, но гонки относительно других состояний поля исключаются микроблокировкой (например, seq-lock). (Поэтому в заблокированном состоянии блокируемое поле эффективнее, чем в разблокированном.)
- Таким образом, блокируемые поля value-типов, которые (a) объявлены неразрываемыми и (b) слишком велики, чтобы поместиться в атомарно обновляемую единицу памяти, будут выполнять свой контракт атомарности.
- Неблокируемые поля таких value-типов будут либо запрещены, либо обрабатываться с помощью дополнительных состояний блокировки содержащего их объекта.
- Объекты с блокируемыми полями можно будет создавать, как обычно, конструкторами; в теле конструктора объект будет заблокирован за текущим потоком, но при нормальном возврате из конструктора объект будет переведён в некоторое заданное выбранное состояние, например замороженное, заблокированное за заданным терминалом или заблокированное за каким-либо потоком. Какие из этих состояний доступны клиентам, будет определять сам класс объекта.
Состязание за переменную можно обрабатывать двумя способами: блокированием или выбрасыванием исключения. Существующая синхронизация Java поддерживает блокирование, а значит, состязание должно активно разрешаться между текущим владельцем и пулом ожидающих потоков, и делать это нужно при каждом освобождении блокировки. Из-за этого и захват, и освобождение блокировки очень дорого реализовать: требуется межпроцессорное взаимодействие (через обращения к памяти с барьерами или volatile-обращения).
Выбрасывание исключения, напротив, сразу устраняет конкурирующие потоки. Оно заставляет программиста продумать структуру состязания на верхнем уровне программы и развернуть меньшее число явно запрограммированных блокирующих мониторов, чтобы управлять потребностями во взаимоисключении всех объектов программы.
Если состязание обрабатывается выбрасыванием исключения, операция monitor-enter сводится к проверке того, что в заголовке объекта ожидаемый битовый шаблон, соответствующий текущему потоку. (Если объект не заморожен, при чтении тоже нужно проверять этот битовый шаблон.) Для monitor-exit ничего делать не нужно (1+0).
Поскольку заголовок объекта не может перейти из допустимого состояния в недопустимое без того, чтобы это изменение сделал текущий поток, проверку блокировки легко объединять и выносить из циклов, так же как проверки на null или проверки границ.
(Это свойство выносимости очень сильное: оно позволяет дешёвый, но безопасный доступ к данным, но при этом сильно ограничивает допустимые переходы между состояниями блокировки. Вероятно, после того как объект сделан неизменяемым, его состояние блокировки больше никогда не может измениться.)
Выносимая проверка блокировки достаточно дешева, чтобы оправдать создание нового класса блокируемых полей, перед каждым обращением к которым выполняется проверка состязания с выбрасыванием исключения. Конечно, некоторые пользователи по-прежнему будут предпочитать «голые» изменяемые поля, но необходимость делать защитные копии объектов, подверженных гонкам, в некоторых случаях окажется обременительнее, чем позволить JVM исключать гонки, управляя блокировками полей.
Вспоминая biased locking: в какой-то момент многим казалось, что техника смещённой блокировки (biased locking) может сделать синхронизацию Java настолько дешёвой, что её можно будет использовать свободно. Из этого опыта мы узнали, что для объектов, которыми стабильно «владеет» один поток, все стороны действительно могут легко проверить, что они находятся в этом состоянии. Мы также узнали, что отзыв блокировки может быть настолько дорогим, что вся схема становится непрактичной. Отзыв блокировки должен происходить, когда объект X смещён к потоку T1, которому он больше не нужен, а другой поток T2 пытается получить к нему доступ. Чтобы передать блокировку T2, поток T1 нужно прервать и привести в состояние, доказуемо безопасное относительно X. В общем случае для этого требуется обойти стек T1, чтобы найти оставшиеся ссылки на X, которые логически могут быть синхронизированы за T1. Проблема здесь не в смещённом владении, а в модели для пользователя, в которой передача от T1 к T2 неявна и может произойти между любыми двумя эпизодами синхронизации в T1. Новая форма блокировки объектов устраняет проблемы смещённой блокировки, сохраняя её преимущества.
Блокировка полей взаимодействует с классической синхронизацией Java. Когда новый поток входит в монитор объекта (monitor-enter), он запрашивает передачу у прежнего владельца объекта. Когда прежний владелец удовлетворяет запрос, новый поток становится владельцем объекта, и все блокируемые поля становятся доступны исключительно новому потоку. При выходе из монитора (monitor-exit) владение возвращается прежнему владельцу (если это был терминал) или, возможно, объект переходит в другое состояние, определяемое классом объекта. Во время критической секции поток-владелец может (и должен) назначить последующее состояние блокировки для заблокированного объекта. Это состояние выбирается из следующих:
- другой терминал
- другой конкретный поток
- ожидающий поток, выбранный JVM
- предыдущий терминал или поток (если был)
- разблокированное состояние
- замороженное состояние
С точки зрения состояний блокировки операция monitor-exit похожа на возврат из конструктора. В обоих случаях происходит контролируемая передача от текущего потока (который либо создаёт новый объект, либо изменяет существующий) следующему владельцу.
Будет предоставлен стандартный терминал, у которого нет особых свойств, но который предоставляет доступ любому потоку. Блокировка объекта за этим терминалом временно запрещает любое чтение и запись, пока какой-нибудь поток не заберёт объект и не сделает с ним что-то другое.