Дважды проверил блокировку в.NET
Я наткнулся на эту статью, обсуждающую, почему парадигма блокировки двойной проверки нарушена в Java. Действительна ли парадигма для.NET (в частности, C#), если объявлены переменные volatile
?
8 ответов
Реализация шаблона Singleton в C# говорит об этой проблеме в третьей версии.
Это говорит:
Если сделать переменную экземпляра volatile, она может работать так же, как и явные вызовы барьера памяти, хотя в последнем случае даже эксперты не могут прийти к единому мнению, какие именно барьеры необходимы. Я стараюсь избегать ситуаций, когда эксперты не согласны с тем, что правильно, а что нет!
Автор, кажется, подразумевает, что двойная блокировка с меньшей вероятностью сработает, чем другие стратегии, и поэтому не должна использоваться.
Двойная проверка блокировки теперь работает как в Java, так и в C# (модель памяти Java изменилась, и это один из эффектов). Тем не менее, вы должны сделать это правильно. Если вы все испортите, вы можете потерять безопасность потока.
Как уже говорилось в других ответах, если вы реализуете шаблон синглтона, есть гораздо лучшие способы сделать это. Лично, если я нахожусь в ситуации, когда мне приходится выбирать между двойной проверкой блокировки и кодом "каждый раз блокировать", я буду использовать блокировку каждый раз, пока не получу реальные доказательства того, что это вызывает узкое место. Когда дело доходит до многопоточности, простой и заведомо правильный шаблон стоит многого.
.NET 4.0 имеет новый тип: Lazy<T>
это снимает любую озабоченность по поводу неправильной схемы. Это часть новой параллельной библиотеки задач.
См. Центр разработки параллельных вычислений MSDN: http://msdn.microsoft.com/en-us/concurrency/default.aspx
Кстати, здесь есть обратный порт (я думаю, что он не поддерживается) для.NET 3.5 SP1.
Обратите внимание, что в Java (и, скорее всего, в.Net) двойная проверка блокировки для одноэлементной инициализации совершенно не нужна, а также не работает. Поскольку классы не инициализируются до тех пор, пока они не будут впервые использованы, желаемая ленивая инициализация уже достигается этим;
private static Singleton instance = new Singleton();
Если ваш класс Singleton не содержит таких вещей, как константы, к которым можно получить доступ до первого использования экземпляра Singleton, это все, что вам нужно сделать.
Я не понимаю, почему все люди говорят, что блокировка двойной проверки - плохой шаблон, но не адаптируйте код, чтобы заставить его работать правильно. На мой взгляд, приведенный ниже код должен работать просто отлично.
Если кто-то может сказать мне, если этот код страдает от проблемы, упомянутой в статье Кэмерона, пожалуйста, сделайте это.
public sealed class Singleton {
static Singleton instance = null;
static readonly object padlock = new object();
Singleton() {
}
public static Singleton Instance {
get {
if (instance != null) {
return instance;
}
lock (padlock) {
if (instance != null) {
return instance;
}
tempInstance = new Singleton();
// initialize the object with data
instance = tempInstance;
}
return instance;
}
}
}
Я дважды проверил блокировку на работу, используя логическое значение (т. Е. Используя примитив, чтобы избежать ленивой инициализации):
Синглтон, использующий логическое значение, не работает. Порядок операций в разных потоках не гарантируется, если вы не пройдете через барьер памяти. Другими словами, как видно из второго потока, created = true
может быть выполнен раньше instance= new Singleton();
Я не совсем понимаю, почему существует множество шаблонов реализации для двойной проверки блокировки (по-видимому, для обхода идиосинкразий компилятора на разных языках). Статья в Википедии на эту тему показывает наивный метод и возможные пути решения проблемы, но ни один из них не так прост (как в C#):
public class Foo
{
static Foo _singleton = null;
static object _singletonLock = new object();
public static Foo Singleton
{
get
{
if ( _singleton == null )
lock ( _singletonLock )
if ( _singleton == null )
{
Foo foo = new Foo();
// Do possibly lengthy initialization,
// but make sure the initialization
// chain doesn't invoke Foo.Singleton.
foo.Initialize();
// _singleton remains null until
// object construction is done.
_singleton = foo;
}
return _singleton;
}
}
В Java вы использовали бы synchronized() вместо lock(), но это в основном та же идея. Если существует возможное несоответствие того, когда назначается одноэлементное поле, то почему бы просто не использовать сначала локальную переменную, а затем назначить одноэлементное поле в последний возможный момент перед выходом из критической секции? Я что-то пропустил?
@ Michael-borgwardt утверждает, что в C# и Java статическое поле инициализируется только один раз при первом использовании, но это поведение зависит от языка. И я часто использовал этот шаблон для ленивой инициализации свойства коллекции (например, user.Sessions).
Я дважды проверил блокировку на работу, используя логическое значение (т. Е. Используя примитив, чтобы избежать ленивой инициализации):
private static Singleton instance;
private static boolean created;
public static Singleton getInstance() {
if (!created) {
synchronized (Singleton.class) {
if (!created) {
instance = new Singleton();
created = true;
}
}
}
return instance;
}