7

我在 Mike Ash “单身人士的照顾和喂养”中遇到了这个问题,他的评论让我有些困惑:

不过,这段代码有点慢。拿锁有点贵。更痛苦的是,在绝大多数情况下,锁是没有意义的。只有当 foo 为 nil 时才需要锁,这基本上只发生一次。初始化单例后,不再需要锁,但锁本身仍然存在。

+(id)sharedFoo {
    static Foo *foo = nil;
    @synchronized([Foo class]) {
        if(!foo) foo = [[self alloc] init];
    }
    return foo;
}

我的问题是,这毫无疑问是有充分理由的,但你为什么不能写(见下文)将锁限制在 foo 为 nil 时?

+(id)sharedFoo {
    static Foo *foo = nil;
    if(!foo) {
        @synchronized([Foo class]) {
            foo = [[self alloc] init];
        }
    }
    return foo;
}

干杯加里

4

5 回答 5

18

因为那时测试受制于竞争条件。两个不同的线程可能会独立测试foois nil,然后(依次)创建单独的实例。当一个线程执行测试而另一个线程仍在内部+[Foo alloc]-[Foo init]但尚未设置时,这可能会在您的修改版本中发生foo

顺便说一句,我根本不会那样做。查看该dispatch_once()函数,它可以让您保证一个块在您的应用程序的生命周期内只执行一次(假设您的目标平台上有 GCD)。

于 2010-03-10T17:01:11.497 回答
7

这称为双重检查锁定“优化”。正如随处记录的那样,这是不安全的。即使它没有被编译器优化打败,它也会被现代机器上的内存工作方式打败,除非你使用某种栅栏/障碍。

Mike Ash 还展示volatile了使用and的正确解决方案OSMemoryBarrier();

问题是,当一个线程执行foo = [[self alloc] init];时,不能保证当另一个线程看到foo != 0由执行的所有内存写入时init也是可见的。

另请参阅DCL 和 C++以及DCL 和 java了解更多详细信息。

于 2010-03-11T08:34:44.373 回答
1

在您的版本中,检查!foo可能同时发生在多个线程上,允许两个线程跳转到alloc块中,一个等待另一个完成,然后再分配另一个实例。

于 2010-03-10T17:04:06.830 回答
1

您可以通过仅在 foo==nil 时获取锁来进行优化,但之后您需要再次测试(在 @synchronized 内)以防止竞争条件。

+ (id)sharedFoo {
    static Foo *foo = nil;
    if(!foo) {
        @synchronized([Foo class]) {
            if (!foo)  // test again, in case 2 threads doing this at once
                foo = [[self alloc] init];
        }
    }
    return foo;
}
于 2010-03-10T17:04:28.777 回答
1

最好的方式,如果你有大中央调度

+ (MySingleton*) instance {
 static dispatch_once_t _singletonPredicate;
 static MySingleton *_singleton = nil;

 dispatch_once(&_singletonPredicate, ^{
    _singleton = [[super allocWithZone:nil] init];
 });

 return _singleton
 }
+ (id) allocWithZone:(NSZone *)zone {
  return [self instance];
 }
于 2014-03-07T11:21:05.163 回答