1. 什么叫单例模式:
简单的说,就是通过这种方法,可以控制某个类型的对象在程序的整个生命周期中只有一个实例。
2. 单例模式的应用场景:
在某些应用场景下,某个对象在整个程序的生命周期只需要一个就OK。比如线程池、缓存、日志对象等。
在不使用单例的时候,比如大家约定就用某个全局变量,也能达到类似的效果。但是有个缺点,比如说,全局对象在程序一开始就要初始化,若这个全局对象很耗资源,但是在这次运行过程中没有使用到这个对象,那么资源不就浪费了吗?
我们可以通过单例模式在需要对象的时候再去创建它,而不是在程序一开始就创建好(懒汉模式)
3. 单例模式的实现:
3.1 如何控制对象只有一个?
程序员可以创建一个对象,就可以创建多个对象,如果要保证对象只有一个,显然,外部是不能创建对象的,所以,构造函数不能为PUBLIC。若构造器是私有的,那么外部显然就不能够创建该类了。当我们需要对象时,可以如下使用:
Log* pLog = Log::getInstance(); 通过该方法去获取对象实例
注:getInstance方法为静态方法: static Log* getInstance();
private:
Log(void); //构造函数为私有
Log* Log::getInstance()
{
if (!m_pLog)
{
m_pLog = new Log();
}
return m_pLog;
}
通过上述代码,我们从逻辑上会觉得只会创建一个Log对象。但是在多线程下会有问题。
3.2 多线程互斥问题
在如下逻辑下:上述代码会有问题:
有两个线程, 线程A, 线程B
时间片1: A 判断m_pLog 是否为NULL, 结果为TRUE,进入if语句内部
时间片2: B抢占CPU, 也判断 m_pLog 是否为NULL, 结果为TRUE,进入if语句内部
时间片3: A 抢占CPU, 创建对象
时间片4: B抢占CPU, 创建对象
明明我们只想要一个对象,但是出现了两个,这不是我们想要的。我们只需要一个。
于是想到了用锁。将代码改成如下
添加锁变量:static Mutex m_mutex;
Log* Log::getInstance()
{
if (!m_pLog)
{
MutexLock mutexLock(&m_mutex);
m_pLog = new Log();
}
return m_pLog;
}
这个能够避免上述问题吗?只要两个线程都进入IF语句内部,加锁并不能解决会创建多个对象的问题。
这里用到一个方法就是:双重检查。
Log* Log::getInstance()
{
if (!m_pLog) //(1)
{
MutexLock mutexLock(&m_mutex); //(2)
if (!m_pLog) //(3)
{
m_pLog = new Log(); //(4)
}
}
return m_pLog;
}
在上述代码中,我们在上锁之后多加了一重判断: 在按照两个线程的逻辑走一遍:
时间片1:线程 A 进入 getInstance() 方法。
时间片2:由于(1)处 m_pLog 为 null,线程 A进入IF语句,并在(2)处 lock
时间片3:线程 B 抢占CPU。
时间片4:线程 B进入 getInstance() 方法。
时间片5:m_pLog 仍旧为 null,线程 B 试图获取锁。然而,由于线程 A持有该锁,线程B在(2)处阻塞。
时间片6:线程 A 抢占CPU。
时间片7:线程 A 执行,由于在(3)处判断m_pLog仍为 null,线程 A 在(4)处创建一个 Log对象并让 m_pLog指向该对象。
时间片8:线程 A 解锁并从 getInstance() 方法返回实例。
时间片9:线程 B 抢占CPU。
时间片10:线程 B 获取锁并在(3)处检查m_pLog 是否为 null。
时间片11:由于 m_pLog 是非 null 的,并没有创建第二个 Log对象,返回由线程A创建的Log对象。
这种方法只有在对象还没有创建的时候需要加锁,等创建第一次创建好了之后,就直接返回对象指针,并不会涉及锁的操作。
4. 使用单例模式实现的日志工具的实现源码:
先交代一下为什么会去做这个日志工具:
在测试时,如果是明显的BUG,我们通过VS工具,比如程序崩掉,我们可以通过中断,去查看堆栈情况,确认错误错在哪里,但是有些错误并不会如此容易的定位,也不报错,但是不符合逻辑,就需要我们通过日志工具去记录关键操作及相关参数,去分析可能存在的问题。
.h文件
#pragma once
#include <string>
#include "Mutex.h"
using namespace std;
class Log
{
private:
Log(void);
public:
~Log(void);
static Log* getInstance();
public:
void logToFile(string strFileName, int mode, char* szFormat, ...); //将日志信息输出到FILE
void logToScreen(char* szFormat, ...); //将日志信息输出到控制台
private:
static Log* m_pLog;
static Mutex m_mutex;
};
.cpp文件
#include "StdAfx.h"
#include "Log.h"
#include "DebugUtil.h"
#include <fstream>
#include "MacroDef.h"
#include <iostream>
using namespace std;
Log* Log::m_pLog = NULL;
Mutex Log::m_mutex;
Log::Log(void)
{
}
Log::~Log(void)
{
}
/************************************************************************
函数名:logToFile:
参数: [in] strFileName 输出的文件路径名
[in] mode 文件打开方式
[in] szFormat 格式内容
功能:把内容输出到文件中
/************************************************************************/
void Log::logToFile(string strFileName, int mode, char* szFormat, ...)
{
//LOG内容数组
char szLogContent[LOG_CONTENT_LEN];
memset(szLogContent, 0, LOG_CONTENT_LEN);
//时间戳存放数组
char szTime[100];
DebugUtil::GetTimeStamp(szTime, 100);
int nTimeLen = strlen(szTime);
int nLogCount = 0;
va_list pArgList;
va_start(pArgList, szFormat);
nLogCount = _vsnprintf(szLogContent, LOG_CONTENT_LEN, szFormat, pArgList);
va_end(pArgList);
fstream fileStream;
mode = mode | ios::app | ios::out;
MutexLock mutexLock(&m_mutex);
fileStream.open(strFileName.c_str(), mode);
fileStream.write(szTime, nTimeLen);
fileStream.write(szLogContent, nLogCount);
char cChangeLine = '\n';
fileStream.write(&cChangeLine, CHAR_SIZE);
fileStream.close();
};
void Log::logToScreen(char* szFormat, ...)
{
//LOG内容数组
char szLogContent[LOG_CONTENT_LEN];
memset(szLogContent, 0, LOG_CONTENT_LEN);
//时间戳内容存放数组
char szTime[100];
DebugUtil::GetTimeStamp(szTime, 100);
int nListCount = 0;
va_list pArgList;
va_start(pArgList, szFormat);
nListCount = _vsnprintf(szLogContent, LOG_CONTENT_LEN, szFormat, pArgList);
va_end(pArgList);
MutexLock mutexLock(&m_mutex);
cout<<szTime<<szLogContent<<endl;
};
Log* Log::getInstance() //获取LOG实例对象
{
if (!m_pLog)
{
MutexLock mutexLock(&m_mutex);
if (!m_pLog)
{
m_pLog = new Log();
}
}
return m_pLog;
}
注:
(1)本身单例模式有些陷阱,比如说在早起的JAVA中,双重检测有时候也是不行的。后面会讲述。
(2)在该Log类型中,使用了变参,后面也会讲一讲。