常用设计模式(单例模式,工厂模式)

常用设计模式

  常用的设计模式总共有七中:单例模式、工厂方法模式、抽象工厂模式、代理模式、装饰器模式、观察者模式和责任链模式。本文单例和工厂模式的描述引用了这篇文章的说法,具体的设计模式划分准则和设计原则可以看这篇文章。设计模式的目的是为了:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性。
  单例模式:一个类只有一个实例,且该类能自行创建这个实例的一种模式
   ①单例类只有一个实例对象
   ②该单例对象必须由单例类自行创建
   ③单例类对外提供一个访问该单例的全局访问点
   ④、优点
   单例模式可以保证内存里只有一个实例,减少了内存的开销。
   可以避免对资源的多重占用。
   单例模式设置全局访问点,可以优化和共享资源的访问。
   ⑤、缺点
   单例模式一般没有接口,扩展困难。
   单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
   通俗来讲单例模式就是为了避免不一致状态,比如打印机同时只能打印一个东西,操作系统中只能有一个文件系统等等。
  懒汉模式:在第一次使用的时候才进行初始化,达到了懒加载的效果;

class Singleton{
private:
	Singleton();
	Singleton(const Singleton& other);
public:
	static Singleton* getInstance();
	static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
//线程非安全版本
Singleton* Singleton::getInstance(){
	if(m_instance == nullptr){
		m_instance = new Singleton();
		}
		return m_instance;
}

  这是一个线程非安全的版本,有可能会出现new了多个实例。之后就有了懒汉模式的双重检测机制(DCL)的写法。采用双重检测防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查),当某一线程获得锁创建一个m_Instance对象时,即已有引用指向对象,lazyMan不为空,从而保证只会创建一个对象。

class Singleton
{
public:
	static Singleton* GetInstance() {
	// 注意这里一定要使用Double-Check的方式加锁,才能保证效率和线程安全
		if (nullptr == m_Instance) {
			m_mtx.lock();
			if (nullptr == m_Instance) {
				m_pInstance = new Singleton();
			}
			m_mtx.unlock();
		}
		return m_Instance;
	}// 实现一个内嵌垃圾回收类
	class CGarbo {
	public:
		~CGarbo(){
			if (Singleton::m_Instance)
				delete Singleton::m_Instance;
		}
	};// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;
private:
	// 构造函数私有
	Singleton(){};
	// 防拷贝
	Singleton(Singleton const&) = delete;
	Singleton& operator=(Singleton const&) = delete;
	static Singleton* m_Instance; // 单例对象指针
	static mutex m_mtx; //互斥锁
};
Singleton* Singleton::m_Instance = nullptr;
Singleton::CGarbo Garbo;

  这是懒汉模式比较常见的一种写法。但是因为编译器优化,指令的执行顺序可能会reorder重新排序(CPU执行指令的层次,而且线程是在指令层次抢时间片),理想情况下是执行(分配内存,调用构造,赋值),可能出现的情况是分配内存,赋值,调用构造。比如在reorder的情况下:线程1走到赋值,但还没调用构造的阶段,而线程2进来判断m_instance,此时它已经被赋值所以不为空,这时候线程2就直接返回m_instance,但事实上它还没构造出来,通俗来讲就是双检查锁欺骗了线程2。较为安全的单例模式可以使用智能指针的方式或者是局部静态变量的方式去实现单例:智能指针实现单例。

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

  原因是C++ 11标准中新增了一个特性叫Magic Static:如果变量在初始化时,并发线程同时进入到static声明语句,并发线程会阻塞等待初始化结束。这样可以保证在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性,同时也避免了new对象时指令重排序造成对象初始化不完全的现象。并且相比较与使用智能指针以及mutex来保证线程安全和内存安全来说,这样做能够提升效率。
  饿汉模式:这种方式比较常用,但容易产生垃圾对象。优点:没有加锁,获取实例的静态方法没有使用同步所以执行效率会提高,没有线程安全问题。缺点:类加载时就初始化,浪费内存。它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

class Singleton{
public:
       static Singleton* getInstatce(){
              return &m_instance;
       }
private:
    Singleton(){}
    Singleton(Singleton const & single);
    Singleton& operator = (const Singleton& single);
    static Singleton* m_instance;
    class GC{
    public :
        ~GC(){
            // 销毁所有资源
            if (m_Instance != NULL ){
                 delete m_instance;
                 m_instance = NULL ;
             }
         }
     };
     static GC gc;
};
//类外初始化
Singleton* Singleton::m_instance = new Singleton;
Singleton::GC Singleton::gc;

  工厂方法模式:实例化对象不是用new,用工厂方法替代。将选择实现类,创建对象统一管理和控制。从而将调用者跟我们的实现类解耦。
  简单工厂模式:用来生产同一等级架构中的任意产品(对于增加新的产品,需要修改已有代码)在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例。接下来创建一个接口,两个实现类,一个工厂,一个测试类。

enum CTYPE {PHONEA, PHONEB};     
class SinglePhone    
{    
public:    
    virtual void Show() = 0;  
};    
//手机A    
class SinglePhoneA: public SinglePhone    
{    
public:    
    void Show() { cout<<"SinglePhone A"<<endl; }    
};    
//手机B 
class SinglePhoneB: public SinglePhone    
{    
public:    
    void Show() { cout<<"SinglePhone B"<<endl; }    
};    
//唯一的工厂,可以生产两种型号的手机,在内部判断    
class Factory    
{    
public:     
    SinglePhone* CreateSinglePhone(enum CTYPE ctype)    
    {    
        if(ctype == PHONEA) //工厂内部判断    
            return new SinglePhoneA(); //生产手机A    
        else if(ctype == PHONEB)    
            return new SinglePhoneB(); //生产手机B    
        else    
            return NULL;    
    }    
};

  我们通过创建一个PhoneFactory类,成功的完成工厂的创建。我们在创建对象时,也就不需要直接创建对象,而是可以通过创建工厂,这样大大的降低了代码的耦合性。但是,静态工厂模式是不能添加数据的。比如说,我们想添加一个“Oppo”手机类,你不直接修改PhoneFactory工厂代码,是不能实现的,就是说在增加新的手机类时要修改工厂类。所以,就有了第二种的工厂方法模式。
  工厂方法模式:用来生产同一等级架构中的固定产品,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。(支持增加任意产品)

class SinglePhone{    
public:    
    virtual void Show() = 0;  
};    
//手机A    
class SinglePhoneA: public SinglePhone{    
public:    
    void Show() { cout<<"SinglePhone A"<<endl; }    
};    
//手机B    
class SinglePhoneB: public SinglePhone {    
public:    
    void Show() { cout<<"SinglePhone B"<<endl; }    
};    
class Factory{    
public:    
    virtual SinglePhone* CreateSinglePhone() = 0;  
};    
//生产A手机的工厂    
class FactoryA: public Factory{    
public:    
    SinglePhoneA* CreateSinglePhone() { return new SinglePhoneA; }    
};    
//生产B手机的工厂    
class FactoryB: public Factory{    
public:    
    SinglePhoneB* CreateSinglePhone() { return new SinglePhoneB; }    
};

  创建了手机工厂接口Factory,再分别创建工厂A,B实现工厂,这样就可以通过工厂A,B创建对象。增加新的具体工厂和产品族很方便,比如说,我们想要增加小米,只需要创建一个小米工厂FactoryC实现手机工厂接口Factory,合理的解决的简单工厂模式不能修改代码的缺点。但是,在现实使用中,简单工厂模式占绝大多数,因为简单工程结构复杂度,代码复杂度,编程复杂度,管理复杂度都更好。
  既然有了简单工厂模式和工厂方法模式,为什么还要有抽象工厂模式呢?它到底有什么作用呢?还是举这个例子,这家公司的技术不断进步,不仅可以生产单核手机,也能生产多核手机。现在简单工厂模式和工厂方法模式都鞭长莫及。抽象工厂模式登场了。它的定义为提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。具体这样应用,这家公司还是开设两个工厂,一个专门用来生产A型号的单核多核处理器,而另一个工厂专门用来生产B型号的单核多核处理器,下面给出实现的代码:

class SinglePhone     
{    
public:    
    virtual void Show() = 0;  
};    
class SinglePhoneA: public SinglePhone      
{    
public:    
    void Show() { cout<<"Single Phone A"<<endl; }    
};    
class SinglePhoneB :public SinglePhone    
{    
public:    
    void Show() { cout<<"Single Phone B"<<endl; }    
};    
//多核手机    
class MultiCorePhone      
{    
public:    
    virtual void Show() = 0;  
};    
class MultiCorePhoneA : public MultiCorePhone      
{    
public:    
    void Show() { cout<<"Multi Core Phone A"<<endl; }    
};    
class MultiCorePhoneB : public MultiCorePhone      
{    
public:    
    void Show() { cout<<"Multi Core Phone B"<<endl; }    
};    
//工厂    
class PhoneFactory      
{    
public:    
    virtual SinglePhone* CreateSinglePhone() = 0;  
    virtual MultiCorePhone* CreateMultiCorePhone() = 0;  
};    
//工厂A,专门用来生产A型号的处理器    
class FactoryA :public PhoneFactory    
{    
public:    
    SinglePhone* CreateSinglePhone() { return new SinglePhoneA(); }    
    MultiCorePhone* CreateMultiCorePhone() { return new MultiCorePhoneA(); }    
};    
//工厂B,专门用来生产B型号的处理器    
class FactoryB : public PhoneFactory    
{    
public:    
    SinglePhone* CreateSinglePhone() { return new SinglePhoneB(); }    
    MultiCorePhone* CreateMultiCorePhone() { return new MultiCorePhoneB(); }    
}; 

web服务器项目中的日志系统实现:

  下面以社长的web服务器项目作为例子介绍下单例模式的使用过程。
  日志:本项目中,使用单例模式创建日志系统,对服务器运行状态、错误信息和访问数据进行记录,该系统可以实现按天分类,超行分类功能,可以根据实际情况分别使用同步和异步写入两种方式。其中异步写入方式,将生产者-消费者模型封装为阻塞队列,创建一个写线程,工作线程将要写的内容push进队列,写线程从队列中取出内容,写入日志文件。
  日志系统中使用了单例模式:单例模式作为最常用的设计模式之一,保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。实现思路:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。单例模式有两种实现方法,分别是懒汉和饿汉模式。顾名思义,懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;饿汉模式,即迫不及待,在程序运行时立即初始化。
  一种懒汉模式:这是在c++11之前的标准,需要加上锁,C++11之后的版本不需要加锁。

class single{
private:
    static pthread_mutex_t lock;
    single(){
        pthread_mutex_init(&lock, NULL);
    }
    ~single(){}

public:
    static single* getinstance();

};
pthread_mutex_t single::lock;
single* single::getinstance(){
    pthread_mutex_lock(&lock);
    static single obj;
    pthread_mutex_unlock(&lock);
    return &obj;
}

  饿汉模式:不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。所以是线程安全的,不需要在获取实例的成员函数中加锁。

class single{
private:
    static single* p;
    single(){}
    ~single(){}

public:
    static single* getinstance();

};
single* single::p = new single();
single* single::getinstance(){
    return p;
}

  饿汉模式存在的隐患是:在于非静态对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。
  日志中的阻塞队列是通过生产者消费者模型去实现的,当队列为空时,从队列中获取元素的线程将会被挂起;当队列是满时,往队列里添加元素的线程将会挂起。push相当于生产者,pop相当于消费者,在push时需要将使用队列的线程先唤起pthread_cond_broadcast(m_cond),这里利用了条件变量得广播函数,如果这时超出长度就唤起无效。
  主要的日志类创建的流程如下:通过局部变量的懒汉单例模式创建日志实例,对其进行初始化生成日志文件后,格式化输出内容,并根据不同的写入方式,完成对应逻辑,写入日志文件。在初始化日志文件时按照当前时间创建文件名设置队列大小(表示队列中可以放几个数据),队列大小为0就是同步否则为异步,如果是同步模式就直接格式化输出内容,不管是同步还是异步都需要对文件进行份文件判断(如果当前日志超过最大行数限制,就在当前日志末尾加一个后缀count/max_lines再创建新的日志)如果是异步就格式化输出内容,将内容写入阻塞队列,再创建一个写线程,从阻塞队列中取内容写入日志文件,如果是同步日志,那么日志写入函数与工作线程串行执行;写入日志文件的信息在格式化时会分级有Info还有Error和Fatal几个等级,并且写入的内容除了有时间还有当前的执行流程或是接收的信息或者是系统的错误。
  数据库的连接:在数据库连接操作中也是使用单例模式去实现的,系统访问数据库时,先是系统创建数据库连接,完成数据库操作,然后系统断开数据库连接,所以如果频繁的要访问数据库就要频繁的创建和断开数据库连接,但是创建数据库连接是比较耗时的,也容易对数据库造成安全隐患,所以一般使用池化的思想去解决。项目中使用了单例和链表去创建数据库连接池,实现对数据库连接资源的复用。项目中的数据库模块分为两部分,其一是数据库连接池的定义,其二是利用连接池完成登录和注册的校验功能。具体的,工作线程从数据库连接池取得一个连接,访问数据库中的数据,访问完毕后将连接交还连接池。在初始化时,连接池的销毁不能被外部调用,这里是通过RAII机制来完成自动释放的,而线程池争夺连接的同步机制是通过信号量实现的,所以将信号量初始化为数据库的连接总数。
  什么是RAII机制:RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源,详细可以看这篇文章。使用RAII机制可以很好的避免了为每一个new的内存空间都要分配delete,这会导致极度臃肿,效率低下,并且更容易发生内存泄漏(内存泄漏是指申请内存后,无法释放已经申请的内存空间,内存泄漏的堆积会导致内存被占用光;内存溢出是指申请内存时,没有足够的内存空间供使用)。
  首先会将数据库的内容user表中的用户名和密码存储到服务器上的map中,之后的登陆,注册会从map中取出数据进行比对。这里信息的校验使用了CGI校验(通用网关接口),它是一个运行在Web服务器上的程序,在编译的时候将相应的.cpp文件编程成.cgi文件并在主程序中调用即可。这些CGI程序通常通过客户在其浏览器上点击一个button时运行。这些程序通常用来执行一些信息搜索、存储等任务,而且通常会生成一个动态的HTML网页来响应客户的HTTP请求。我们可以发现项目中的sign.cpp文件就是我们的CGI程序,将用户请求中的用户名和密码保存在一个id_passwd.txt文件中,通过将数据库中的用户名和密码存到一个map中用于校验。在主程序中通过execl(m_real_file, &flag, name, password, NULL);这句命令来执行这个CGI文件,这里CGI程序仅用于校验,并未直接返回给用户响应。这个CGI程序的运行通过多进程来实现,根据其返回结果判断校验结果(使用pipe进行父子进程的通信,子进程将校验结果写到pipe的写端,父进程在读端读取)。
  对于接收到的请求用一个标志位m_url[2]去判断是GET还是POST,不同的请求他要跳转的页面是什么,以注册登陆的状态为例,先对用户名进行查找看是否有注册过,注册过就错误,否则正常进行注册,或异常注册失败。

        //判断map中能否找到重复的用户名
        if (users.find(name) == users.end())
        {
            //向数据库中插入数据时,需要通过锁来同步数据
            m_lock.lock();
            int res = mysql_query(mysql, sql_insert);
            users.insert(pair<string, string>(name, password));
            m_lock.unlock();

           //校验成功,跳转登录页面
            if (!res)
                strcpy(m_url, "/log.html");
            //校验失败,跳转注册失败页面
            else
                strcpy(m_url, "/registerError.html");
        }
        else
            strcpy(m_url, "/registerError.html");
    }

  同时项目中还使用的是SIGALRM信号来实现定时器,利用alarm函数周期性的触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环使用I/O复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来监测,从而实现统一处理。主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。处理的时候管道的写端是非阻塞的因为send是将信息发送给缓冲区,如果缓冲区满了,则会阻塞,这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞。但是没有对非阻塞返回值处理,所以如果阻塞就意味着这一次定时事件失效了,不过定时事件是非必须立即处理的事件,可以允许这样的情况发生。这里可以优化的地方是方每次遍历添加和修改定时器使用的是双向升序链表,效率偏低(O(n)),使用最小堆结构可以降低时间复杂度降至(O(logn))。
  还是用了Webbench进行了压测,原理是:父进程fork若干个子进程,每个子进程在用户要求时间或默认的时间内对目标web循环发出实际访问请求,父子进程通过管道进行通信,子进程通过管道写端向父进程传递在若干次请求访问完毕后记录到的总信息,父进程通过管道读端读取子进程发来的相关信息,子进程在时间到后结束,父进程在所有子进程退出后统计并给用户显示最后的测试结果,然后退出。

你可能感兴趣的:(单例模式,设计模式,java)