单例模式

理解单例模式

什么是单例模式?为什么要使用单例模式?

单例模式是保证类只有一个实例,并提供一个访问该实例的全局节点。该实例被所有程序模块共享。

单例模式是为了解决如下两个问题:

  1. 控制类实例的个数,保证类只有一个实例。例如对于数据库或者文件这种共享资源,保证资源类只有一个实例实际上就是控制了访问权限,同时时间只能有一个应用程序去访问;

  2. 有时候我们会使用全局变量去存储一些信息,但是很不安全,而且不方便管理,因为任何代码、任何地点都有可能改变全局变量的内容。可以利用单例模式为代替全局变量提供一个全局访问的节点,而不是将解决同一个问题的代码分散在程序各处。

所有单例的实现都包含以下两个相同的步骤:

  • 构造函数和析构函数为私有类型,目的是禁止外部构造和析构。拷贝构造函数和赋值构造函数是私有类型,目的是禁止外部拷贝和赋值,确保实例的唯一性。

  • 新建一个静态构建方法作为“构建”函数 (作为获取实例的唯一接口)。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。单例模式分为如下三种:

  • 饿汉式(Eager Singleton)(线程安全,但是存在潜在风险)

    系统一运行,就初始化创建实例,当需要时,直接调用即可。这种方式本身就线程安全,没有多线程的线程安全问题。

  • 懒汉式 (Lazy Singleton)(需要加锁保证线程安全)

    系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。这种方式要考虑线程安全。

  • Meyers’ Singleton(线程不安全,最优雅)

接下来,我们来看—下上述三种单例模式的代码实现。

单例模式实现

饿汉式(Eager Singleton)

饿汉式单例模式是指单例实例在程序运行时被立即执行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Singleton
{
private:
// 唯一单实例对象指针
static Singleton instance;

private:
// 将其构造和析构成为私有的, 禁止外部构造和析构
Singleton() = default;
~Singleton() = default;

public:
static Singleton& getInstance() {
return instance;
}

// 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值(这里使用 `delete` 关键字也能达到一样的效果)
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete ;
}

// 立即执行初始化
Singleton Singleton::instance;

由于在main函数之前初始化,所以没有线程安全的问题。但是潜在问题在于no-local static对象 (函数外的static对象) 在不同编译单元中的初始化顺序是未定义的。也即,static Singletoninstance;static Singleton& getInstance()二者的初始化顺序不确定,如果在初始化完成之前调用getInstance()方法会返回一个未定义的实例。

懒汉式(Lazy 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
struct Singleton
{
public:
static Singleton* getInstance()
{
if(instance == nullptr)
{
instance = new Singleton;
}
return instance;
}

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
Singleton() = default;
~Singleton() = default;

private:
static Singleton* instance;
};

Singleton* Singleton::instance = nullptr;
  • 问题1:Lazy Singleton存在内存泄露的问题,有两种解决方法:
  1. 使用智能指针
  2. 使用静态的嵌套类对象

使用智能指针的代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Singleton
{
public:
static Singleton* getInstance()
{
if(instance.get() == nullptr)
{
instance.reset(new Singleton);
}
return instance.get();
}

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
Singleton() = default;

private:
static std::unique_ptr<Singleton> instance;
};
std::unique_ptr<Singleton> Singleton::instance;

使用静态的嵌套类对象代码示例:

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
// 使用静态的嵌套类对象
struct Singleton
{
public:
static Singleton* getInstance()
{
if(instance == nullptr)
{
instance = new Singleton;
}
return instance;
}

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

private:
struct Delector
{
~Delector()
{
if(Singleton::instance != nullptr)
{
delete Singleton::instance;
}
}
};
static Delector delector;

private:
Singleton() = default;

private:
static Singleton* instance;
};

Singleton* Singleton::instance = nullptr;
Singleton::Delector Singleton::delector;

在程序运行结束时,系统会调用静态成员delector的析构函数,该析构函数会删除单例的唯一实例。使用这种方法释放单例对象有以下特征:

  • 在单例类内部定义专有的嵌套类。

  • 在单例类内定义私有的专门用于释放的静态成员。

  • 利用程序在结束时析构全局变量的特性,选择最终的释放时机。

  • 问题2:懒汉式单例模式是线程不安全的,因此上述的代码示例都是线程不安全的,在多线程情况下会出现race condition。要使其线程安全,能在多线程环境下实现单例模式,我们首先想到的是利用同步机制来正确的保护我们的shared data。可以使用双检测锁模式(DCL: Double-CheckedLocking Pattern)

1
2
3
4
5
6
7
8
9
10
11
12
static Singleton* getInstance()
{
if(instance == nullptr)
{
Lock lock; // 基于作用域的加锁,超出作用域,自动调用析构函数解锁
if (instance == nullptr)
{
instance = new Singleton();
}
}
return instance;
}

注意,线程安全问题仅出现在第一次初始化(new)过程中,而在后面获取该实例的时候并不会遇到,也就没有必要再使用lock。双检测锁很好地解决了这个问题,它通过加锁前检测是否已经初始化,避免了每次获取实例时都要首先获取锁资源。

加入DCL后,其实还是有问题的,关于memory model。在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用getInstance()就有可能使用到一个不完全初始化的对象。换句话说,就是代码中第3行: if(instance == nullptr)和第8行instance = new Singleton();没有正确的同步,在某种情况下会出现new返回了地址赋值给instance变量而
Singleton此时还没有构造完全,当另一个线程随后运行到第3行时将不会进入if从而返回了不完全的实例对象给用户使用,造成了严重的错误。

C++11没有出来的时候,只能靠插入两个memory barrier (内存屏障) 来解决这个错误,但是C++11引进了memory model,提供了atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。
因此,在有了C++11后就可以正确的跨平台的实现DCL模式了,利用atomic,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
atomic<singleton*> Singleton::pInstance{ nullptr };

Singleton* Singleton::getInstance() {
Singleton* p = pInstance;
if (p == nullptr) {
lock_guard<mutex> lock{ mutW };
if ((p = pInstance) == nullptr) {
pInstance = p = new Widget();
}
}
return p;
}

Meyer’s Singleton

C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。在C++11标准下,《Effective C++》提出了一种更优雅的单例模式实现,使用函数内的 local static 对象。这样,只有当第一次访问getInstance()方法时才创建实例。这种方法也被称为Meyers’ Singleton。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;

public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};

C++静态对象的初始化

  1. non-local static对象 (函数外)

C++规定,non-local static对象的初始化发生在main函数执行之前,也即main函数之前的单线程启动阶段,所以不存在线程安全问题。但C++没有规定多个non-local static对象的初始化顺序,尤其是来自多个编译单元的non-local static对象,他们的初始化顺序是随机的。

  1. local static对象 (函数内)

对于local static对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。

C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。

C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static对象初始化完成。

工厂模式

工厂模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。工厂模式的终极目的是为了解耦,实现创建者和调用者的分离。

简单来说,使用了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
// 鞋子抽象类
class Shoes
{
public:
virtual ~Shoes() {}
virtual void Show() = 0;
};

// 耐克鞋子
class NiKeShoes : public Shoes
{
public:
void Show()
{
std::cout << "我是耐克球鞋,我的广告语:Just do it" << std::endl;
}
};

// 阿迪达斯鞋子
class AdidasShoes : public Shoes
{
public:
void Show()
{
std::cout << "我是阿迪达斯球鞋,我的广告语:Impossible is nothing" << std::endl;
}
};

// 李宁鞋子
class LiNingShoes : public Shoes
{
public:
void Show()
{
std::cout << "我是李宁球鞋,我的广告语:Everything is possible" << std::endl;
}
};
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
enum SHOES_TYPE
{
NIKE,
LINING,
ADIDAS
};

// 总鞋厂
class ShoesFactory
{
public:
// 根据鞋子类型创建对应的鞋子对象
Shoes *CreateShoes(SHOES_TYPE type)
{
switch (type)
{
case NIKE:
return new NiKeShoes();
break;
case LINING:
return new LiNingShoes();
break;
case ADIDAS:
return new AdidasShoes();
break;
default:
return NULL;
break;
}
}
};

简单工厂模式虽然简单明了,但是如果需要增加类的话,就得去修改生产工厂类的内容了,这就违反了开闭原则,所以衍生了工厂方法模式。

工厂方法模式

将产品生产分配给多个工厂,但是每个工厂只生产一种产品;

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
// 总鞋厂
class ShoesFactory
{
public:
virtual Shoes *CreateShoes() = 0;
virtual ~ShoesFactory() {}
};

// 耐克生产者/生产链
class NiKeProducer : public ShoesFactory
{
public:
Shoes *CreateShoes()
{
return new NiKeShoes();
}
};

// 阿迪达斯生产者/生产链
class AdidasProducer : public ShoesFactory
{
public:
Shoes *CreateShoes()
{
return new AdidasShoes();
}
};

// 李宁生产者/生产链
class LiNingProducer : public ShoesFactory
{
public:
Shoes *CreateShoes()
{
return new LiNingShoes();
}
};
  • 工厂方法模式的特点:

    • 工厂方法模式抽象出了工厂类,提供创建具体产品的接口,交由子类去实现。
    • 工厂方法模式的应用并不只是为了封装具体产品对象的创建,而是要把具体产品对象的创建放到具体工厂类实现。
  • 工厂方法模式的缺陷:

    • 每新增一个产品,就需要增加一个对应的产品的具体工厂类。相比简单工厂模式而言,工厂方法模式需要更多的类定义。
    • 一条生产线只能一个产品。

抽象工厂模式

将产品生产分配给多个工厂,每个工厂可以生产多种产品;

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
// 基类 衣服
class Clothe
{
public:
virtual void Show() = 0;
virtual ~Clothe() {}
};

// 耐克衣服
class NiKeClothe : public Clothe
{
public:
void Show()
{
std::cout << "我是耐克衣服,时尚我最在行!" << std::endl;
}
};

// 基类 鞋子
class Shoes
{
public:
virtual void Show() = 0;
virtual ~Shoes() {}
};

// 耐克鞋子
class NiKeShoes : public Shoes
{
public:
void Show()
{
std::cout << "我是耐克球鞋,让你酷起来!" << std::endl;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 总厂
class Factory
{
public:
virtual Shoes *CreateShoes() = 0;
virtual Clothe *CreateClothe() = 0;
virtual ~Factory() {}
};

// 耐克生产者/生产链
class NiKeProducer : public Factory
{
public:
Shoes *CreateShoes()
{
return new NiKeShoes();
}

Clothe *CreateClothe()
{
return new NiKeClothe();
}
};
  • 抽象工厂模式的特点:
    提供一个接口,可以创建多个产品族中的产品对象。如创建耐克工厂,则可以创建耐克鞋子产品、衣服产品、裤子产品等。

  • 抽象工厂模式的缺陷:
    同工厂方法模式一样,新增产品时,都需要增加一个对应的产品的具体工厂类。