Блокировка с двойной проверкой

Блокировка с двойной проверкой

Double checked locking (блокировка с двойной проверкой) — шаблон проектирования применяющийся в параллельном программировании. Он предназначен для уменьшения накладных расходов, связанных с получением блокировки. Сначала проверяется условие блокировки без какой-либо синхронизации; поток делает попытку получить блокировку только если результат проверки говорит о том, что ни один другой поток не владеет блокировкой.

На некоторых языках и/или на некоторых машинах невозможно безопасно реализовать данный шаблон. Поэтому иногда его называют анти-паттерном.

Обычно он используется для уменьшения накладных расходов при реализации ленивой инициализации в многопоточных программах, например в составе шаблона проектирования Одиночка. При ленивой инициализации переменной, инициализация откладывается до тех пор, пока значение переменной действительно понадобится при вычислениях.

Пример использования в Java, взятый из [1]:

// Однопоточная версия
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;
    }
 
    // и остальные члены класса…
}

Этот код работает, но он вносит дополнительные накладные расходы на синхронизацию. Первый вызов getHelper() создаст объект, и нужно синхронизировать только те несколько потоков, которые будут вызывать getHelper() во время инициализации объекта. После инициализации синхронизация при вызове getHelper() является излишней, так как будет производиться только чтение переменной. Так как синхронизация может уменьшить производительность в 100 раз и более, накладные расходы на блокировку при каждом вызове этого метода кажутся излишними: как только инициализация завершена, необходимость в блокировке отпадает. Многие программисты попытались оптимизировать этот код следующим образом:

  1. Сначала проверяется, инициализирована ли переменная (без получения блокировки). Если она инициализирована, её значение возвращается немедленно.
  2. Получение блокировки.
  3. Повторно проверяется, инициализирована ли переменная, так как вполне возможно, что после первой проверки другой поток инициализировал переменную. Если она инициализирована, её значение возвращается.
  4. В противном случае, переменная инициализируется и возвращается.
// Неработающая многопоточная версия
// Шаблон "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;
    }
 
    // и остальные члены класса…
}

На интуитивном уровне, этот код кажется корректным. Однако, существуют некоторые проблемы, которых следует избежать. Представим себе, что события в многопоточной программе протекают так:

  1. Поток А замечает, что переменная не инициализирована, затем получает блокировку и начинает инициализацию.
  2. Семантика некоторых языков программирования такова, что потоку А разрешено присвоить разделяемой переменной ссылку на объект, который находится в процессе инициализации.
  3. Поток Б замечает, что переменная инициализирована (по крайней мере, ему так кажется), и возвращает значение переменной без получения блокировки. Если поток Б теперь будет использовать переменную до того момента, когда поток А закончит инициализацию, поведение программы будет некорректным.

Одна из опасностей использования блокировки с двойной проверкой в J2SE 1.4 (и более ранних версиях) состоит в том, что часто кажется, что программа работает корректно. Во-первых, рассмотренная ситуация будет возникать не очень часто; во-вторых, сложно отличить корректную реализацию данного шаблона от такой, которая имеет описанную проблему. В зависимости от компилятора, распределения планировщиком процессорного времени для потоков, а также природы других работающих конкурентных процессов, ошибки, спровоцированные с некорректной реализацией блокировки с двойной проверкой, обычно происходят бессистемно. Воспроизведение таких ошибок обычно затруднено.

Можно решить проблему при использовании J2SE 5.0. Новое ключевое слово volatile даёт возможность корректно обработать запись в переменную в данном случае. Этот новый шаблон описан в [2]:

// Работает с новой семантикой 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;
    }
 
    // и остальные члены класса…
}

Было предложено много вариантов блокировки с двойной проверкой, которые явно (при помощи volatile или синхронизации) не сообщают то, что объект полностью сконструирован, и все они являются некорректными [3][4].

Пример использования в C#

public sealed class Singleton
{
	private Singleton()
	{
		// инициализировать новый экземпляр объекта
	}
	private static object m_SyncRoot = new object();
	private static volatile Singleton m_SingletonInstance;
	public static Singleton GetInstance()
	{
		// создан ли объект
		if (m_SingletonInstance == null)
		{
			// нет, не создан
			// только один поток может создать его
			lock (m_SyncRoot)
			{
				// проверяем, не создал ли объект другой поток
				if (m_SingletonInstance == null)
				{
					// нет не создал — создаём
					m_SingletonInstance = new Singleton();
				}
			}
		}
		return m_SingletonInstance;
	}
}

Microsoft подтверждает [5], что при использовании ключевого слова volatile, использование паттерна Double checked locking является безопасным.


Ссылки


Wikimedia Foundation. 2010.

Игры ⚽ Поможем сделать НИР

Полезное


Смотреть что такое "Блокировка с двойной проверкой" в других словарях:

  • Double checked locking — Шаблон проектирования Блокировка с двойной проверкой Double checked locking Описан в Design Patterns Нет Double checked locking (блокировка с двойной проверкой) шаблон проектирования, применяющийся в параллельном программировании. Он… …   Википедия

  • Шаблон проектирования — У этого термина существуют и другие значения, см. Паттерн. В разработке программного обеспечения, шаблон проектирования или паттерн (англ. design pattern) повторимая архитектурная конструкция, представляющая собой решение проблемы… …   Википедия

  • Single Thread Execution — Шаблон проектирования Однопоточное выполнение Single Threaded Execution Описан в Design Patterns Нет Однопоточное выполнение (англ. Single Threaded Execution)  известный также под названием англ. Critical Section …   Википедия

  • Абстрактная фабрика (шаблон проектирования) — Шаблон проектирования Абстрактная фабрика Abstract factory Тип: порождающий Описан в Design Patterns Да Абстрактная фабрика (англ. Abstract factory) порождающий шаблон проектирования, позволяющий изменять поведение системы …   Википедия

  • Интерфейс (шаблон проектирования) — Шаблон проектирования Интерфейс Interface Описан в Design Patterns Нет В информатике, шаблон интерфейса не является особым шаблоном среди шаблонов проектирования. Он является общим методом для структурирования компьютерных программ для того …   Википедия

  • Порождающие шаблоны проектирования — Порождающие шаблоны (англ. Creational patterns)  шаблоны проектирования, которые абстрагируют процесс инстанцирования. Они позволяют сделать систему независимой от способа создания, композиции и представления объектов. Шаблон,… …   Википедия

  • Строитель (шаблон проектирования) — У этого термина существуют и другие значения, см. Строитель. Шаблон проектирования Строитель Builder Тип: порождающий Описан в Design Patterns Да Строитель (англ. Builder)  п …   Википедия

  • Адаптер (шаблон проектирования) — У этого термина существуют и другие значения, см. Адаптер. Шаблон проектирования Адаптер Adapter …   Википедия

  • Proxy (шаблон проектирования) — У этого термина существуют и другие значения, см. Proxy. Шаблон проектирования Заместитель Proxy Тип: структурный Описан в Design Patterns Да Шаблон Proxy (определяет объект заместитель англ. surrogate …   Википедия

  • Фасад (шаблон проектирования) — У этого термина существуют и другие значения, см. Фасад (значения). Шаблон проектирования Фасад Facade Тип: структурный Описан в Design Patterns Да Шаблон Facade (Фасад)  Шаблон проектирования, позволяющий ск …   Википедия


Поделиться ссылкой на выделенное

Прямая ссылка:
Нажмите правой клавишей мыши и выберите «Копировать ссылку»