最直接,最简单的实现就是这个
static MySingleton *_instance = nil;
+ (MySingleton *) instance
{
@synchronized (_instance)
{
if (_instance == nil)
{
_instance = [[MySingleton alloc] init];
}
return _instance;
}
}
其实我知道几个关于单身人士的热门帖子 Implementing a Singleton in iOS 和流行template
所以我的问题是“上述实施的任何缺陷”?
答案 0 :(得分:5)
是的,您的实施存在很大缺陷。 @synchronized
指令变为对objc_sync_enter
的调用以及稍后调用objc_sync_exit
。当您第一次调用instance
方法时,_instance
为nil
。 objc_sync_enter
功能在您nil
传递instance
时不会锁定,如looking at its source code所示。
因此,如果两个线程在初始化_instance
之前同时调用MySingleton
,您将创建两个_instance
实例。
此外,您应该将dispatch_once
变量放在函数中,除非您有理由将其公开给整个源文件。
iOS 4.0及更高版本的单例访问器的首选实现使用非常高效的+ (MySingleton *)sharedInstance {
static MySingleton *theInstance;
static dispatch_once_t once;
dispatch_once(&once, ^{
theInstance = [[self alloc] init];
});
return theInstance;
}
函数,如下所示:
dispatch_once
在iOS 4.0之前,@synchronized
功能不可用,因此如果您确实需要支持较旧的iOS版本(不太可能),则必须使用效率较低的nil
。由于无法在+ (MySingleton *)sharedInstance {
static volatile MySingleton *theInstance;
if (!theInstance) {
@synchronized (self) {
if (!theInstance)
theInstance = [[self alloc] init];
}
}
return theInstance;
}
上进行同步,因此可以在类对象上进行同步:
{{1}}
答案 1 :(得分:1)
What should my Objective-C singleton look like?对单身人士来说是一个很好的讨论(正如乔希指出的那样),但要回答你的问题:
不,那不行。为什么呢?
@synchronized需要一个已分配的常量对象来进行同步。把它想象成一个参考点。你正在使用_instance,它最初将是零(不好)。 objective-c的一个很好的部分是类本身就是对象,所以你可以这样做:
@同步(自)
现在就缺陷而言,一旦你对作为类本身的常量对象(使用self)进行同步,你将拥有一个无缺陷的单例,但是每次访问你的单例时你都会承担同步开销的代价。这可能会产生重大的性能影响。
缓解这种情况的简单机制是确定创建只需要发生一次,然后所有后续调用将返回相同的引用,该引用在进程生命周期中永远不会改变。
识别这个,我们可以将你的@synchronization块包装成nil检查,以避免在创建单例之后出现性能损失:
static MySingleton *_instance = nil;
+ (MySingleton *) instance
{
if (!_instance)
{
@synchronized (self)
{
if (!_instance)
{
_instance = [[MySingleton alloc] init];
}
}
}
return _instance;
}
现在你有一个双重NULL检查实现你的单身人士。可是等等!还有更多要考虑的事情!什么?什么可以留下?对于这个示例代码,没什么,但是在单例创建工作的情况下,我们需要做一些考虑......
让我们看看第二个零检查,并为需要额外工作的常见单例场景添加一些额外的代码。
if (!_instance)
{
_instance = [[MySingleton alloc] init];
[_instance performAdditionalPrepWork];
}
这一切看起来都很棒,但是在allocininited类分配给_instance引用和我们执行准备工作的点之间存在竞争条件。第二个线程可以看到_instance存在并且如果在准备工作完成之前使用,则创建具有未定义(并且可能崩溃)结果的竞争条件。
那么我们需要做什么?好吧,我们需要在分配给_instance引用之前完全准备好单例。
if (!_instance)
{
MySingleton* tmp = [[MySingleton alloc] init];
[tmp performAdditionalPrepWork];
_instance = tmp;
}
简单,对吗?我们通过将单例的赋值延迟到_instance引用来解决问题,直到我们完全准备好对象为止,对吧?在一个完美的世界中,是的,但我们并没有生活在一个完美的世界中,因为事实证明我们需要考虑一个外部的力量,用我们完美的代码......编译器。
编译器非常先进,通过消除看似冗余来努力提高代码效率。冗余就像使用临时指针一样,可以通过直接使用_instance引用来避免。诅咒高效的编译器!
没关系,每个平台都有一个低级API,但是有这个问题。我们将在tmp变量的准备和_instance引用(用于iOS和Mac OS X平台的OSMemoryBarrier())的分配之间使用内存屏障。它只是作为编译器的指示器,应该独立于后面的代码考虑屏障之前的代码,从而消除编译器对代码冗余的解释。
这是我们在零检查中的新代码:
if (!_instance)
{
MySingleton* tmp = [[MySingleton alloc] init];
[tmp performAdditionalPrepWork];
OSMemoryBarrier();
_instance = tmp;
}
乔治,我想我们已经得到了!双NULL检查单例访问器,100%线程安全。这有点矫枉过正吗?取决于性能和线程安全性对您是否重要。这是最终的单例实现:
#include <libker/OSAtomic.h>
static MySingleton *_instance = nil;
+ (MySingleton *) instance
{
if (!_instance)
{
@synchronized (self)
{
if (!_instance)
{
MySingleton* tmp = [[MySingleton alloc] init];
[tmp performAdditionalPrepWork];
OSMemoryBarrier();
_instance = tmp;
}
}
}
return _instance;
}
现在,如果你没有在objective-C中做准备工作,那么只需分配alloc-inited MySingleton即可。但是,在C ++中,操作顺序要求必须使用临时变量和内存屏障技巧。为什么?因为对象的分配和对引用的赋值将在构造对象之前发生。
_instance = new MyCPPSingleton(someInitParam);
(有效)与
相同_instance = (MyCCPSingleton*)malloc(sizeof(MyCPPSingleton)); // allocating the memory
_instance->MyCPPSingleton(someInitParam); // calling the constructor
因此,如果您从未使用过C ++,请不要使用C ++,但如果您这样做 - 请务必在计划在C ++中应用双NULL检查单例时牢记这一点。