浅析单例模式与线程安全(Linux环境c++版本)

什么是单例模式

单例模式是设计模式中一种常用模式,定义是Ensure a class has only one instance, and provide a global point of access to it.(确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例)

用《设计模式之禅》里的话说,就是,在一个系统中,要求一个类有且仅有一个对象,如果出现多个就会出现“不良反应”,比如以下情景可能会用到单例模式

  • 要求生成唯一序列号的环境
  • 在整个项目中需要一个共享访问点或者共享数据,例如一个Web页面上的计数器,可以不用把每次的刷新纪录都记录到数据库中,使用单例模式保持计数器的值(要涉及到稍后谈到的线程安全)
  • 创建一个对象需要都消耗资源过多,比如访问IO和数据库资源
  • 需要定义大量的静态常量和静态方法(如工具类)

单例模式的优点

  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的作用很明显
  • 减少了系统的性能开支,当一个对象的产生需要比较多的资源时,如读取数据,产生其他依赖对象时,可以通过在应用启动时直接产生一个单例对象,永驻内存
  • 避免对资源的多重占用,例如写一个文件动作,由于只有一个实例存在内存中,避免了对同一个资源的同时写操作
  • 可以在系统设置全局的访问点,优化共享资源,例如可以设计一个单例类,负责所有数据表的映射处理

单例模式实现

单线程实现

说了这么多,但是模式如何设计?

为了避免后面的派生类继承这个类,误将该类多次初始化(即内存中多个备份),我们应该将其构造函数设为私有,并且把唯一的一次初始化留到静态函数中,比如c++写出来是这样的

#include <iostream>

template<typename T>
class Singleton
{
public:
	static T& instance()
	{
		if (flag_init)
		{
			flag_init = false;
			flag_destory = true;
			init();
		}
		return *value_;
	}

	static void init()
	{
		value_ = new T();
	}

	static void destory()
	{
		if (flag_destory)
		{
			flag_destory = false;
			suicide();
		}
	}

	static void suicide()
	{
		delete value_;
	}

private:
  	static T* value_;       //只进行一次的初始化的指针
	static bool flag_init;     //表明是否可以进行初始化的标志位
	static bool flag_destory; //表明是否进行过销毁的标志位

	Singleton(){}            //私有函数,导致无法多次初始化这个类
	~Singleton(){}           //私有函数,导致无法多次销毁这个类
};

//静态变量必须在外面初始化
template<typename T>
T* Singleton<T>::value_ = NULL;

template<typename T>
bool Singleton<T>::flag_destory = false;

template<typename T>
bool Singleton<T>::flag_init = true;

//测试单例是否可用的测试类
class Example
{
public:
	Example(){value = 0;}
	~Example(){}

	void tool()
	{
		value++;
		std::cout << value<< std::endl;
	}
private:
	int value;
};

int main(int argc, char *argv[])
{
	Example& ex1 = Singleton<Example>::instance();
	ex1.tool();

	Example& ex2 = Singleton<Example>::instance();
	ex2.tool();

	Singleton<Example>::destory();
	Singleton<Example>::destory();

	return 0;
}

输出两次分别是1 和 2 表示操纵的是同一个对象,内存中只有一份,多次销毁,无效的销毁也会被忽略

多线程实现

但是这样就行了吗,如果在多线程会怎么样, 如果是多线程的话,在同时修改创建或者销毁的两个bool值时就可能发生错误。

那么怎么解决呢,有人提出了DCLP(double checked locking pattern)机制,结合互斥锁mutex,询问两次的方法。

分析:

当多个线程同时进入instance()时,都会发现第一个if (value_ == NULL)为真,之后开始竞争,拿到锁的线程会直接通过第二个if (value_ == NULL)进行初始化,然后释放锁,其他的线程拿到之后会到达第二个if ,此时实例已经被初始化,直接返回实例,不会进行二次初始化

实现如下

<span style="font-size:12px;">#include <iostream>
#include <unistd.h>
#include "Mutex.h"

template<typename T>
class Singleton
{
public:
	static T& instance()
	{
		if (value_ == NULL)
		{
			MutexLockGuard guard(mutex_); //这个表示区域锁,实现后附代码,原理参见《Linux多线程服务端编程》陈硕著
			if (value_ == NULL)
			{
				init();
			}
		}
		return *value_;
	}

	static void init()
	{
		value_ = new T();
	}

private:
  	static T* value_;

	static MutexLock mutex_;

	Singleton(){}
	~Singleton(){}
};

//静态变量必须在外面初始化
template<typename T>
T* Singleton<T>::value_ = NULL;
template<typename T>
MutexLock Singleton<T>::mutex_;

//测试单例是否可用的测试类
class Example : boost::noncopyable
{
public:
	Example(){value = 0;}
	~Example(){}

	void tool()
	{
		value++;
		std::cout << value<< " ";
	}
private:
	int value;
};

void* thread(void*arg)
{
	Example& ex3 = Singleton<Example>::instance();
	ex3.tool();

	return NULL;
}

int main(int argc, char *argv[])
{
	pthread_t tid;

	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);
	pthread_create(&tid, NULL, thread, NULL);

	Example& ex1 = Singleton<Example>::instance();
	ex1.tool();
	sleep(1);

	return 0;
}</span>

编译时记得加参数。。。-pthread

Mutex.h代码

运行检测,好像多线程也没什么问题,但是这样真的可以吗,国外大神过来打脸了

meyers大神的一篇文章

meyers指出 new Singleton,这步(也就是init()函数里的new)在真正运行时会分解成三个行为

  1. 分配内存
  2. 构造实例对象
  3. 将指针指向分配的内存

这三个指令可能会被CPU重排,然后执行顺序发生变化比如 3->1->2

这在一般情况下不会有异常,因为乱序执行就是cpu的一种优化手段(详情自行查阅,内容很多,不展开叙述),而且在外层有互斥锁的保护。但是,我们的互斥锁的保护是有条件的,只有先经过第一个if 判断才能进入互斥锁保护范围,而这个条件却被3所影响,倘若cpu先执行了3,这时另一个cpu同时进行 1处的判断,发现指针已不为空,直接返回对象供上层使用,而这时你返回的却是一个根本还没构造完毕的对象!

pthread_once解决DCLP问题

pthread_once()由Pthreads库保证,某个函数在多线程下只执行一次,实现出来是这样的:

#include <iostream>
#include <boost/noncopyable.hpp>

template<typename T>
class Singleton
{
public:
	static T& instance()
	{
		pthread_once(&ponce_, &Singleton::init);
		return *value_;
	}

	static void init()
	{
		value_ = new T();
	}

private:
	static pthread_once_t ponce_;
	static T* value_;
};

template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

template<typename T>
T* Singleton<T>::value_ = NULL;

有人会问了,pthread_once是怎么解决这个问题的呢,看源码把

<span style="font-size:12px;">int
__pthread_once (once_control, init_routine)
     pthread_once_t *once_control;
     void (*init_routine) (void);
{
  /* XXX Depending on whether the LOCK_IN_ONCE_T is defined use a
     global lock variable or one which is part of the pthread_once_t
     object.  */
  if (*once_control == PTHREAD_ONCE_INIT)
    {
      lll_lock (once_lock, LLL_PRIVATE);

      /* XXX This implementation is not complete.  It doesn't take
     cancelation and fork into account.  */
      if (*once_control == PTHREAD_ONCE_INIT)
    {
      init_routine (); 

      *once_control = !PTHREAD_ONCE_INIT;
    }   

      lll_unlock (once_lock, LLL_PRIVATE);
    }   

  return 0;
}</span>

看了以下,发现这个实现其实就是DCLP机制,不信你看

if (*once_control == PTHREAD_ONCE_INIT)出现了两次,原理上和之前我们自己写的差不多,那它为什么可靠呢,其实关键在于

<span style="font-size:12px;">  lll_lock (once_lock, LLL_PRIVATE);</span>

经过查询和查大神博客,这个是一个基于gcc内嵌指令的宏,不同硬件平台实现不一样,我们只记作用就好,作用是

  • 不要把这段指令和前面的指令重排,也就是前面的指令必须按序执行
  • 不要把变量缓存到寄存器

所以就避免了CPU指令乱序重排,有人对c++比较熟,会说那我给之前的变量加上volatile关键字就好了,对,但是linux下的c++,这里特指c++98标准的volatile很鸡肋,没有做到内存屏障的作用也就无法实现控制指令顺序的作用,所以自己很难实现,直接用pthread_once就好,如果是新的版本可以尝试最初的DCLP做法,这里有详细博文,我就不赘述了

c++11标准下DCLP

一些细枝末节

前面代码中出现了继承boost::noncopyable,这个是一个常用做法,noncopyable类把构造函数和析构函数设置成protected权限,这样子类可以调用,外面的类不能调用,说白了就是外面的调用者不能够通过赋值和copy构造新子类

    #include <boost/noncopyable>

    class Example: public boost::noncopyable
    {
    public:
        Example(){};
        Example(int i){};
    };  

    int main()
    {
        Example cl1();
        Example cl2(1);  

        //Example cl3(cl1);    // error
        //Example cl4(cl2);    // error  

        return 0;
    }  

最后奉上皓叔的单例模式讲解,这个是以java为例的

单例模式java

本文在阅读书籍和查阅网上资料完成,如有不足之处,请提出

参考资料:

《Linux多线程服务端编程》陈硕 著

《设计模式之禅》秦小波 著

时间: 2024-10-29 04:54:14

浅析单例模式与线程安全(Linux环境c++版本)的相关文章

python Linux 环境 (版本隔离工具)

python Linux 环境 (版本隔离工具) 首先新建用户,养成良好习惯useradd python 1.安装pyenv GitHub官网:https://github.com/pyenv/pyenv-installer pyenv installer This tool installs pyenv and friends. It is inspired by rbenv-installer. Prerequisites In general, compiling your own Pyt

Linux环境下线程消息同步的陷阱

我们程序中常常会使用到线程间的消息同步处理,比如以下一段伪码 var message = "": void func()  {   1. 启动线程Thread(该线程中填充message的内容):   2. 阻塞,直到等待到完成message填充的事件:   3. 处理message:   .... } void Thread()  {   1. 通过某种处理填充message:   2. 触发func中的阻塞事件: } 我们通常会使用条件变量来完成类似情况的线程同步处理 比如wind

Linux环境下线程的同步与互斥以及死锁问题

由于本次要讨论操作系统的死锁问题,所以必须先研究的是linux环境下的线程同步与互斥 先看下面的代码 大家猜想输出应该是什么呢? 结果是下面这个样子 好吧,似乎并没有什么区别... 那么下面再看这段代码(请无视并忽略屏蔽的内容...) 大家猜想正确的结果是什么呢?5000,10000? 好吧,或许你们都错了. 在运行了一段时间后,它的结果是这样的. 是不是又对又错? 为什么呢? 这就是因为程序中printf语句作用:本身是库函数,所以必须进行系统调用,必须进入内核进行切换,有很大概率形成数据的混

mosquitto在Linux环境下的部署/安装/使用/测试

mosquitto在Linux环境下的部署 看了有三四天的的源码,(当然没怎么好好看了),突然发现对mosquitto的源码有了一点点感觉,于是在第五天决定在Linux环境下部署mosquitto. 使用传统源码安装步骤: 步骤1:http://mosquitto.org/files/source/官网下载源码,放到Linux环境中.解压后,找到主要配置文件config.mk,其中包含mosquitto的安装选项,需要注意的是,默认情况下mosquitto的安装需要OpenSSL(一个强大的安全

C语言 linux环境基于socket的简易即时通信程序

转载请注明出处:http://www.cnblogs.com/kevince/p/3891033.html   By Kevince 最近在看linux网络编程相关,现学现卖,就写了一个简易的C/S即时通信程序,代码如下: head.h 1 /*头文件,client和server编译时都需要使用*/ 2 #include <unistd.h> 3 #include <stdio.h> 4 #include <sys/types.h> 5 #include <sys

Linux环境编程之IPC进程间通信(五):Posix消息队列1

对于管道和FIFO来说,必须应该先有读取者存在,否则先有写入者是没有意义的.而消息队列则不同,它是一个消息链表,有足够写权限的线程可往别的队列中放置消息,有足够读权限的线程可从队列中取走消息.每个消息都是一个记录,它由发送者赋予一个优先级.在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达.消息队列是随内核的持续性,一个进程可以往某个队列写入一些消息,然后终止,再让另外一个进程在以后的某个时刻读出这些消息.这跟管道和FIFO不一样,当一个管道或FIFO的最后一次关闭时

Spring单例模式与线程安全

问题背景 这段时间在做项目的时候,考虑到Spring中的bean默认是单例模式的,那么当多个线程调用同一个bean的时候就会存在线程安全问题.如果是Spring中bean的创建模式为非单例的,也就不存在这样的问题了. Spring 单例模式与线程安全 Spring 框架里的 bean ,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方. 单例模式的意思就是只有一个实例.单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个类称为单例类.

6 单例模式与线程池

做j2ee如果不懂单例模式和线程池慢慢就是白学了. 线程池到处都能看到,httpsession,数据库连接池,redis连接池,MQ连接池... 使用场景:频繁使用且创建本消耗高 多线程环境下,使用场景随处可见... 饿汉式和懒汉式 名词懒得解释了... 懒汉式需要双重锁定 饿汉式没线程安全问题 1,双锁单例模式,懒汉式 1 /// 定义一个全局访问点 2 /// 设置为静态方法 3 /// 则在类的外部便无需实例化就可以调用该方法 4 public static Singleton GetIn

Linux环境编程之进程(一):main函数调用、进程终止以及命令行参数和环境表

(一)main函数调用 main函数作为程序运行时的入口函数,它是如何被调用的呢?首先必须清楚一点,main函数也是一个函数,它只有被调用才能够执行.其实,在执行可执行程序时,在调用main函数之前,内核会先调用一个特殊的启动例程,将此启动例程作为可执行程序的起始地址.启动例程是如何作为可执行程序的起始地址的?这是由链接编译器设置的,而链接编译器则是由C编译器(如gcc编译器)调用的.启动例程作为可执行程序的起始地址主要做哪些工作呢?启动例程从内核取得命令行参数和环境变量值,以此来为main函数