Пример использования в Java
Рассмотрим следующий код на языке Java, взятый из:
Этот код не будет корректно работать в многопоточной программе. Метод getHelper должен получать блокировку на случай, если его вызовут одновременно из двух потоков. Действительно, если поле helper ещё не инициализировано, и одновременно два потока вызовут метод getHelper, то оба потока попытаются создать объект, что приведет к созданию лишнего объекта. Эта проблема решается использованием синхронизации, как показано в следующем примере.
Этот код работает, но он вносит дополнительные накладные расходы на синхронизацию. Первый вызов getHelper создаст объект, и нужно синхронизировать только те несколько потоков, которые будут вызывать getHelper во время инициализации объекта. После инициализации синхронизация при вызове getHelper является излишней, так как будет производиться только чтение переменной. Так как синхронизация может уменьшить производительность в 100 раз и более, накладные расходы на блокировку при каждом вызове этого метода кажутся излишними: как только инициализация завершена, необходимость в блокировке отпадает. Многие программисты попытались оптимизировать этот код следующим образом:
Сначала проверяется, инициализирована ли переменная (без получения блокировки). Если она инициализирована, её значение возвращается немедленно.
Получение блокировки.
Повторно проверяется, инициализирована ли переменная, так как вполне возможно, что после первой проверки другой поток инициализировал переменную. Если она инициализирована, её значение возвращается.
В противном случае, переменная инициализируется и возвращается.
На интуитивном уровне, этот код кажется корректным. Однако, существуют некоторые проблемы, которых следует избегать. Представим себе, что события в многопоточной программе протекают так:
Поток А замечает, что переменная не инициализирована, затем получает блокировку и начинает инициализацию.
Семантика некоторых языков программирования такова, что потоку А разрешено присвоить разделяемой переменной ссылку на объект, который находится в процессе инициализации.
Поток Б замечает, что переменная инициализирована (по крайней мере, ему так кажется), и возвращает значение переменной без получения блокировки. Если поток Б теперь будет использовать переменную до того момента, когда поток А закончит инициализацию, поведение программы будет некорректным.
Одна из опасностей использования блокировки с двойной проверкой в J2SE 1.4 (и более ранних версиях) состоит в том, что часто кажется, что программа работает корректно. Во-первых, рассмотренная ситуация будет возникать не очень часто; во-вторых, сложно отличить корректную реализацию данного шаблона от такой, которая имеет описанную проблему. В зависимости от компилятора, распределения планировщиком процессорного времени для потоков, а также природы других работающих конкурентных процессов, ошибки, спровоцированные с некорректной реализацией блокировки с двойной проверкой, обычно происходят бессистемно. Воспроизведение таких ошибок обычно затруднено.
Можно решить проблему при использовании J2SE 5.0. Новая семантика ключевого слова volatile даёт возможность корректно обработать запись в переменную в данном случае. Этот новый шаблон описан в:
Было предложено много вариантов блокировки с двойной проверкой, которые явно (при помощи volatile или синхронизации) не сообщают то, что объект полностью сконструирован, и все они являются некорректными.
Рассмотрим следующий код на языке Java, взятый из:
Код
// Однопоточная версия
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// и остальные члены класса…
}
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// и остальные члены класса…
}
Этот код не будет корректно работать в многопоточной программе. Метод getHelper должен получать блокировку на случай, если его вызовут одновременно из двух потоков. Действительно, если поле helper ещё не инициализировано, и одновременно два потока вызовут метод getHelper, то оба потока попытаются создать объект, что приведет к созданию лишнего объекта. Эта проблема решается использованием синхронизации, как показано в следующем примере.
Код
// Правильная, но "дорогая" по времени выполнения многопоточная версия
class Foo {
private Helper helper = null;
public synchronized Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// и остальные члены класса…
}
class Foo {
private Helper helper = null;
public synchronized Helper getHelper() {
if (helper == null)
helper = new Helper();
return helper;
}
// и остальные члены класса…
}
Этот код работает, но он вносит дополнительные накладные расходы на синхронизацию. Первый вызов getHelper создаст объект, и нужно синхронизировать только те несколько потоков, которые будут вызывать getHelper во время инициализации объекта. После инициализации синхронизация при вызове getHelper является излишней, так как будет производиться только чтение переменной. Так как синхронизация может уменьшить производительность в 100 раз и более, накладные расходы на блокировку при каждом вызове этого метода кажутся излишними: как только инициализация завершена, необходимость в блокировке отпадает. Многие программисты попытались оптимизировать этот код следующим образом:
Сначала проверяется, инициализирована ли переменная (без получения блокировки). Если она инициализирована, её значение возвращается немедленно.
Получение блокировки.
Повторно проверяется, инициализирована ли переменная, так как вполне возможно, что после первой проверки другой поток инициализировал переменную. Если она инициализирована, её значение возвращается.
В противном случае, переменная инициализируется и возвращается.
Код
// Неработающая многопоточная версия
// Шаблон "Double-Checked Locking"
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// и остальные члены класса…
}
// Шаблон "Double-Checked Locking"
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// и остальные члены класса…
}
На интуитивном уровне, этот код кажется корректным. Однако, существуют некоторые проблемы, которых следует избегать. Представим себе, что события в многопоточной программе протекают так:
Поток А замечает, что переменная не инициализирована, затем получает блокировку и начинает инициализацию.
Семантика некоторых языков программирования такова, что потоку А разрешено присвоить разделяемой переменной ссылку на объект, который находится в процессе инициализации.
Поток Б замечает, что переменная инициализирована (по крайней мере, ему так кажется), и возвращает значение переменной без получения блокировки. Если поток Б теперь будет использовать переменную до того момента, когда поток А закончит инициализацию, поведение программы будет некорректным.
Одна из опасностей использования блокировки с двойной проверкой в J2SE 1.4 (и более ранних версиях) состоит в том, что часто кажется, что программа работает корректно. Во-первых, рассмотренная ситуация будет возникать не очень часто; во-вторых, сложно отличить корректную реализацию данного шаблона от такой, которая имеет описанную проблему. В зависимости от компилятора, распределения планировщиком процессорного времени для потоков, а также природы других работающих конкурентных процессов, ошибки, спровоцированные с некорректной реализацией блокировки с двойной проверкой, обычно происходят бессистемно. Воспроизведение таких ошибок обычно затруднено.
Можно решить проблему при использовании J2SE 5.0. Новая семантика ключевого слова volatile даёт возможность корректно обработать запись в переменную в данном случае. Этот новый шаблон описан в:
Код
// Работает с новой семантикой volatile
// Не работает в Java 1.4 и более ранних версиях из-за семантики volatile
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
// и остальные члены класса…
}
// Не работает в Java 1.4 и более ранних версиях из-за семантики volatile
class Foo {
private volatile Helper helper = null;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null)
helper = new Helper();
}
}
return helper;
}
// и остальные члены класса…
}
Было предложено много вариантов блокировки с двойной проверкой, которые явно (при помощи volatile или синхронизации) не сообщают то, что объект полностью сконструирован, и все они являются некорректными.