Singleton: How should it be used
编辑:从另一个问题,我提供了一个答案,链接到很多关于单件的问题/答案:这里是关于单件的更多信息:
所以我读过单件线:好的设计还是拐杖?争论仍然激烈。
我把单身作为一种设计模式(好的和坏的)。singleton的问题不是模式,而是用户(对不起每个人)。每个人和他们的父亲都认为他们可以正确地执行一个(从我做过的许多采访来看,大多数人都不能)。另外,因为每个人都认为他们可以实现正确的单例,所以他们滥用该模式,并在不合适的情况下使用它(用单例替换全局变量!).
所以需要回答的主要问题是:
- 你什么时候应该用单件的
- 如何正确实现单例
我对这篇文章的希望是,我们可以在一个地方(而不是谷歌和搜索多个网站)收集一个权威的信息源,以确定何时(以及如何)正确地使用一个单一的网站。同样合适的是一个反用法和常见的坏实现的列表,解释它们为什么不能工作,对于好的实现,解释它们的弱点。
让球滚起来:我会举起手说这是我用的,但可能有问题。我喜欢他在《有效C++》一书中对"Scott Myers"的处理。
Good Situations to use Singletons (not many):
- Logging frameworks
- Thread recycling pools
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /* * C++ Singleton * Limitation: Single Threaded Design * See: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf * For problems associated with locking in multi threaded applications * * Limitation: * If you use this Singleton (A) within a destructor of another Singleton (B) * This Singleton (A) must be fully constructed before the constructor of (B) * is called. */ class MySingleton { private: // Private Constructor MySingleton(); // Stop the compiler generating methods of copy the object MySingleton(MySingleton const& copy); // Not Implemented MySingleton& operator=(MySingleton const& copy); // Not Implemented public: static MySingleton& getInstance() { // The only instance // Guaranteed to be lazy initialized // Guaranteed that it will be destroyed correctly static MySingleton instance; return instance; } }; |
好啊。让我们把一些批评和其他实现放在一起。-)
你们都错了。阅读问题。答:
如果出现以下情况,请使用单件:
- 您需要系统中只有一个类型的对象
如果出现以下情况,请勿使用单件:
- 你想保存内存
- 你想尝尝新的
- 你想炫耀你知道多少
- 因为其他人都在这样做(参见维基百科中的Cargo Cult程序员)
- 在用户界面小部件中
- 应该是个缓存
- 弦乐
- 在会议中
- 我可以走一整天
如何创建最佳单件:
- 越小越好。我是个极简主义者
- 确保它是线程安全的
- 确保它从不为空
- 确保只创建一次
- 延迟还是系统初始化?符合您的要求
- 有时,操作系统或JVM为您创建单机(例如在爪哇中,每个类定义都是单独的)。
- 提供一个析构函数,以某种方式找出如何处理资源
- 用很少的记忆
单身汉能让你在同一个班级里把两个坏品质结合起来。这几乎是错误的。
单人票给你:
第一个是直截了当的。全球经济普遍不景气。除非我们真的需要,否则我们不应该让对象全局可访问。
第二个听起来似乎有道理,但让我们想想。上次**意外*创建新对象而不是引用现有对象是什么时候?由于这是标记C++,让我们使用该语言的一个例子。你经常不小心写
1 2 3 | std::ostream os; os <<"hello world "; |
当你打算写
1 2 | std::cout <<"hello world "; |
当然不是。我们不需要防止这种错误,因为这种错误是不会发生的。如果是这样,正确的反应就是回家睡12-20个小时,希望你感觉好一点。
如果只需要一个对象,只需创建一个实例。如果一个对象应该是全局可访问的,则将其设置为全局。但这并不意味着不可能创建其他实例。
"只有一个实例是可能的"约束并不能真正保护我们免受可能的错误的影响。但这确实使我们的代码很难重构和维护。因为我们经常在稍后发现我们确实需要多个实例。我们有多个数据库,我们有多个配置对象,我们需要多个记录器。我们的单元测试可能希望能够在每个测试中创建和重新创建这些对象,以一个常见的例子为例。
因此,当且仅当我们需要它提供的两个特性时,才应该使用单例:如果我们需要全局访问(这很少见,因为全局访问通常不受鼓励),并且我们需要阻止任何人创建类的多个实例(在我看来这是一个设计问题)。我能看到的唯一原因是,创建两个实例是否会损坏我们的应用程序状态——可能是因为类包含许多静态成员或类似的愚蠢行为。在这种情况下,显而易见的答案是修复那个类。它不应该依赖于成为唯一的实例。
如果需要对对象进行全局访问,请使其成为全局对象,如
如果您确实需要将一个类的实例数量限制为一个,并且无法安全地处理创建第二个实例,那么就强制执行它。但不要让它也能在全球范围内使用。
如果你确实需要这两个特征,那么1)让它成为一个单身汉,2)让我知道你需要它做什么,因为我很难想象这样的情况。
单例的问题不是它们的实现。他们把两个不同的概念混为一谈,这两个概念显然都不可取。
1)单例提供对象的全局访问机制。虽然在没有定义好的初始化顺序的语言中,它们的线程安全性可能稍高一些,或者可靠性稍高一些,但这种用法在道义上仍然相当于全局变量。它是一个全局变量,使用了一些笨拙的语法(比如foo::get_instance()而不是g_foo),但它的用途完全相同(整个程序中可以访问单个对象),并且有着完全相同的缺点。
2)单例阻止类的多个实例化。IME,很少有人会把这种功能放到课堂上。通常情况下,这是一个更为上下文相关的东西;很多被视为一个且只有一个的东西实际上只是一个而已。IMO一个更合适的解决方案是只创建一个实例——直到您意识到需要多个实例为止。
模式有一点:不要泛化。当他们有用的时候,当他们失败的时候,他们拥有所有的案例。
当您必须测试代码时,singleton可能很糟糕。通常情况下,您只能使用类的一个实例,并且可以选择在构造函数中打开一扇门,或者使用某种方法重置状态,等等。
另一个问题是,事实上,单例变量只是一个伪装的全局变量。当你的程序中有太多的全局共享状态时,事情往往会倒退,我们都知道。
这可能会使依赖项跟踪变得更加困难。当一切都取决于你的单身生活时,你很难改变它,很难把它一分为二,等等。你通常都被它困住了。这也妨碍了灵活性。研究一些依赖注入框架以尝试缓解这个问题。
单例语言基本上让您在语言中拥有复杂的全局状态,否则就很难或不可能拥有复杂的全局变量。
Java特别是使用单元格作为全局变量的替换,因为所有的内容都必须包含在一个类中。与全局变量最接近的是公共静态变量,可以将其用作与
C++确实有全局变量,但是调用全局类变量的构造函数的顺序是未定义的。因此,单例允许您将全局变量的创建推迟到第一次需要该变量时。
诸如python和ruby之类的语言很少使用singleton,因为您可以在模块中使用全局变量。
那么,什么时候用单件的好/坏呢?几乎准确地说,使用全局变量是好是坏。
Alexandrescu的现代C++设计具有线程安全、可继承的通用单体。
对于我的2P价值,我认为为你的单件产品定义生命周期是很重要的(当绝对有必要使用它们的时候)。我通常不让静态
- How do you implement a Singleton correctly
有一个问题我从来没有提到过,我在以前的工作中遇到过。我们有在DLL之间共享的C++单件,并且通常确保一个类的单个实例的机制是不起作用的。问题是每个DLL都有自己的一组静态变量,以及exe。如果您的get_实例函数是内联的或者是静态库的一部分,那么每个dll最终都会得到它自己的"singleton"副本。
解决方案是确保singleton代码只在一个dll或exe中定义,或者创建一个具有这些属性的singleton管理器来包出实例。
第一个例子不是线程安全的——如果两个线程同时调用getInstance,那么这个静态变量就是pita。某种形式的互斥会有所帮助。
正如其他人所指出的,单例的主要缺点包括无法扩展它们,并且失去了实例化多个实例的能力,例如出于测试目的。
单件产品的一些有用方面:
但是,您不必使用单例来获得这些好处。您可以编写一个完成工作的普通对象,然后让人们通过工厂(一个单独的对象)访问它。工厂可以只考虑实例化一个,并在需要时重用它,等等。另外,如果您编程到一个接口而不是一个具体的类,那么工厂可以使用策略,也就是说,您可以切换接口的各种实现。
最后,工厂将自己借给依赖注入技术,如Spring等。
单身汉真正的失败在于他们破坏了遗产。除非您有权访问引用singleton的代码,否则无法派生新类来提供扩展功能。因此,事实上,单例将使您的代码紧密耦合(可通过策略模式修复)。也就是依赖注入),它还将阻止您关闭代码修订部分(共享库)。
因此,即使是日志记录器或线程池的示例也是无效的,应该用策略替换。
大多数人在试图让自己对使用全局变量感觉良好时使用单例。有合法的用途,但大多数情况下,人们使用它们时,事实上只能有一个实例,这只是一个微不足道的事实,相比之下,它是全局可访问的。
当您在初始化和对象时有很多代码在运行时,单例非常方便。例如,当您在设置持久性对象时使用iBATIS时,它必须读取所有配置、解析映射、确保其全部正确等等。在进入你的代码之前。
如果每次都这样做,性能就会大大降低。在单例中使用它,您只需点击一次,然后所有后续调用就不必这么做。
因为单个实例只允许创建一个实例,所以它有效地控制实例复制。例如,您不需要一个查找的多个实例-例如,一个莫尔斯查找图,因此将其包装在一个单实例类中是很合适的。仅仅因为您有一个该类的实例,并不意味着您也受到对该实例的引用数的限制。您可以将对实例的调用排队(以避免线程问题),并进行必要的更改。是的,单例的一般形式是全局公共的,您当然可以修改设计来创建一个更受访问限制的单例。我以前没累过,但我知道这是可能的。对于那些评论说单例模式是完全邪恶的人,你应该知道:是的,如果你不正确地使用它,或者在它的有效功能和可预测的行为范围内使用它,那就是邪恶的:不要泛化。
但是当我需要一个单例的时候,我经常会用一个施瓦兹计数器来实例化它。
我用单身汉做面试测试。
当我要求开发人员命名一些设计模式时,如果他们只能命名为singleton,那么他们就不会被雇佣。
下面是实现线程安全的单例模式的更好方法,它可以释放析构函数本身中的内存。但我认为析构函数应该是可选的,因为当程序终止时,会自动销毁singleton实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #include<iostream> #include<mutex> using namespace std; std::mutex mtx; class MySingleton{ private: static MySingleton * singletonInstance; MySingleton(); ~MySingleton(); public: static MySingleton* GetInstance(); MySingleton(const MySingleton&) = delete; const MySingleton& operator=(const MySingleton&) = delete; MySingleton(MySingleton&& other) noexcept = delete; MySingleton& operator=(MySingleton&& other) noexcept = delete; }; MySingleton* MySingleton::singletonInstance = nullptr; MySingleton::MySingleton(){ }; MySingleton::~MySingleton(){ delete singletonInstance; }; MySingleton* MySingleton::GetInstance(){ if (singletonInstance == NULL){ std::lock_guard<std::mutex> lock(mtx); if (singletonInstance == NULL) singletonInstance = new MySingleton(); } return singletonInstance; } |
关于我们需要使用singleton类的情况,可以-如果我们想在程序的整个执行过程中保持实例的状态如果我们要写入一个应用程序的执行日志,其中只需要使用文件的一个实例…等等。如果有人能在我的上述代码中提出优化建议,那将是值得注意的。
梅耶斯的单例模式在大多数情况下都能很好地工作,而且在这种情况下,寻找更好的模式并不一定要付出代价。只要构造函数永远不会抛出,并且单例之间没有依赖关系。
singleton是一个全局可访问对象(从现在起的GAO)的实现,尽管并非所有的GAO都是singleton。
日志记录程序本身不应该是单例的,但理想情况下,日志记录的方法应该是全局可访问的,以便从日志记录的位置或方式中分离出日志消息的生成位置。
延迟加载/延迟评估是一个不同的概念,而singleton通常也实现了这一点。它有很多自己的问题,特别是线程安全性,如果它失败了,除了一些例外,那么在当时看起来是个好主意的东西最终并没有那么好。(有点像字符串中的cow实现)。
考虑到这一点,goas可以这样初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | namespace { T1 * pt1 = NULL; T2 * pt2 = NULL; T3 * pt3 = NULL; T4 * pt4 = NULL; } int main( int argc, char* argv[]) { T1 t1(args1); T2 t2(args2); T3 t3(args3); T4 t4(args4); pt1 = &t1; pt2 = &t2; pt3 = &t3; pt4 = &t4; dostuff(); } T1& getT1() { return *pt1; } T2& getT2() { return *pt2; } T3& getT3() { return *pt3; } T4& getT4() { return *pt4; } |
它不需要像那样简单地完成,而且很明显,在一个包含对象的已加载库中,您可能需要一些其他机制来管理它们的生存期。(将它们放在加载库时得到的对象中)。
当我用单件的时候?我用它们做了两件事-指示使用dlopen加载了哪些库的singleton表。-日志记录程序可以订阅的消息处理程序,也可以向其发送消息。信号处理器专用。
我仍然不明白为什么单身汉必须是全球性的。
我将生成一个singleton,在那里我将一个数据库作为一个私有常量静态变量隐藏在类中,并生成一些类函数,这些函数使用数据库而不向用户公开数据库。
我不明白为什么这个功能会不好。
我认为这是C最强大的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | using System; using System.Collections; using System.Threading; namespace DoFactory.GangOfFour.Singleton.RealWorld { // MainApp test application class MainApp { static void Main() { LoadBalancer b1 = LoadBalancer.GetLoadBalancer(); LoadBalancer b2 = LoadBalancer.GetLoadBalancer(); LoadBalancer b3 = LoadBalancer.GetLoadBalancer(); LoadBalancer b4 = LoadBalancer.GetLoadBalancer(); // Same instance? if (b1 == b2 && b2 == b3 && b3 == b4) { Console.WriteLine("Same instance "); } // All are the same instance -- use b1 arbitrarily // Load balance 15 server requests for (int i = 0; i < 15; i++) { Console.WriteLine(b1.Server); } // Wait for user Console.Read(); } } //"Singleton" class LoadBalancer { private static LoadBalancer instance; private ArrayList servers = new ArrayList(); private Random random = new Random(); // Lock synchronization object private static object syncLock = new object(); // Constructor (protected) protected LoadBalancer() { // List of available servers servers.Add("ServerI"); servers.Add("ServerII"); servers.Add("ServerIII"); servers.Add("ServerIV"); servers.Add("ServerV"); } public static LoadBalancer GetLoadBalancer() { // Support multithreaded applications through // 'Double checked locking' pattern which (once // the instance exists) avoids locking each // time the method is invoked if (instance == null) { lock (syncLock) { if (instance == null) { instance = new LoadBalancer(); } } } return instance; } // Simple, but effective random load balancer public string Server { get { int r = random.Next(servers.Count); return servers[r].ToString(); } } } } |
以下是.NET优化版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | using System; using System.Collections; namespace DoFactory.GangOfFour.Singleton.NETOptimized { // MainApp test application class MainApp { static void Main() { LoadBalancer b1 = LoadBalancer.GetLoadBalancer(); LoadBalancer b2 = LoadBalancer.GetLoadBalancer(); LoadBalancer b3 = LoadBalancer.GetLoadBalancer(); LoadBalancer b4 = LoadBalancer.GetLoadBalancer(); // Confirm these are the same instance if (b1 == b2 && b2 == b3 && b3 == b4) { Console.WriteLine("Same instance "); } // All are the same instance -- use b1 arbitrarily // Load balance 15 requests for a server for (int i = 0; i < 15; i++) { Console.WriteLine(b1.Server); } // Wait for user Console.Read(); } } // Singleton sealed class LoadBalancer { // Static members are lazily initialized. // .NET guarantees thread safety for static initialization private static readonly LoadBalancer instance = new LoadBalancer(); private ArrayList servers = new ArrayList(); private Random random = new Random(); // Note: constructor is private. private LoadBalancer() { // List of available servers servers.Add("ServerI"); servers.Add("ServerII"); servers.Add("ServerIII"); servers.Add("ServerIV"); servers.Add("ServerV"); } public static LoadBalancer GetLoadBalancer() { return instance; } // Simple, but effective load balancer public string Server { get { int r = random.Next(servers.Count); return servers[r].ToString(); } } } } |
你可以在dotfactory.com上找到这个模式。
当我有一个封装了大量内存的类时,我发现它们很有用。例如,在最近的一个游戏中,我正在研究一个影响图类,它包含一组非常大的连续内存数组。我希望在启动时分配所有文件,在关闭时释放所有文件,并且我肯定只需要其中的一个副本。我还必须从许多地方进入它。在这种情况下,我发现单例模式非常有用。
我确信还有其他解决方案,但我发现这一方案非常有用且易于实现。
反使用:
过度使用单例的一个主要问题是,模式阻止了替代实现的简单扩展和交换。类名称在使用singleton的任何位置都是硬编码的。
如果你是创建了单例并使用它的人,不要把它当作单例(它没有意义,因为你可以控制对象的奇点而不使它成为单例),但是当你是一个库的开发人员,并且你只想向你的用户提供一个对象(在这种情况下,你是创建单例的人)时,它是有意义的。打开,但您不是用户)。
单例是对象,所以把它们作为对象使用,很多人直接通过调用返回它的方法访问单例,但这是有害的,因为你让你的代码知道对象是单例的,我更喜欢使用单例作为对象,我通过构造函数传递它们,然后把它们作为普通对象使用,这样,你的代码不知道这些对象是否是单例对象,这使得依赖关系更加清晰,对重构有一点帮助…
另一个实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Singleton { public: static Singleton& Instance() { // lazy initialize if (instance_ == NULL) instance_ = new Singleton(); return *instance_; } private: Singleton() {}; static Singleton *instance_; }; |
在桌面应用程序中(我知道,只有美国恐龙会再写这些了!)它们对于获得相对不变的全局应用程序设置(用户语言、帮助文件的路径、用户首选项等)至关重要,否则,这些设置必须应用到每个类和每个对话框中。
编辑-当然这些应该是只读的!