设计模式-单例模式

        单例模式是非常简单且基础的设计模式之一。麻雀虽小五脏俱全,在单例模式的设计过程中依然涉及到众多的知识点,而且一个优秀的单例模式并不是一蹴而就完成的,其中涉及到内存泄漏、多线程以及泛型编程等知识点。因此,本文从网上搜集了些许资料,逐步从简单的单例模式出发,对其进行了不断的优化。

        单例模式是指该类只能有一个对象。因此,该类的构造函数、析构函数、复制构造函数以及赋值运算符都必须是private的,并且需要提供一个创建对象的公共接口。由于在程序运行期间,这个对象必须一致存在,所以这个对象以及公共接口都必须是static性质的,只有当程序运行结束之后,static对象才会被销毁。

        根据对象创建的时机划分,主要分为懒汉模式以及饿汉模式。懒汉模式是指只有当调用公共接口的时候,我们才定义这个类对象;饿汉模式是指,我们在定义类的时候(在调用公共接口之前)就已经把这个类对象给定义出来了。

1 第一版:一个简单的单例模式

#include <iostream>

using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}

    static Singleton * instance;

public:
    static Singleton* init(){
        if(!instance){
            instance = new Singleton();
        }
        return instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();

int main(){
    Singleton::init()->show();
    return 0;
}

        上述代码就实现了一个简单的单例模式,但是这个单例模式存在两方面的缺点:内存泄漏与多线程的问题。由于在类的内部存在指针,而该类的析构函数是private,这就导致在程序结束的时候,不能够释放new出来的指针变量,从而导致了内存泄漏。多线程的问题是指当多个线程运行到init函数中,if语句中会判读当前是否需要创建新的对象,如果此时多个线程同时判断出需要创建一个新的对象,那么就会导致该程序创建多个对象,就违背了单例模式的初衷了。因此,后续的改进将主要针对这两点进行改进。

补充知识点:

1.1 局部变量、全局变量、静态局部变量以及静态全局变量的区别

           局部变量与静态局部变量:局部变量只在定义的代码块中有效,当程序执行完代码块之后就会被释放掉,局部变量保存在栈内存空间中。静态局部变量也是在定义的代码块中有效,但是在整个程序执行过程中,静态局部变量只会分配一次内存。

        全局变量与与静态全局变量:全局变量在整个工程文件中都可见,而且直到程序运行结束之后才会被销毁,保存在内存中的静态区域。静态全局变量只在定义的文件中可见,也保存在内存汇总的静态区域中。

1.2 静态成员变量与静态成员函数

        静态成员变量:多个对象公用一个静态成员变量,并且静态成员变量只能够在类外进行定义,或者在类内声明的时候就直接进行定义了。

        静态成员函数:静态成员函数表示该成员函数独立于对象之外,与对象的无关,可以直接通过类名进行调用。因此,静态成员函数只能只能调用静态成员函数,无法调用非静态成员函数。

2 第二版:atexit()解决内存泄漏问题

        由于Singleton类的析构函数是private的,因此无法在程序结束之前对new出来的指针进行释放,因此就需要额外提供一个函数来释放指针。atexit()函数就可以解决上述问题。

#include <iostream>
#include <stdlib.h>

using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}

    static Singleton * instance;

    static void destroy(){
        if(instance){
            delete instance;
            instance = nullptr;
        }
    }

public:
    static Singleton* init(){
        if(!instance){
            instance = new Singleton();
        }
        atexit(destroy);
        return instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();

int main(){
    Singleton::init()->show();
    return 0;
}

        在第二版代码中,添加了destroy()方法,并在init的时候指定在程序运行之前执行的destroy()函数,用于释放new出来的指针。这样,就会执行执行函数的析构函数,进行变量的释放,下面是程序执行的结果:

只是点:

2.1 什么时候会调用类的析构函数?

(1)当类对象销毁的时候:如果此时的析构函数是private的话,表明系统无法自动销毁new出来的指针变量。

(2) 使用delete关键字的时候:使用delete的时候会调用析构函数,与析构函数的属性无关。

(3)继承关系与包含关系:当B是A的子类或者B是A的一个成员变量的时候,当B销毁的时候,不仅会调用B的析构函数,而且还会调用A的析构函数。

3 第三版:借刀杀人解决内存泄漏问题

        由于我们希望在程序运行结束之后调用析构函数,因此我们可以在Singleton类中再创建一个子类,并且将该子类的对象作为Singleton的成员变量。在该类的析构函数中调用Singleton的析构函数,这样就会使得当该子类对象销毁的时候,就会调用Singleton的析构函数进行内存释放了。

#include <iostream>
#include <stdlib.h>

using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}

    static Singleton * instance;
    class Destroy{
        public:
        ~Destroy(){
            if(instance){
                delete instance;
                instance = nullptr;
            }
        }
    };
    static Destroy destroy;

public:
    static Singleton* init(){
        if(!instance){
            instance = new Singleton();
        }
        return instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
Singleton::Destroy Singleton::destroy;

int main(){
    Singleton::init()->show();
    return 0;
}

        有上述代码可知,我们借助Destroy类的析构函数“杀掉了”,这就是孙子兵法中的“借刀杀人”。

4 第四版:互斥量解决多线程的问题

        在多线程机制中,需要对创建对象的临界区进行加锁处理,而锁的颗粒度会直接影响整个程序运行的效率。如果直接将互斥锁加载if语句之前,而只有在第一次的时候才需要进行加锁,那么则会浪费很大的时间资源。因此,我们需要在if语句里面进行加锁处理。

#include <iostream>
#include <stdlib.h>
#include <mutex>

using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}

    static Singleton * instance;
    class Destroy{
        public:
        ~Destroy(){
            if(instance){
                delete instance;
                instance = nullptr;
            }
        }
    };
    static Destroy destroy;
    static mutex my_mutex;

public:
    static Singleton* init(){
        // lock_guard<mutex> my_lock(_mutex);
        // 效率太低,因为每次都需要判断,所以不必要上锁的次数太多
        if(!instance){
            lock_guard<mutex> my_lock(my_mutex);
            instance = new Singleton();
        }
        return instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

// 1. 懒汉模式
Singleton* Singleton::instance = nullptr;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
Singleton::Destroy Singleton::destroy;
mutex Singleton::my_mutex;

int main(){
    Singleton::init()->show();
    return 0;
}

        然而,上述代码依然存在一定的问题:比如当多个线程同时运行到lock_guard<mutex> my_lock(my_mutex)的时候,加入a线程创建一个实例并返回之后,b在a创建的过程中会一直堵塞在这里,一旦a结束之后,b就会执行下面的代码,再次创建一个实例。这显然是不符合单例初衷的。因此,还需要在加一层if语句,来判断到底需不需要再次创建一个实例对象。

if(!instance){
    lock_guard<mutex> my_lock(my_mutex);
    if(!instance){
        instance = new Singleton();
    }
}

        其中,lock_guard是管理互斥量mutex的对象,当lock_guard创建的时候,mutex就上锁了;当lock_guard对象销毁的时候,mutex就解锁了。就不需要我们在对mutex进行上锁与解锁了。

5 第五版:memory order解决cpu与编译器重排代码的问题

        在c++中,new一个对象并不是原子的操作,这其中涉及到:

(1)分配一个内存空间

(2)调用类的构造函数

(3)让指针指向内存空间进行赋值

        而在实际的运行过程中,cpu以及编译器会对上述三个步骤进行重排,所以实际中并不是严格按照上面三个步骤逐步进行运行的。这就会导致,线程在执行完(1)和(3)之后就返回了对象,但是步骤(2)并没有执行,这就会报错。因此,我们需要给new对象这一语句进行操作,之后当其严格完成三个步骤才能够之后后续的代码。这其中就涉及到内存重排的顺序:

#include <iostream>
#include <stdlib.h>
#include <mutex>
#include <atomic>


using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}
    class Destroy{
        public:
        ~Destroy(){
            if(instance){
                delete instance;
                instance = nullptr;
            }
        }
    };
    static atomic<Singleton *> instance;
    static Destroy destroy;
    static mutex my_mutex;

public:
    static Singleton* init(){
        // lock_guard<mutex> my_lock(_mutex);效率太低,因为每次都需要判断,所以不必要上锁的次数太多
        if(!instance){
            lock_guard<mutex> my_lock(my_mutex);
            Singleton* tmp = instance.load(memory_order_relaxed);
            atomic_thread_fence(memory_order_acquire);
            if(!instance){
                instance = new Singleton();
                atomic_thread_fence(memory_order_release);
            }
        }
        return instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

// 1. 懒汉模式
atomic<Singleton*> Singleton::instance;
// 2. 饿汉模式
// Singleton* Singleton::instance = new Singleton();
Singleton::Destroy Singleton::destroy;
mutex Singleton::my_mutex;

int main(){
    Singleton::init()->show();
    return 0;
}

6 第六版:局部静态变量解决上述两个问题

        从上面的内存泄漏以及多线程的问题不难发现,其主要原因是我们在串讲对象的时候使用到了指针的形式。因此,为了避免上述问题,我们可以使用变量的形式代替指针的形式,这样就可以有效的避免内存泄漏以及多线程的问题了。

        下面就是饿汉模式的代码,我们使用全局静态变量变量进行初始化。

#include <iostream>

using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}
    static Singleton instance;

public:
    static Singleton* init(){
        return &instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

// 1. 饿汉模式
Singleton Singleton::instance;


int main(){
    Singleton::init()->show();
    return 0;
}

        下面就是懒汉模型的代码,我们使用局部静态变量进行初始化:

#include <iostream>

using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}


public:
    static Singleton* init(){
        // 2.懒汉模式
        static Singleton instance;
        return &instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};


int main(){
    Singleton::init()->show();
    return 0;
}

        然而,在使用静态变量的时候,系统会随机初始化静态变量。如果两个类互相有对方的实例,那么在实际应用的过程中就会造成一个对象还未初始化的问题。而且,在懒汉模式下,局部静态变量的初始化也不是原子性的,也存在多线程的问题,该问题可参考这个博客

7 第七版:Boost库的终极版本-借花献佛

        在第三版中,我们就说过可以再创建一个类,在其析构函数中使用“借刀杀人”的方法调用private的构造函数。为了避免静态变量出现初始化顺序以及多线程的问题,我们同样可以使用“借花献佛”的思想,再创建一个类,在其构造函数中创建类的对象。

#include <iostream>

using namespace std;

class Singleton{
private:
    Singleton(){cout << "Singleton 构造函数" << endl;}
    ~Singleton(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton(const Singleton& _instance){}
    Singleton& operator= (const Singleton& _instance){}

    class Creator{
        public:
            Creator(){
                Singleton::init();
            };
    };
    static Creator creator;

public:
    static Singleton* init(){
        // 2.懒汉模式
        static Singleton instance;
        return &instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

Singleton::Creator Singleton::creator;

int main(){
    Singleton::init()->show();
    return 0;
}

        这主要的原因就是在main函数执行之前,就已经对static变量进行了初始化。而该类的模板类实现代码如下:

#include <iostream>

using namespace std;

template<typename T>
class Singleton{
protected:
    Singleton<T>(){cout << "Singleton 构造函数" << endl;}
    ~Singleton<T>(){
        cout << "Singleton 析构函数" << endl;
    }
    Singleton<T>(const Singleton<T>& _instance){}
    Singleton<T>& operator= (const Singleton<T>& _instance){}

    class Creator{
        public:
            Creator(){
                Singleton<T>::init();
            };
    };
    static Creator creator;


public:
    static T* init(){
        // 2.懒汉模式
        static T instance;
        return &instance;
    }
    void show(){
        cout << "singletion show function." << endl;
    }
};

class Instance{
public:
    Instance(){};
    ~Instance(){};
    void show(){
        cout << "instance show funcion" << endl;
    }
    friend class Singleton<Instance>;
};

int main(){
    Instance* instance = Singleton<Instance>::init();
    instance->show();
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值