单例:应该如何使用它

< p >编辑: 从另一个问题中,我提供了一个答案,它链接到许多关于单例对象的问题/答案:更多关于单身人士的信息:

所以我已经读取线程单品:好的设计还是拐杖?

我认为单例是一种设计模式(好和坏) 单例的问题不在于模式,而在于用户(对不起大家)。每个人和他们的父亲都认为他们可以正确地实施一个(从我所做的许多采访来看,大多数人都不能)。也因为每个人都认为他们可以实现一个正确的单例,他们滥用模式,并在不合适的情况下使用它(用单例替换全局变量!)

所以需要回答的主要问题是:

  • 什么时候应该使用单例
  • 如何正确地实现单例

我对本文的希望是,我们可以在一个地方(而不是谷歌和搜索多个站点)收集何时(以及如何)正确使用Singleton的权威来源。同样合适的是一个反用法和常见的坏实现的列表,解释为什么他们不能工作,对于好的实现他们的弱点

那么开始吧:
我会举起我的手,说这是我用的,但可能有问题 我喜欢“Scott Myers”在他的书“Effective c++”中对这个主题的处理

使用单例的好情况(不多):

  • 日志框架
  • 线程回收池
/*
* 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;
}
};
< p > OK。让我们把一些批评和其他实现放在一起 : -) < / p >
204472 次浏览

第一个例子不是线程安全的——如果两个线程同时调用getInstance,这个静态将是一个PITA。某种形式的互斥会有所帮助。

单例基本上让你在语言中拥有复杂的全局状态,否则就很难或不可能拥有复杂的全局变量。

Java特别使用单例变量作为全局变量的替代品,因为所有内容都必须包含在类中。最接近全局变量的是公共静态变量,它们可以作为import static的全局变量来使用

c++确实有全局变量,但是调用全局类变量的构造函数的顺序未定义。因此,单例可以让您推迟全局变量的创建,直到第一次需要该变量。

Python和Ruby等语言很少使用单例,因为您可以在模块中使用全局变量。

那么什么时候使用单例对象是好的/坏的呢?差不多就是什么时候使用全局变量是好是坏。

Anti-Usage:

过度使用单例的一个主要问题是,该模式阻碍了可选实现的简单扩展和交换。类名在任何使用单例的地方都是硬编码的。

关于模式的一件事:不推广。当他们有用的时候,当他们失败的时候,他们有所有的情况。

当你必须测验代码时,单例可能很讨厌。您通常只能使用类的一个实例,并且可以选择在构造函数中打开一扇门或使用一些方法来重置状态等等。

另一个问题是,Singleton实际上不过是一个伪装的全局变量。当你的程序有太多的全局共享状态时,事情往往会倒退,我们都知道这一点。

它可能会使依赖跟踪更加困难。当所有事情都依赖于你的单例时,就很难改变它,拆分为两个等等。你通常会被它困住。这也妨碍了灵活性。研究一些依赖注入框架来缓解这个问题。

我认为这是c#的最健壮的版本:

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\n");
}


// 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-optimised版本:

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\n");
}


// 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找到这个模式。

单例对象的问题不在于它们的实现。而是它们合并了两个不同的概念,这两个概念显然都不可取。

1)单例提供了对对象的全局访问机制。尽管在没有定义良好的初始化顺序的语言中,它们可能更线程安全或更可靠,但这种用法在道义上仍然相当于全局变量。它是一个用一些笨拙的语法修饰的全局变量(比如说,foo::get_instance()而不是g_foo),但它具有完全相同的目的(在整个程序中可访问的单个对象),并且具有完全相同的缺点。

2)单例防止一个类的多个实例化。据我所知,这种特性很少被添加到类中。这通常是一个更有语境的东西;很多被认为是独一无二的东西实际上只是碰巧是独一无二的。在我看来,更合适的解决方案是只创建一个实例——直到您意识到需要多个实例为止。

当初始化和object时有很多代码正在运行时,单例非常方便。例如,当你使用iBatis设置一个持久化对象时,它必须读取所有的配置,解析映射,确保它是正确的,等等。在开始编写代码之前。

如果每次都这样做,性能会大大降低。在单例中使用它,你只执行一次命中,然后所有后续调用都不必执行它。

独生子女的真正缺点是他们打破了继承。你不能派生一个新的类来提供扩展的功能,除非你能访问引用单例的代码。因此,除了Singleton将使您的代码紧密耦合之外(可通过策略模式修复…)又名依赖注入),它也会阻止你关闭部分代码的修订(共享库)。

因此,即使日志记录器或线程池的示例也是无效的,应该由策略替换。

在桌面应用程序中(我知道,只有我们这些恐龙才会写这些!),它们对于获得相对不变的全局应用程序设置是必不可少的——用户语言,帮助文件的路径,用户首选项等等,否则就必须传播到每个类和每个对话框中。

编辑-当然这些应该是只读的!

我用Singletons作为面试测试。

当我让开发人员说出一些设计模式时,如果他们只能说出Singleton,他们就不会被录用。

但是当我需要像Singleton这样的对象时,我通常会使用施瓦兹的计数器来实例化它。

现代c++设计 by Alexandrescu有一个线程安全的,可继承的泛型单例。

对于我的2p价值,我认为为你的单例对象定义生命周期是很重要的(当绝对有必要使用它们时)。我通常不让静态get()函数实例化任何东西,并将设置和销毁留给主应用程序的某个专用部分。这有助于突出单例程序之间的依赖关系——但是,正如上面强调的,如果可能的话最好避免它们。

答:

在以下情况下使用单例:

  • 您需要在系统中有且只有一个类型的对象

在以下情况下不要使用单例:

  • 你想要节省内存
  • 你想尝试一些新的东西
  • 你想炫耀你知道多少
  • 因为其他人都在这么做(参见wikipedia中的Cargo cult程序员)
  • 在用户界面小部件中
  • 它应该是一个缓存
  • 在字符串
  • 在会话
  • 我可以玩一整天

如何创建最佳单例:

  • 越小越好。我是一个极简主义者
  • 确保它是线程安全的
  • 确保它永远不为空
  • 确保只创建一次
  • 懒惰还是系统初始化?符合您的要求
  • 有时操作系统或JVM为你创建单例(例如,在Java中每个类定义都是单例)
  • 提供析构函数或以某种方式计算出如何处理资源
  • 使用较少的内存

单身让你有能力在一个职业中结合两种坏特性。这在任何方面都是错误的。

单例可以给你:

  1. 对对象的全局访问,以及
  2. 保证不超过一个此类型可以被创造出来的对象

第一点很简单。全局变量通常不好。除非真的需要,否则决不应使对象具有全局可访问性。

第二点可能听起来很有道理,但让我们想想。上次你**不小心*创建了一个新对象而不是引用一个现有对象是什么时候?因为它被标记为c++,所以让我们使用该语言中的一个示例。你经常不小心写东西吗

std::ostream os;
os << "hello world\n";

当你打算写信的时候

std::cout << "hello world\n";

当然不是。我们不需要防范这种错误,因为这种错误根本不会发生。如果确实如此,正确的反应是回家睡12-20个小时,希望你能感觉好点。

如果只需要一个对象,只需创建一个实例。如果一个对象应该是全局可访问的,则将其设置为全局。但这并不意味着不可能创建它的其他实例。

“只有一个实例是可能的”约束并不能真正保护我们免受可能的错误。但是它使我们的代码很难重构和维护。因为我们经常发现晚些时候确实需要多个实例。我们有多个数据库,我们有多个配置对象,我们确实需要几个记录器。举一个常见的例子,我们的单元测试可能希望能够在每次测试中创建和重新创建这些对象。

因此,当且仅当我们需要这两个它所提供的特性时,应该使用单例:如果我们需要全局访问(这很少见,因为通常不鼓励使用全局)而且我们需要以防止任何人创建一个类的多个实例(这在我听起来像是一个设计问题)。我能看到的唯一原因是,创建两个实例是否会破坏我们的应用程序状态——可能是因为类包含许多静态成员或类似的愚蠢行为。在这种情况下,显而易见的答案是修复这个类。它不应该依赖于是唯一的实例。

如果你需要全局访问一个对象,把它设为全局的,比如std::cout。但是不要限制可以创建的实例的数量。

如果您确实需要将一个类的实例数量限制为一个,并且无法安全地处理创建第二个实例,那么就强制执行。但不要让它在全球范围内都可以使用。

如果你确实需要这两个特性,那么1)让它成为单例,2)让我知道你需要它做什么,因为我很难想象这样的情况。

  • 如何正确地实现单例

有一个问题我从未见人提起过,是我在之前的工作中遇到的。我们有在dll之间共享的c++单例,而通常的确保类的单个实例的机制就不起作用了。问题是每个DLL都有自己的一组静态变量,还有EXE。如果你的get_instance函数是内联的或者是静态库的一部分,那么每个DLL都会有它自己的“单例”副本。

解决方案是确保单例代码只定义在一个DLL或EXE中,或者创建一个具有这些属性的单例管理器来分配实例。

大多数人在试图让自己对使用全局变量感觉良好时使用单例变量。有合法的使用,但大多数时候,当人们使用它们时,只能有一个实例的事实,与它是全局可访问的事实相比,只是一个微不足道的事实。

因为单例只允许创建一个实例,所以它有效地控制了实例复制。例如,你不需要一个查找的多个实例——例如一个莫尔斯查找映射,因此将它包装在一个单例类中是合适的。仅仅因为你有一个类的单个实例并不意味着你对该实例的引用数量也有限制。您可以将调用排队(以避免线程问题)到实例,并实现必要的更改。是的,单例的一般形式是全局公共的,你当然可以修改设计来创建一个访问受限的单例。我以前没试过,但我确信这是可能的。 对于所有评论说单例模式是完全邪恶的人,你应该知道这一点:是的,如果你没有正确地使用它,或者在它有效的功能和可预测的行为范围内,它是邪恶的:不要泛化

正如其他人所注意到的,单例的主要缺点包括无法扩展它们,以及失去实例化多个实例的能力,例如用于测试目的。

单例对象的一些有用的方面:

  1. 延迟或预先实例化
  2. 适用于需要设置和/或状态的对象

但是,您不必使用单例来获得这些好处。您可以编写一个普通的对象来完成这项工作,然后让人们通过工厂(一个单独的对象)访问它。如果需要,工厂可以只考虑实例化一个,并重用它等等。同样,如果你对一个接口而不是一个具体的类编程,工厂可以使用策略,也就是说,你可以切换到和退出接口的各种实现。

最后,工厂可以使用依赖注入技术,如Spring等。

Meyers单例模式在大多数情况下工作得足够好,在某些情况下,寻找更好的模式并不一定值得。只要构造函数永远不会抛出,并且单例对象之间没有依赖关系。

单例对象是随处可访问的对象(从现在开始是GAO)的实现,尽管不是所有的GAO都是单例对象。

日志记录器本身不应该是单例的,但是理想情况下,记录日志的方法应该是全局可访问的,以便将日志消息的生成位置与日志记录的位置或方式分离。

延迟加载/延迟计算是一个不同的概念,单例通常也实现了这一点。它自身也有很多问题,尤其是线程安全问题,如果出现异常而失败,那么在当时看来是个好主意,结果却并不是那么好。(有点像字符串中的COW实现)。

考虑到这一点,goa可以像这样初始化:

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加载 -日志记录器可以订阅的消息处理程序,您可以向其发送消息。

另一个实现

class Singleton
{
public:
static Singleton& Instance()
{
// lazy initialize
if (instance_ == NULL) instance_ = new Singleton();


return *instance_;
}


private:
Singleton() {};


static Singleton *instance_;
};

我还是不明白为什么单例必须是全局的。

我将生成一个单例,其中我将一个数据库隐藏在类中作为私有常量静态变量,并使类函数利用数据库,而不向用户公开数据库。

我看不出这个功能有什么不好。

当我有一个封装大量内存的类时,我发现它们很有用。例如,在我最近开发的一款游戏中,我有一个影响映射类,它包含一个非常大的连续内存数组集合。我想在启动时全部分配,在关闭时全部释放,我肯定只想要它的一个副本。我还必须从很多地方访问它。我发现单例模式在这种情况下非常有用。

我相信还有其他的解决方案,但我觉得这个非常有用,而且很容易实现。

如果你是创建单例对象并使用它的人,不要将它设置为单例对象(这没有意义,因为你可以控制对象的奇点而不将其设置为单例对象),但如果你是一个库的开发人员,并且你只想向你的用户提供一个对象,这是有意义的(在这种情况下,你是创建单例对象的人,但你不是用户)。

单例对象所以使用它们作为对象,很多人访问单例直接通过调用方法返回它,但这是有害的因为你使你的代码知道对象是单身,我喜欢用单例对象,我通过构造函数传递它们,使用它们作为普通对象,通过这种方式,你的代码不知道是否这些对象是单例对象,这让依赖关系更加明确,它有助于重构……

下面是实现线程安全的单例模式的更好方法,在析构函数本身释放内存。但我认为析构函数应该是可选的,因为单例实例将在程序终止时自动销毁:

#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;
}
关于我们需要使用单例类的情况可以是- 如果我们想在整个程序执行过程中维护实例的状态 如果我们参与写入应用程序的执行日志,其中只需要使用文件的一个实例....等等。 如果有人能在我上面的代码中提出优化建议,那将是值得赞赏的