C++学习之路: 单例模板

引言:

1.单例模式的目的:确保一个类只有一个实 例,并提供对该实例的全局访问。

2. 单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

单例模式 如何实现只有一个实例?? 禁用拷贝构造函数,防止拷贝。

那么还可以通过显式定义来 定义多个实例。

如  NonCopyable a, b; 虽然禁止拷贝,但是可以定义多个对象。

所以我们把构造函数也设为私有,这样就不能通过显式定义多个对象了。

 

那么如果把构造函数设为私有, 该如何实例化这个类呢? 我们之前不是学过 如果把构造函数设为私有, 这个便不可使用,那么不就没有意义了吗?

该如何解决这个问题呢?  采用曲线救国的方式:在类内部定义一个static 全局函数, 让它来替我们调用构造函数, 因为该函数在类的内部, 所以可以访问私有的 构造函数,又因为是static函数, 所以也可以再类外部直接调用。

 

既然static静态函数在外部直接调用, 那么是否可以定义多个对象呢?

例如 1.getInstrance()

       2.getInstrance()     这样调用两次不就等于创建两个对象了吗? 

  假如 类内部定义一个Singleton*pInstance_, 一个指向自身类的 对象指针那么我们便可以通过

  static getInstrance()来动态创建一个自身类 的对象了。

  { static pInstance_ = new Singleton; } 如此便实现了我们的要求了。

  但是也带来了问题, 我们多次调用getInstance()便可以创建多个对象,

  如果pInstance_ 设置为非static, 在实例化为一个对象之前, 它是不存在的,想想 如果我们没有定义一个string s,

不存在对象,就不存在pInstance_,那么如何调用里面的成员呢?   答案是 static 成员, 不管这个是否存在一个对象, 我们都可以访问static成员。 static成员的存在 不依赖对象。 只有该类存在, 该指针就存在。

单例模式需要解决的问题:

 

a)      把构造函数设为私有,禁用赋值和复制。带来的问题:main中无法随意生成对象

b)      提供一个static函数绕过构造函数为private的限制。问题:对象不唯一。

c)      设置一个static指针,每次先判断是否为NULL(防止重复new出新的对象导致内存泄露)。此时实现了一个简单的单例模式。但是此时在多线程环境下不唯一。

 

1. 如何构造不可复制的类。

 1 #include <iostream>
 2 #include "Thread.h"
 3 #include <stdlib.h>
 4 using namespace std;
 5 
 6 //多线程下具有隐患
 7 class Singleton
 8 {
 9 public:
10     static Singleton *getInstance()
11     {
12         if(pInstance_ == NULL) //线程的切换
13         {
14             ::sleep(1);
15             pInstance_ = new Singleton;
16         }
17             
18         return pInstance_;
19     }
20 private:
21     Singleton() { }
22 
23     static Singleton *pInstance_; //静态成员可以再外部直接调用
24 };
25 
26 Singleton *Singleton::pInstance_ = NULL; //这一行并不是调用,而是定义, 如果在main中这样写显然是不行的,因为pInstance_是一个私有成员
27 
28 
29 class TestThread : public Thread
30 {
31 public:
32     void run()
33     {
34         cout << Singleton::getInstance() << endl;
35         cout << Singleton::getInstance() << endl;
36     }
37 };
38 
39 int main(int argc, char const *argv[])
40 {
41     //Singleton s; ERROR
42 
43 
44     //测试证明了多线程下本代码存在竞争问题
45 
46     TestThread threads[12];
47     for(int ix = 0; ix != 12; ++ix)
48     {
49         threads[ix].start();
50     }
51 
52     for(int ix = 0; ix != 12; ++ix)
53     {
54         threads[ix].join();
55     }
56     return 0;
57 }

 

 1 0xb1300468
 2 0xb1300498
 3 0x9f88728
 4 0xb1300498
 5 0xb1300478
 6 0xb1300498
 7 0xb1100488
 8 0xb1300498
 9 0xb1300488
10 0xb1300498
11 0xb1300498
12 0xb1300498
13 0x9f88738
14 0xb1300498
15 0x9f88748
16 0xb1300498
17 0xb1100478
18 0xb1300498
19 0xb1100498
20 0xb1300498
21 0xb1100468
22 0xb1300498
23 0xb11004a8
24 0xb11004a8

多线程条件下打印出的结果。 多个对象其地址不同, 说明多线程new出了多个 对象,导致内存泄露。

 

 

上述代码在多线程下回出现明显的 bug,   如果两个线程同时运行getInstance_ ,且同时进入判断 pInstance_ 为空, 导致new 出多个Singleton对象, 但是pInstance_ 只能指向其中一个, 那么就导致了内存的泄露。

 

2. 为了解决线程同时访问pInstance_的问题, 我们给它加上互斥锁mutex;

 

 1 #include <iostream>
 2 #include "MutexLock.h"
 3 #include "Thread.h"
 4 #include <stdlib.h>
 5 #include <stdio.h>
 6 #include <string.h>
 7 #include <sys/time.h>
 8 using namespace std;
 9 
10 int64_t getUTime();
11 
12 //多线程下具有隐患
13 class Singleton
14 {
15 public:
16     static Singleton *getInstance()
17     {
18         mutex_.lock();
19         if(pInstance_ == NULL) //线程的切换
20             pInstance_ = new Singleton;
21         mutex_.unlock();
22         return pInstance_;
23     }
24 private:
25     Singleton() { }
26 
27     static Singleton *pInstance_;
28     static MutexLock mutex_;
29 };
30 
31 Singleton *Singleton::pInstance_ = NULL;
32 MutexLock Singleton::mutex_;
33 
34 class TestThread : public Thread
35 {
36 public:
37     void run()
38     {
39         const int kCount = 1000 * 1000;
40         for(int ix = 0; ix != kCount; ++ix)
41         {
42             Singleton::getInstance();
43         }
44     }
45 };
46 
47 int main(int argc, char const *argv[])
48 {
49     //Singleton s; ERROR
50 
51     int64_t startTime = getUTime();
52 
53     const int KSize = 100;
54     TestThread threads[KSize];
55     for(int ix = 0; ix != KSize; ++ix)
56     {
57         threads[ix].start();
58     }
59 
60     for(int ix = 0; ix != KSize; ++ix)
61     {
62         threads[ix].join();
63     }
64 
65     int64_t endTime = getUTime();
66 
67     int64_t diffTime = endTime - startTime;
68     cout << "cost : " << diffTime / 1000 << " ms" << endl;
69 
70     return 0;
71 }
72 
73 
74 
75 int64_t getUTime()
76 {
77     struct timeval tv;
78     ::memset(&tv, 0, sizeof tv);
79     if(gettimeofday(&tv, NULL) == -1)
80     {
81         perror("gettimeofday");
82         exit(EXIT_FAILURE);
83     }
84     int64_t current = tv.tv_usec;
85     current += tv.tv_sec * 1000 * 1000;
86     return current;
87 }

我们在线程访问pInstance_之前加上了互斥变量,  那么每次只能有一个线程访问到该指针, 所以避免了new出多个Singleton对象导致内存泄露的问题。   

但是这样也导致了一个问题,  每个线程都要抢到锁以后才能判断 是否需要创建一个 Singleton对象, 其中抢到锁以后,其他线程都堵塞在mutex_那行代码, 浪费了大量的CPU系统资源。

 

3. 我们采用 DCLP(double-check-locking-pattern)来避免线程资源的浪费。

 

 1 #include <iostream>
 2 #include "MutexLock.h"
 3 #include "Thread.h"
 4 #include <stdlib.h>
 5 #include <stdio.h>
 6 #include <string.h>
 7 #include <sys/time.h>
 8 using namespace std;
 9 
10 int64_t getUTime();
11 
12 //多线程下具有隐患
13 class Singleton
14 {
15 public:
16     static Singleton *getInstance()
17     {
18         if(pInstance_ == NULL)
19         {
20             mutex_.lock();
21             if(pInstance_ == NULL) //线程的切换
22                 pInstance_ = new Singleton;
23             mutex_.unlock();
24         }
25         
26         return pInstance_;
27     }
28 private:
29     Singleton() { }
30 
31     static Singleton *pInstance_;
32     static MutexLock mutex_;
33 };
34 
35 Singleton *Singleton::pInstance_ = NULL;
36 MutexLock Singleton::mutex_;
37 
38 class TestThread : public Thread
39 {
40 public:
41     void run()
42     {
43         const int kCount = 1000 * 1000;
44         for(int ix = 0; ix != kCount; ++ix)
45         {
46             Singleton::getInstance();
47         }
48     }
49 };
50 
51 int main(int argc, char const *argv[])
52 {
53     //Singleton s; ERROR
54 
55     int64_t startTime = getUTime();
56 
57     const int KSize = 100;
58     TestThread threads[KSize];
59     for(int ix = 0; ix != KSize; ++ix)
60     {
61         threads[ix].start();
62     }
63 
64     for(int ix = 0; ix != KSize; ++ix)
65     {
66         threads[ix].join();
67     }
68 
69     int64_t endTime = getUTime();
70 
71     int64_t diffTime = endTime - startTime;
72     cout << "cost : " << diffTime / 1000 << " ms" << endl;
73 
74     return 0;
75 }
76 
77 
78 
79 int64_t getUTime()
80 {
81     struct timeval tv;
82     ::memset(&tv, 0, sizeof tv);
83     if(gettimeofday(&tv, NULL) == -1)
84     {
85         perror("gettimeofday");
86         exit(EXIT_FAILURE);
87     }
88     int64_t current = tv.tv_usec;
89     current += tv.tv_sec * 1000 * 1000;
90     return current;
91 }

以上代码和2的不同在于, 抢锁之前先进行一次判断pInstance_是否为空, 如果不为空那么不需要再抢锁了直接return原指针, 避免堵塞。

那么为什么抢到锁之后还有再判断一次 该指针是否为空呢??  因为在最初的情况下(尚未new出一个对象时的零界条件下), 如果有2个线程同时通过了第一个判断, 一个抢到锁, 另一个被锁堵塞,

如果锁后面无判断 指针是否为空, 依然导致 内存泄露, 因为被堵塞的线程在其后抢到锁 依然会执行new, 导致内存泄露。

 

所以 采用 DCLP, 我们想象一下, 当Single同被创建出来, pInstance_ == NULL 始终为假, 使判断后面的 抢锁和再判断 短路,这样既可以保证安全, 又保证了效率。

 

以上 

 

posted @ 2014-10-09 20:50  tilly_chang  阅读(315)  评论(0编辑  收藏  举报