309

编辑:从另一个问题,我提供了一个答案,其中包含很多关于单身人士的问题/答案的链接:关于单身人士的更多信息:

所以我读过单例:好的设计还是拐杖?
争论仍在继续。

我将单例视为一种设计模式(好的和坏的)。

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

因此,需要回答的主要问题是:

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

我对这篇文章的希望是,我们可以在一个地方(而不是必须谷歌和搜索多个站点)收集何时(以及如何)正确使用 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;
        }
};

好的。让我们一起得到一些批评和其他实现。
:-)

4

24 回答 24

191

回答:

在以下情况下使用单例:

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

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

  • 你想节省内存
  • 你想尝试一些新的东西
  • 你想炫耀你知道多少
  • 因为其他人都在这样做(参见维基百科中的货物崇拜程序员)
  • 在用户界面小部件中
  • 应该是缓存
  • 在字符串中
  • 在会话中
  • 我可以去一整天

如何创建最佳单例:

  • 越小越好。我是一个极简主义者
  • 确保它是线程安全的
  • 确保它永远不会为空
  • 确保它只创建一次
  • 懒惰还是系统初始化?满足您的要求
  • 有时操作系统或 JVM 会为您创建单例(例如,在 Java 中,每个类定义都是单例)
  • 提供析构函数或以某种方式弄清楚如何处理资源
  • 使用很少的内存
于 2008-09-18T12:43:04.273 回答
82

单身人士让您能够在一个班级中结合两个不良特征。这在几乎所有方面都是错误的。

单身人士为您提供:

  1. 对对象的全局访问,以及
  2. 保证不能创建超过一个此类对象

第一个是直截了当的。全局变量通常很糟糕。除非我们真的需要它,否则我们永远不应该让对象全局可访问。

第二个可能听起来很有意义,但让我们考虑一下。您最后一次**意外*创建一个新对象而不是引用现有对象是什么时候?由于这是标记为 C++,让我们使用该语言的示例。你是不是经常不小心写

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

当你打算写

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

当然不是。我们不需要针对这种错误进行保护,因为这种错误不会发生。如果是这样,正确的反应是回家睡 12-20 小时,希望你感觉好些。

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

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

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

如果您需要对对象进行全局访问,请将其设为全局,例如std::cout. 但不要限制可以创建的实例数量。

如果您绝对肯定需要将类的实例数限制为一个,并且无法安全地处理创建第二个实例,那么请强制执行。但也不要让它在全球范围内可访问。

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

于 2009-04-17T18:47:59.673 回答
37

单例的问题不在于它们的实现。而是它们将两个不同的概念混为一谈,显然这两个概念都不是可取的。

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

2)单例防止类的多个实例化。很少见,IME,这种特性应该被纳入一个类。这通常是一个更具上下文的事情。许多被认为是唯一的事情实际上只是碰巧是唯一的。IMO 一个更合适的解决方案是只创建一个实例——直到您意识到您需要多个实例。

于 2008-09-17T19:27:56.633 回答
29

模式的一件事:不要一概而论。当它们有用时,它们都有所有情况,当它们失败时。

当您必须测试代码时,单例可能会令人讨厌。您通常会被该类的一个实例所困扰,并且可以选择在构造函数中打开一扇门或使用某种方法来重置状态等等。

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

它可能会使依赖关系跟踪变得更加困难。当一切都取决于你的单例时,更难改变它,分成两个,等等。你通常会坚持下去。这也妨碍了灵活性。研究一些依赖注入框架来尝试缓解这个问题。

于 2008-09-17T19:26:48.477 回答
14

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

Java 尤其使用单例来代替全局变量,因为所有东西都必须包含在一个类中。最接近全局变量的是公共静态变量,它们可以像全局变量一样使用import static

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

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

那么什么时候使用单例是好/坏呢?几乎确切地说,何时使用全局变量是好/坏。

于 2008-09-17T19:24:58.120 回答
6

Alexandrescu 的Modern C++ Design有一个线程安全的、可继承的泛型单例。

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

于 2008-09-18T12:29:04.667 回答
6
  • 如何正确实现单例

有一个问题我从未见过,我在以前的工作中遇到过。我们有在 DLL 之间共享的 C++ 单例,并且确保类的单个实例的通常机制不起作用。问题是每个 DLL 都有自己的一组静态变量以及 EXE。如果您的 get_instance 函数是内联函数或静态库的一部分,则每个 DLL 都会以自己的“单例”副本结束。

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

于 2009-04-17T18:50:59.493 回答
5

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

于 2008-09-17T19:20:13.687 回答
5

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

单例的一些有用方面:

  1. 惰性或预先实例化
  2. 对于需要设置和/或状态的对象很方便

但是,您不必使用单例来获得这些好处。您可以编写一个正常的对象来完成这项工作,然后让人们通过工厂(一个单独的对象)访问它。如果需要,工厂可以担心只实例化一个,然后重用它等。此外,如果您对接口而不是具体类进行编程,则工厂可以使用策略,即您可以切换进出接口的各种实现。

最后,工厂适用于 Spring 等依赖注入技术。

于 2009-09-25T16:30:14.083 回答
4

当您在初始化和对象时运行大量代码时,单例非常方便。例如,当您在设置持久性对象时使用 iBatis 时,它必须读取所有配置、解析映射、确保其全部正确等等,然后才能访问您的代码。

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

于 2008-09-17T19:28:22.590 回答
3

单例的真正失败在于它们破坏了继承。除非您有权访问引用 Singleton 的代码,否则您无法派生新类来为您提供扩展功能。因此,除了单例将使您的代码紧密耦合(可通过策略模式修复......也称为依赖注入)这一事实之外,它还将阻止您关闭代码部分的修订(共享库)。

所以即使是记录器或线程池的例子也是无效的,应该用 Strategies 代替。

于 2008-09-17T19:33:19.770 回答
3

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

于 2009-04-17T19:09:19.043 回答
3

因为单例只允许创建一个实例,所以它有效地控制了实例复制。例如,您不需要查找的多个实例 - 例如莫尔斯查找映射,因此将其包装在单例类中是恰当的。并且仅仅因为你有一个类的实例并不意味着你也受到对该实例的引用数量的限制。您可以对实例的调用进行排队(以避免线程问题)并进行必要的更改。是的,单例的一般形式是全局公共的,您当然可以修改设计以创建更多访问受限的单例。我以前没有厌倦过这个,但我确定这是可能的。对于所有评论说单例模式完全邪恶的人,您应该知道这一点:

于 2009-08-01T12:41:53.303 回答
2

但是当我需要像 Singleton 这样的东西时,我经常最终使用Schwarz Counter来实例化它。

于 2008-09-17T19:44:56.403 回答
2

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

#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;
}

Regarding the situations where we need to use singleton classes can be- If we want to maintain the state of the instance throughout the execution of the program If we are involved in writing into execution log of an application where only one instance of the file need to be used....and so on. It will be appreciable if anybody can suggest optimisation in my above code.

于 2018-08-11T20:00:27.997 回答
1

我使用 Singletons 作为面试测试。

当我要求开发人员说出一些设计模式时,如果他们只能说出 Singleton,那么他们不会被雇用。

于 2008-09-17T19:40:26.430 回答
1

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

我确信还有其他解决方案,但我发现这个解决方案非常有用且易于实施。

于 2017-10-18T20:45:59.063 回答
0

反使用:

过度使用单例的一个主要问题是该模式阻止了替代实现的轻松扩展和交换。无论在何处使用单例,类名都是硬编码的。

于 2008-09-17T19:26:16.777 回答
0

我认为这是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 优化版本

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找到此模式。

于 2008-09-17T19:27:22.390 回答
0

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 加载了哪些库 - 记录器可以订阅并且您可以向其发送消息的消息处理程序。专门用于信号处理程序。

于 2011-03-02T13:09:27.017 回答
0

我仍然不明白为什么单身人士必须是全球性的。

我打算生成一个单例,我将一个数据库作为私有常量静态变量隐藏在类中,并制作利用数据库的类函数,而无需将数据库暴露给用户。

我不明白为什么这个功能会不好。

于 2014-01-31T03:35:23.977 回答
0

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

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

于 2018-07-28T17:14:28.567 回答
-1

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

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

于 2008-09-17T19:39:40.410 回答
-1

另一种实现

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

        return *instance_;
    }

private:
    Singleton() {};

    static Singleton *instance_;
};
于 2012-01-07T17:10:33.110 回答