Глава.30 .Гибридные.конструкции.синхронизации.потоков
// Открытый статический метод, возвращающий объект Singleton
// (создавая его при необходимости)
public static Singleton GetSingleton() {
// Если объект Singleton уже создан, возвращаем его
if (s_value != null) return s_value;
Monitor.Enter(s_lock); // Если не создан, позволяем одному
// потоку сделать это
if (s_value == null) {
// Если объекта все еще нет, создаем его
Singleton temp = new Singleton();
// Сохраняем ссылку в переменной s_value (см. обсуждение далее)
Volatile.Write(ref s_value, temp);
}
Monitor.Exit(s_lock);
// Возвращаем ссылку на объект Singleton
return s_value;
}
}
Принцип блокировки с двойной проверкой состоит в том, что при вызове метода
GetSingleton
быстро проверяется поле
s_value
, чтобы выяснить, создан ли объ-
ект. При положительном результате проверки метод возвращает ссылку на объект.
В результате отпадает необходимость в синхронизации потоков, и приложение
работает очень быстро. Однако если поток, вызвавший метод
GetSingleton
, не
обнаруживает объекта, он прибегает к блокированию в рамках синхронизации
потоков, гарантируя, что созданием объекта будет заниматься только один поток.
То есть снижение производительности наблюдается только после первого запроса
к одноэлементному объекту.
Теперь я объясню, почему этот паттерн не работает в Java. В начале метода
GetSingleton
виртуальная машина Java считывает значение поля
s_value
в регистр
процессора и при выполнении второй инструкции
if
ограничивается запросом
к этому регистру. В итоге результатом данной проверки всегда является значение
true
, а это означает, что в создании объекта
Singleton
принимают участие все по-
токи. Разумеется, это возможно только при условии, что все потоки вызвали метод
GetSingleton
одновременно, чего в большинстве случаев не происходит. Именно
поэтому ошибка столько времени оставалась нераспознанной.
В CLR вызов любого метода блокирования означает установку непреодолимого
барьера на доступ к памяти: вся запись в переменные должна завершиться до этого
барьера, а любое чтение переменных может начаться только после барьера. Для
метода
GetSingleton
это означает, что повторное чтение поля
s_value
должно
быть произведено после вызова метода
Monitor.Enter
; в процессе вызова метода
значение поля нельзя сохранить в регистре.
Внутри метода
GetSingleton
вызывается метод
Volatile.Write
. Предположим,
что вторая инструкция
if
содержит следующую строку кода:
s_value = new Singleton(); // В идеале хотелось бы использовать эту команду
877
Блокировка.с.двойной.проверкой
Можно ожидать, что компилятор создаст код, выделяющий память под объект
Singleton
, вызовет конструктор для инициализации полей данного объекта и при-
своит ссылку на него полю
s_value
, чтобы это значение увидели другие потоки — это
называется
публикацией
(publishing). Однако компилятор может выделить память
под объект
Singleton
, назначить ссылку переменной
s_value
(выполнив публи-
кацию) и только после этого вызвать конструктор. Если в процедуре участвует
всего один поток, подобное изменение очередности операций не имеет значения.
Но что произойдет, если после публикации ссылки в поле
s_value
, но до вызова
конструктора другой поток вызовет метод
GetSingleton
? Этот поток обнаружит, что
значение поля
s_value
отлично от
null
и начнет пользоваться объектом
Singleton
,
хотя его конструктор еще не закончил работу! Подобную ошибку крайне сложно
отследить, особенно из-за того, что время ее появления случайно.
Эту проблему решает вызов метода
Interlocked.Exchange
. Он гарантирует, что
ссылка из переменной
temp
будет опубликована в поле
s_value
только после того,
как конструктор завершит свою работу. Альтернативным способом решения про-
блемы является пометка поля
s_value
ключевым словом
volatile
. Запись в такое
волатильное (неустойчивое) поле
s_value
возможна только после завершения
конструктора. К сожалению, то же самое относится ко всем процедурам чтения во-
латильного поля, а так как никакой необходимости в этом нет, вряд ли стоит идти
на снижение производительности без полной уверенности в полезности этого.
В начале этого раздела я назвал блокировку с двойной проверкой не особо инте-
ресной. С моей точки зрения, разработчики используют это решение гораздо чаще,
чем следовало бы. В большинстве случаев оно только снижает производительность.
Вот гораздо более простая версия класса
Singleton
с аналогичным предыдущей
версии поведением, но без блокирования с двойной проверкой:
internal sealed class Singleton {
private static Singleton s_value = new Singleton();
// Закрытый конструктор не дает коду вне данного класса
// создавать экземпляры
private Singleton() {
// Код инициализации объекта Singleton
}
// Открытый статический метод, возвращающий объект Singleton
// (и создающий его, если это нужно)
public static Singleton GetSingleton() { return s_value; }
}
Так как CLR автоматически вызывает конструктор класса при первой по-
пытке получить доступ к члену этого класса, при первом запросе потока к методу
GetSingleton
класса
Singleton
автоматически создается экземпляр объекта. Более
того, среда CLR гарантирует безопасность в отношении потоков при вызове кон-
структора класса. Все это уже объяснялось в главе 8. Недостатком такого подхода
является вызов конструктора типа при первом доступе к любому члену класса.
То есть если в типе
Singleton
определить другие статические члены, первая же
878
Do'stlaringiz bilan baham: |