
目录
一、线程同步的概念
1.饥饿问题
线程互斥这种现象是正确的,因为如果没有线程互斥的话,不能保证多个线程访问临界资源时数据的一致性。但是线程互斥也是不合理的,多个线程中有可能存在一个线程优先级很高,一个线程优先级又很低,在这些线程竞争锁资源的时候,优先级很高的那个线程有可能一直都抢到了锁,偶尔让优先级比它低一点的线程抢到了锁,但优先级很低的那个线程却有可能一直都抢不到锁。
这其实就是饥饿问题,指的是存在一个执行流长时间得不到某种资源。线程互斥就有可能会导致饥饿问题。但互斥不是错误的,它只是不合理。线程互斥的使用要看使用的场景,它比较适合于运用在一种突发情况,我们要进行无任何优先级的竞争时,比如说抢票这种场景下,要求每个线程的优先级都相同,用线程互斥的方式来实现是可以的。
2.线程同步的概念
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。就比如在多个线程中有一个优先级很高的线程和一个优先级很低的线程,可能刚开始申请锁资源的时候确实是按照优先级来分配,先分配给优先级最高的线程,但当优先级最高的线程解锁了以后,这个优先级最高的线程不能立即重新申请锁,必须排在队列的末尾重新按顺序获取锁资源。(这就是排队的概念!)
3.条件变量
要想实现线程同步,我们就需要使用到条件变量,在没有使用条件变量的时候,所有线程都一窝蜂地去竞争锁资源,没有一个机制能够管理起这些线程来有序地竞争锁资源。所以条件变量是一种代码策略,我们使用条件变量以后,就可以控制哪些线程被唤醒去使用锁资源。下面我们先通过函数接口和模拟代码来简单地认识一下条件变量。
Linux操作系统下的条件变量是pthread_cond_t类型的,条件变量可以定义为全局的也可以定义为局部的,同样的,我们可以用 PTHREAD_COND_INITIALIZER 宏来进行初始化。
条件变量必须与互斥锁一并使用。
// 用宏初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
我们再看一下条件变量函数接口的使用:
pthread_cond_init函数
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t
*restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
pthread_cond_init函数是条件变量的初始化函数,一般用于定义局部条件变量的初始化。这个参数也是非常简单,第一个参数cond传递定义好的条件变量,第二个参数attr设置条件变量的属性,一般我们设置为nullptr代表默认即可。
pthread_cond_destroy函数

pthread_cond_destroy函数是释放条件变量的函数,在我们不再需要使用条件变量的时候,调用该函数将指定的条件变量释放,参数传递条件变量即可。
pthread_cond_wait

pthread_cond_wait函数可以设置线程等待条件满足,如果不满足的时候线程会阻塞式的等待。第一个参数cond传递指定的条件变量,代表在该条件变量上等待。第二个参数传递指定的互斥锁,这个函数需要传递互斥锁的原因是当线程在持有互斥锁的时候被阻塞等待条件变量,会自动解锁给其它线程使用,当阻塞结束的时候,该函数也会自动帮助线程重新获得互斥锁,然后才返回。
pthread_cond_broadcast
pthread_cond_broadcast函数也是唤醒在指定条件变量下等待的线程,只不过它是唤醒在等待的所有线程,参数也是传递指定的条件变量即可。

pthread_cond_signal函数

pthread_cond_signal函数与信号里的signal函数不同,pthread_cond_signal函数是用来唤醒某个线程的。它可以唤醒在指定条件变量下等待的线程。参数传递指定的条件变量即可。
咱们也可以根据后缀单词来区分。
4.生产者消费者模型
生产者消费者模型是一种编程模型,它结合我们现实生活中的例子就非常好理解了。
我们生活中如果要去超市购买东西,我们就属于是消费者,消费超市里的商品。而工厂就属于生产者,生产超市里的商品。因此超市就相当于是一个集散地,他聚集了周围的用户来这里购买商品,用户不需要到处去找厂家生产自己需要的商品。他也聚集了各种各样的厂商,为厂商生产的商品提供销售的机会,厂商不需要全国各地去找消费者来消费自己生产的商品。所以生产者消费者的这个模型,可以提高效率。生产者不用到处找消费者购买,消费者也不用到处找生产者生产,既提高了生产者的生产效率,也提高了消费者的消费效率。
除此之外,由于有了超市的存在,生产厂商在生产商品的时候,不需要等待有消费者来消费了才生产,他直接将商品卖给超市即可。消费者也不需要等生产厂商生产完了才能够购买,只要超市里还有存货就可以。所以生产者消费者模型还可以大大地降低生产者和消费者之间地耦合性,生产者做自己的事情,消费者也做自己的事情,两者之间不会因为对方的动作而受影响。
所以,经过这么一个例子,我们再来结合图片理解一下下面这段话就豁然开朗了。

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
- 所以在生产者消费者模型当中,生产者和消费者分别都是一批批的线程,超市就是临界资源。生产者与生产者之间是竞争关系,消费者与消费者之间也是竞争关系,所以站在线程的角度他们就是互斥的。
- 并且生产者与消费者之间也必须是互斥的,这样才能保证读写数据不会混乱。
- 同时生产者与消费者之间还必须是同步的。
在多线程编程中阻塞队列是一种常用于实现生产者和消费者模型的数据结构。阻塞队列与普通队列的区别在于如果阻塞队列为空的时候,从队列中获取数据会阻塞住;如果阻塞队列为满的时候,向队列中写入数据也会阻塞住。

接下来我们就简单实现一下,C++情况下queue模拟阻塞队列的生产消费模型:
BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
using namespace std;
const uint32_t defaultCap = 5; // 阻塞队列的默认容量
template <class T>
class BlockQueue
{
public:
BlockQueue(uint32_t capacity = defaultCap)
: _capacity(capacity)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_conCond, nullptr);
pthread_cond_init(&_proCond, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_conCond);
pthread_cond_destroy(&_proCond);
}
// 生产者生产接口
void push(const T& value)
{
// 1.需要加锁来保护阻塞队列
lockBlockQueue();
// 2.判断队列是否已满,如果满了生产者就不能生产了
while (isFull())
{
// 生产者等待消费者来消费商品以后被唤醒
proBlockWait();
}
// 3.队列还没有满,生产者可以继续生产
pushCore(value);
// 4.解锁
unlockBlockQueue();
// 5.生产者完成生产了,需要唤醒正在等待的消费者
conWakeUp();
}
// 消费者消费接口
T& pop()
{
// 1.需要加锁来保护阻塞队列
lockBlockQueue();
// 2.判断队列是否为空,如果是空的话消费者就不能消费了
while (isEmpty())
{
// 消费者等待生产者来生产商品以后被唤醒
conBlockWait();
}
// 3.队列不为空,消费者可以继续消费
T value = popCore();
// 4.解锁
unlockBlockQueue();
// 5.消费者完成消费了,需要唤醒正在等待的生产者
proWakeUp();
// 6.返回值
return value;
}
private:
// 封装加锁接口
void lockBlockQueue()
{
pthread_mutex_lock(&_mutex);
}
// 封装解锁接口
void unlockBlockQueue()
{
pthread_mutex_unlock(&_mutex);
}
// 判断队列是否已满
bool isFull()
{
return _bq.size() == _capacity;
}
// 判断队列是否为空
bool isEmpty()
{
return _bq.empty();
}
// 封装生产者等待接口
void proBlockWait()
{
pthread_cond_wait(&_proCond, &_mutex);
}
// 封装消费者等待接口
void conBlockWait()
{
pthread_cond_wait(&_conCond, &_mutex);
}
// 封装生产者唤醒接口
void proWakeUp()
{
pthread_cond_signal(&_proCond);
}
// 封装消费者唤醒接口
void conWakeUp()
{
pthread_cond_signal(&_conCond);
}
// 封装内部的入队列接口
void pushCore(const T& value)
{
_bq.push(value);
}
// 封装内部的出队列接口
T& popCore()
{
T value = _bq.front();
_bq.pop();
return value;
}
private:
queue<T> _bq; // 阻塞队列
uint32_t _capacity; // 阻塞队列的容量
pthread_mutex_t _mutex; // 阻塞队列的互斥锁
pthread_cond_t _conCond; // 消费者的条件变量
pthread_cond_t _proCond; // 生产者的条件变量
};
BlockQueueTest.cc
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include "BlockQueue.hpp"
// 生产者运行函数
void* proFunction(void* args)
{
BlockQueue<int>* bq = (BlockQueue<int>*)args;
while(true)
{
// 随机生成一个0-9的数据
int data = rand() % 10;
// 生产数据
bq->push(data);
cout << "生产者生产数据完成: " << data << endl;
sleep(2);
}
}
// 消费者运行函数
void* conFunction(void* args)
{
BlockQueue<int>* bq = (BlockQueue<int>*)args;
while(true)
{
int data = bq->pop();
cout << "消费者消费数据完成: " << data << endl;
}
}
int main()
{
// 创建一个阻塞队列
BlockQueue<int> bq;
// 创建一个随机数种子
srand((unsigned long)time(nullptr) ^ getpid());
// 分别创建一个生产者和消费者线程
pthread_t producer, consumer;
pthread_create(&producer, nullptr, proFunction, (void*)&bq);
pthread_create(&consumer, nullptr, conFunction, (void*)&bq);
// 回收新线程
pthread_join(producer, nullptr);
pthread_join(consumer, nullptr);
return 0;
}
生产者消费者模型还体现了并发性,原因是生产者向容器里放数据和消费者从容器里取数据是互斥的,但在生产者向容器里放数据之前生产者要生产数据,在消费者从容器里取数据之后消费者要处理数据,这两个过程是并发运行的。
5.POSIX信号量
信号量本质是一个计数器,是一个用来描述临界资源数量的计数器,申请信号量就是在预定某种临界资源,当我们申请信号量成功的时候,那个对应的信号量才可以被我们唯一地使用。
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突地访问共享资源的目的。但POSIX信号量可以用于线程间同步,SystemV信号量不能用于线程间同步。
POSIX信号量的函数接口使用起来非常简单,下面分别介绍一下POSIX信号量的函数接口:
sem_init函数

sem_init函数是信号量初始化函数,参数sem传递的是要初始化的信号量;参数pshared传递的是选项,0表示线程间共享,非零表示进程间共享;参数value表示信号量的初始值。
sem_destroy函数

sem_destroy函数是信号量销毁函数,参数只需要传递信号量即可。
sem_wait函数

sem_wait函数是等待信号量函数,它会将信号量的值减1,参数只需要传递信号量即可。
sem_post函数

sem_post函数用于发布信号量,表示资源使用完毕了,可以归还资源了,将信号量的值加1,参数只需要传递信号量即可。
下面我们用信号量实现一个
基于环形队列的生产者消费者模型

我们以前设计的环形队列有两种方式,第一种是永远都是预留出一个空位不存放数据,当头指针和尾指针指向同一位置的时候就代表队列为空,当尾指针的下一个位置是头指针的时候就代表队列为满。第二种是采用计数器的方式,用来判断队列是空还是满。但今天我们用信号量的方式来设计环形队列,就不再需要采用上面这两种方式了。信号量会帮我们控制环形队列的判断工作:
- 当头指针和尾指针指向同一个位置的时候,要么队列为空要么队列为满,此时两个线程是互斥并且同步的。
- 其它任何时候,两个线程都是并发的,放数据和取数据并发运行。
那么信号量如何来保证当队列为空的时候消费线程不能消费数据,当队列为满的时候生产线程不能生产数据呢?对于生产线程,它关心的是队列空间的数量,对于消费线程,它关心的是队列数据的数量。所以我们可以设置两个信号量分别为roomSem和dataSem分别表示空间信号量和数据信号量,当生产者生产数据的时候,空间信号量减1,数据信号量加1。当消费者消费数据的时候,空间信号量加1,数据信号量减1。这样就可以保证环形队列的正常运行。
所以,这里我们实现一个针对多生产线程和多消费线程的生产消费者模型:
RingQueue.hpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <semaphore.h>
#include <pthread.h>
using namespace std;
const u_int32_t cap = 10;
template <class T>
class RingQueue
{
public:
RingQueue(int capacity = cap)
: _ringQueue(capacity), _proIndex(0), _conIndex(0)
{
sem_init(&_roomSem, 0, _ringQueue.size());
sem_init(&_dataSem, 0, 0);
pthread_mutex_init(&_proMutex, nullptr);
pthread_mutex_init(&_conMutex, nullptr);
}
~RingQueue()
{
sem_destroy(&_roomSem);
sem_destroy(&_dataSem);
pthread_mutex_destroy(&_proMutex);
pthread_mutex_destroy(&_conMutex);
}
// 生产者放数据到环形队列中
void push(T &value)
{
// 生产者放一个数据,环形队列的空间数量就要减1
sem_wait(&_roomSem);
// 对临界资源进行加锁
pthread_mutex_lock(&_proMutex);
// 将数据写入环形队列的对应位置
_ringQueue[_proIndex] = value;
// 更新生产者写入位置的下标
_proIndex++;
_proIndex %= _ringQueue.size();
// 解锁
pthread_mutex_unlock(&_proMutex);
// 生产者放一个数据,环形队列的数据数量就要加1
sem_post(&_dataSem);
}
// 消费者从环形队列中拿数据
T &pop()
{
// 消费者从环形队列中拿一个数据,数据数量要减1
sem_wait(&_dataSem);
// 对临界资源进行加锁
pthread_mutex_lock(&_conMutex);
// 从环形队列中拿出一个数据
T value = _ringQueue[_conIndex];
// 更新消费者读取位置的下标
_conIndex++;
_conIndex %= _ringQueue.size();
// 解锁
pthread_mutex_unlock(&_conMutex);
// 消费者从环形队列中拿一个数据,空间数量要加1
sem_post(&_roomSem);
return value;
}
private:
vector<T> _ringQueue; // 环形队列
sem_t _roomSem; // 空间信号量,生产者关注的变量
sem_t _dataSem; // 数据信号量,消费者关注的变量
u_int32_t _proIndex; // 生产者写入的位置
u_int32_t _conIndex; // 消费者读取的位置
pthread_mutex_t _proMutex; // 生产者的互斥锁
pthread_mutex_t _conMutex; // 消费者的互斥锁
};
RingQueueTest.cc
#include <iostream>
#include <string>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include "RingQueue.hpp"
// 线程数据结构体
template <class T>
struct ThreadData
{
RingQueue<T>* ringQueue;
string name;
};
// 生产者运行函数
void* proFunction(void* args)
{
ThreadData<int>* ptd = (ThreadData<int>*)args;
while(true)
{
sleep(1);
int data = rand() % 10;
ptd->ringQueue->push(data);
cout << ptd->name <<" 放入数据成功: " << data << endl;
}
}
void* conFunction(void* args)
{
ThreadData<int>* ctd = (ThreadData<int>*)args;
while(true)
{
int data = ctd->ringQueue->pop();
cout << ctd->name << " 获取数据成功: " << data << endl;
}
}
int main()
{
// 种一个随机数种子
srand((unsigned long)time(nullptr) & getpid());
// 创建环形队列
RingQueue<int> rq;
// 创建生产线程和消费线程
pthread_t producer1, producer2, producer3;
pthread_t consumer1, consumer2, consumer3;
ThreadData<int> ptd1, ptd2, ptd3;
ThreadData<int> ctd1, ctd2, ctd3;
ptd1.ringQueue = &rq;
ptd1.name = "producer1";
pthread_create(&producer1, nullptr, proFunction, (void*)&ptd1);
ptd2.ringQueue = &rq;
ptd2.name = "producer2";
pthread_create(&producer2, nullptr, proFunction, (void*)&ptd2);
ptd3.ringQueue = &rq;
ptd3.name = "producer3";
pthread_create(&producer3, nullptr, proFunction, (void*)&ptd3);
ctd1.ringQueue = &rq;
ctd1.name = "consumer1";
pthread_create(&consumer1, nullptr, conFunction, (void*)&ctd1);
ctd2.ringQueue = &rq;
ctd2.name = "consumer2";
pthread_create(&consumer2, nullptr, conFunction, (void*)&ctd2);
ctd3.ringQueue = &rq;
ctd3.name = "consumer3";
pthread_create(&consumer3, nullptr, conFunction, (void*)&ctd3);
// 回收新线程
pthread_join(producer1, nullptr);
pthread_join(producer2, nullptr);
pthread_join(producer3, nullptr);
pthread_join(consumer1, nullptr);
pthread_join(consumer2, nullptr);
pthread_join(consumer3, nullptr);
return 0;
}
二、线程池和单例模式
1.日志和策略模式
什么是设计模式?
简单的是说,既然是模式,就是解决问题的模板范式。
IT行业这么火,涌入的人很多,俗话说林子大了啥鸟都有,大佬和菜鸡们两极分化的越来越严重.为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是 设计模式。
而计算机中的日志则是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志通常包含一下几项:
- 时间戳
- 日志等级
- 日志内容
以下几个指标是可选的文件名行号
进程,线程相关id信息等
有了前面线程互斥和线程同步的知识,我们现在可以设计出一个简单的线程池,在线程池中创建多个线程,然后我们可以往线程池中分放任务,让线程之间相互去竞争执行任务。
2.线程池实现
ThreadPool.hpp
#pragma once
#include <iostream>
#include <memory>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include <cassert>
#include "Task.hpp"
#include "Log.hpp"
using namespace std;
int Num = 5;
template <class T>
class ThreadPool
{
public:
ThreadPool(int threadNum = Num)
: _threadNum(threadNum), _isStart(false)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
// 如果不加static修饰的话,是类内的成员函数,默认带第二个参数this指针
// 这样的话线程调用就会不匹配了
// 但是设置成static以后就不能访问类内成员了
// 所以要传递进来this指针
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
// 竞争任务,先竞争锁资源
tp->lockQueue();
// 判断任务队列是否有任务,如果没有任务的话,线程必须等待
while(!tp->haveTask())
{
tp->waitForTask();
}
// 走到这里一定代表有任务了
// 拿任务
T task = tp->pop();
tp->unlockQueue();
// 在临界区外处理任务
int one, two;
char oper;
task.get(&one, &two, &oper);
Log() << "新线程完成计算任务: " << one << oper << two << "=" << task.run() << "\n";
}
}
// 线程池启动接口
void start()
{
assert(!_isStart);
// 创建多个线程
for (int i = 0; i < _threadNum; i++)
{
pthread_t thread;
pthread_create(&thread, nullptr, threadRoutine, (void *)this);
}
_isStart = true;
}
// 提供给外部向任务队列中放任务
void push(const T &in)
{
// 加锁
lockQueue();
_taskQueue.push(in);
// 选择一个线程来执行任务
choiceThreadForHandler();
// 解锁
unlockQueue();
}
private:
void lockQueue(){ pthread_mutex_lock(&_mutex); }
void unlockQueue(){ pthread_mutex_unlock(&_mutex); }
bool haveTask(){ return !_taskQueue.empty(); }
void waitForTask(){ pthread_cond_wait(&_cond, &_mutex); }
void choiceThreadForHandler(){ pthread_cond_signal(&_cond); }
T& pop()
{
T task = _taskQueue.front();
_taskQueue.pop();
return task;
}
private:
bool _isStart; // 标记线程池是否启动
int _threadNum; // 线程池中线程的数量
queue<T> _taskQueue; // 任务队列
pthread_mutex_t _mutex; // 互斥锁
pthread_cond_t _cond; // 条件变量
};
Log.hpp
#pragma once
#include <iostream>
#include <ctime>
#include <pthread.h>
std::ostream &Log()
{
std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | " << " Thread[" << pthread_self() << "] | ";
return std::cout;
}
Task.hpp
#pragma once
#include <iostream>
#include <string>
class Task
{
public:
Task() : elemOne_(0), elemTwo_(0), operator_('0')
{
}
Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op)
{
}
int operator() ()
{
return run();
}
int run()
{
int result = 0;
switch (operator_)
{
case '+':
result = elemOne_ + elemTwo_;
break;
case '-':
result = elemOne_ - elemTwo_;
break;
case '*':
result = elemOne_ * elemTwo_;
break;
case '/':
{
if (elemTwo_ == 0)
{
std::cout << "div zero, abort" << std::endl;
result = -1;
}
else
{
result = elemOne_ / elemTwo_;
}
}
break;
case '%':
{
if (elemTwo_ == 0)
{
std::cout << "mod zero, abort" << std::endl;
result = -1;
}
else
{
result = elemOne_ % elemTwo_;
}
}
break;
default:
std::cout << "非法操作: " << operator_ << std::endl;
break;
}
return result;
}
int get(int *e1, int *e2, char *op)
{
*e1 = elemOne_;
*e2 = elemTwo_;
*op = operator_;
}
private:
int elemOne_;
int elemTwo_;
char operator_;
};
ThreadPoolTest.cc
#include <string>
#include <ctime>
#include <cstdlib>
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Log.hpp"
int main()
{
unique_ptr<ThreadPool<Task> > tp(new ThreadPool<Task>());
tp->start();
const string operators = "+-*/%";
srand((unsigned long)time(nullptr) ^ getpid() ^ pthread_self());
// 派发任务的线程
while(true)
{
int one = rand()%50;
int two = rand()%10;
char oper = operators[rand()%operators.size()];
Log() << "主线程派发计算任务: " << one << oper << two << "=?" << "\n";
Task t(one, two, oper);
tp->push(t);
sleep(1);
}
return 0;
}
3.线程安全的单例模式
什么是单例模式?
某些类,只应该具有一个对象(实例),就称之为单例.
例如一个男人只能有一个媳妇.在很多服务器开发场景中,经常需要让服务器加载很多的数据(上百个G)到内存中,此时往往要用一个单例的类来管理这些数据.
单例模式又分为两种实现方式:懒汉和饿汉
最经典的就是洗碗的例子来区分
- 吃完饭,立刻洗碗,这种就是饿汉方式,因为下一顿吃的时候可以立刻拿着碗就能吃饭
- 吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式
这里我们可以对比得出,相比于饿汉使用完就立即”刷新“(这里的碗可以看作资源),懒汉方式最核心的思想是"延时加载"从而能够优化服务器的启动速度
我们再来举个实战的例子:
假设现在我们开发的服务器程序里有多个重量级单例类:
- 数据库连接池(初始化需要建立 10 个数据库连接,耗时 500ms)
- 配置解析器(需要读取并解析 100 个配置项,耗时 300ms)
- 日志管理器(需要初始化日志文件、缓存、分级策略,耗时 200ms)
- 消息队列客户端(需要建立网络连接、认证,耗时 400ms)
饿汉模式下
服务器一启动,就会把所有这些单例实例全部创建完成。
- 启动耗时:500+300+200+400 = 1400ms,这些耗时都叠加在 “服务器启动阶段”。
- 问题:哪怕服务器启动后,可能前 10 分钟都用不到 “消息队列客户端”,但启动时依然要花 400ms 创建它,白白占用了启动时间和内存资源。
懒汉模式下
服务器启动时不创建任何单例实例,只有当某个功能第一次被调用时,才创建对应的单例
- 服务器启动:耗时 0ms(无单例初始化),瞬间完成启动。
- 第一次查询数据库:创建 “数据库连接池”(耗时 500ms)。
- 第一次读取配置:创建 “配置解析器”(耗时 300ms)。
- 第一次打印日志:创建 “日志管理器”(耗时 200ms)。
- 如果全程不用消息队列:永远不创建 “消息队列客户端”。
核心优势:服务器启动阶段的耗时被 “分摊” 到后续的业务请求中,而非集中在启动时 —— 服务器能更快完成启动,更早对外提供服务。
我们再从内核来看:
- 内存上:饿汉式启动时就为所有单例分配内存,懒汉式只在使用时分配。
- CPU/IO: 饿汉式启动时要执行所有单例的初始化逻辑(如读文件、建连接),懒汉式把这些逻辑推迟到实际使用时。
而这也正好是我们现在能明显感受到的,笔记本开机速度快,而启动不同内存级应用时时间不一样,这就是懒汉模式促成的轻量化!
懒汉模式实现单例模式
// 懒汉模式, 线程安全
template <typename T>
class Singleton {
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance() {
if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提⾼性能.
lock.lock(); // 使⽤互斥锁, 保证多线程情况下也只调⽤⼀次 new.
if (inst == NULL) {
inst = new T();
}
lock.unlock();
}
return inst;
}
};
饿汉模式实现单例模式
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
三、STL和智能指针是否线程安全
STL中的容器不是线程安全的。
原因是STL容器在设计的时候就是为了将性能挖掘到极致,而一旦涉及到了加锁保证线程安全,那么该容器的性能一定是会大大降低的。而且不同的容器加锁的方式也不同,性能可能也不同。因此STL容器不是线程安全的,如果要在多线程下使用,往往需要调用者自行保证线程安全。智能指针也不是线程安全的。
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题.。
(只有引用计数的操作是线程安全的,修改shared_ptr对象本身必须加锁)但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。

877

被折叠的 条评论
为什么被折叠?



