DCL с использованием ThreadLocal By Брайан Гетц @ JavaWorld
что не так в DCL?
DCL полагается нанесинхронизированное использование поля ресурса.Это кажется безвредным, но это не так.Чтобы понять почему, представьте, что поток A находится внутри синхронизированного блока, выполняя инструкцию resource = new Resource ();в то время как поток B только вводит getResource ().Рассмотрим влияние на память этой инициализации.Память для нового объекта Resource будет выделена;будет вызван конструктор для Resource, инициализирующий поля-члены нового объекта;и полевому ресурсу SomeClass будет назначена ссылка на вновь созданный объект.
class SomeClass {
private Resource resource = null;
public Resource getResource() {
if (resource == null) {
synchronized {
if (resource == null)
resource = new Resource();
}
}
return resource;
}
}
Однако, поскольку поток B не выполняется внутри синхронизированного блока, он может видеть эти операции с памятью в другом порядке, чемодин поток A выполняется.Это может быть случай, когда B видит эти события в следующем порядке (и компилятор также может свободно переупорядочивать такие инструкции): выделять память, назначать ссылку на ресурс, вызывать конструктор.Предположим, что поток B появляется после того, как память была выделена и поле ресурса установлено, но до вызова конструктора.Он видит, что ресурс не равен нулю, пропускает синхронизированный блок и возвращает ссылку на частично созданный ресурс!Излишне говорить, что результат не является ни ожидаемым, ни желаемым.
Может ли ThreadLocal помочь исправить DCL?
Мы можем использовать ThreadLocal для достижения явной цели идиомы DCL - ленивыйинициализация без синхронизации по общему пути кода.Рассмотрим эту (поточно-ориентированную) версию DCL:
Листинг 2. DCL с использованием ThreadLocal
class ThreadLocalDCL {
private static ThreadLocal initHolder = new ThreadLocal();
private static Resource resource = null;
public Resource getResource() {
if (initHolder.get() == null) {
synchronized {
if (resource == null)
resource = new Resource();
initHolder.set(Boolean.TRUE);
}
}
return resource;
}
}
Я думаю;здесь каждый поток однажды входит в блок SYNC для обновления значения threadLocal;тогда не будет.Таким образом, ThreadLocal DCL обеспечит вход потока только один раз в блок SYNC.
Что на самом деле означает синхронизация?
Java обрабатывает каждый поток так, как если бы он выполнялся на своемсобственный процессор со своей локальной памятью, каждый из которых общается и синхронизируется с общей оперативной памятью.Даже в однопроцессорной системе эта модель имеет смысл из-за влияния кэшей памяти и использования регистров процессора для хранения переменных.Когда поток изменяет местоположение в своей локальной памяти, эта модификация в конечном итоге также должна отображаться в основной памяти, и JMM определяет правила, когда JVM должна передавать данные между локальной и основной памятью.Архитекторы Java поняли, что чрезмерно ограниченная модель памяти серьезно подорвет производительность программы.Они попытались создать модель памяти, которая позволила бы программам хорошо работать на современном компьютерном оборудовании, в то же время предоставив гарантии, которые позволили бы потокам взаимодействовать предсказуемым образом.
Основным инструментом Java для предсказуемого представления взаимодействий между потоками является синхронизированныйключевое слово.Многие программисты думают о синхронизации строго с точки зрения применения семафора взаимного исключения (мьютекса) для предотвращения выполнения критических секций более чем одним потоком одновременно.К сожалению, эта интуиция не полностью описывает, что означает синхронизированный.
Семантика синхронизированных действительно включает взаимное исключение выполнения на основе статуса семафора, но они также включают в себя правила взаимодействия потока синхронизации с основной памятью.В частности, получение или снятие блокировки вызывает барьер памяти - принудительную синхронизацию между локальной памятью потока и основной памятью.(Некоторые процессоры, такие как Alpha, имеют явные машинные инструкции для выполнения барьеров памяти.) Когда поток выходит из синхронизированного блока, он выполняет барьер записи - он должен сбросить все переменные, измененные в этом блоке, в основную память перед освобождениемзамок.Аналогично, при входе в синхронизированный блок он выполняет барьер чтения - это как если бы локальная память была признана недействительной, и он должен извлекать любые переменные, на которые будут ссылаться в блоке, из основной памяти.