现在,我们将基于之前完成的封装来设计一个线程池。在正式编码前,需要做好以下准备工作:
- 完成线程的基本封装
- 实现锁和条件变量的封装
- 引入日志系统,完善线程功能封装
这些准备工作我们已经做完了,下面我们就来设计一个线程池
1. 线程池概念
核心概念与产生背景
线程池 是一种基于池化技术(Pooling)管理线程的并发编程模型。其核心思想是:预先创建好一定数量的线程,放入一个"池子"中统一管理。当有任务需要执行时,不是直接创建一个新线程,而是从池中获取一个空闲线程来执行任务;任务执行完毕后,线程并不立即被销毁,而是返回池中等待执行下一个任务。
产生背景:
在早期并发模型中,"即时创建,即时销毁"的线程生命周期管理方式存在显著瓶颈:
-
资源消耗大:线程的创建和销毁是昂贵的操作,涉及操作系统内核的调用、内存分配、资源初始化等。频繁操作会消耗大量系统资源。
-
响应延迟高:当任务到达时,需要先等待线程创建完毕才能执行,增加了任务的响应时间。
-
系统稳定性风险:无限制地创建线程会耗尽系统资源(如内存、CPU时间片)。每个线程都需要占用一定的内存(如JVM中每个线程有自己的栈空间),大量线程会导致内存溢出(OOM),且过多的线程上下文切换会加剧CPU负载,导致系统效率急剧下降甚至崩溃。
线程池技术通过复用线程、控制并发数量、统一管理生命周期,完美地解决了上述问题,成为了高并发应用不可或缺的基础组件。
核心优势与价值
-
降低资源消耗:通过重复利用已创建的线程,极大地减少了线程频繁创建和销毁所造成的系统开销。
-
提高响应速度:当任务到达时,无需等待线程创建,任务可以立即由空闲线程执行,减少了任务执行的延迟。
-
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性。线程池允许对线程进行统一的分配、调优和监控。例如,可以控制线程的最大并发数,防止过度调度;可以监控线程的运行状态,进行任务队列的管理等。
-
提供更强大的功能 :线程池提供了丰富的扩展点,例如支持定时、延时、周期性的任务执行(如
ScheduledThreadPoolExecutor
)。
线程池的典型应用场景
1. 高并发短任务处理
典型示例:Web服务器请求处理
- 特点:单个请求处理时间短(通常<100ms),但请求量巨大
- 优势:避免了为每个请求创建新线程的开销
- 实际案例:Apache Tomcat默认使用线程池处理HTTP请求
2. 实时性要求高的应用
典型示例:金融交易系统
- 特点:需要极低延迟(通常要求<10ms响应)
- 优势:线程池中的线程始终处于就绪状态,可以立即处理任务
- 实现方式:通常配合工作队列和优先级调度机制
3. 突发流量处理
典型示例:电商秒杀活动
- 特点:短时间内请求量激增(可能达到平时100倍)
- 优势:通过限制最大线程数防止系统过载
- 保护机制:当请求超过处理能力时,可以采用拒绝策略
不适合的场景:
- 长时间任务(如Telnet会话):任务执行时间远超过线程创建时间,线程池优势不明显 。
线程池类型详解
1. 固定大小线程池(FixedThreadPool)
实现原理:
- 创建时指定固定数量的线程
- 使用无界队列保存待处理任务
- 线程空闲时不会被回收
适用场景:
- 需要严格控制资源使用的场景
- 任务量可预测的长期运行服务
- 示例:数据库连接池
特点:
- 优点:实现简单,资源消耗可控
- 缺点:任务堆积可能导致内存溢出
2. 可伸缩线程池(CachedThreadPool)
实现原理:
- 核心线程数为0,最大线程数为Integer.MAX_VALUE
- 使用同步移交队列
- 空闲线程60秒后自动回收
适用场景:
- 执行大量短生命周期的异步任务
- 示例:并行计算任务
特点:
- 优点:弹性伸缩,适应突发流量
- 缺点:线程数无限制可能导致资源耗尽
此处,我们选择固定线程个数的线程池。

2. 实现线程池
2.1 线程池框架
这里我们实现线程池时,使用5个固定数量的线程
cpp
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include "Log.hpp"
#include "Cond.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
namespace ThreadPoolModule
{
using namespace ThreadModlue;
using namespace LogModule;
using namespace CondModule;
using namespace MutexModule;
static const int gnum = 5; // 预创建5个线程
template <class T>
class ThreadPool
{
public:
ThreadPool(int num = gnum)
: _num(num)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
t();
}
}
void Start()
{
for(auto& thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
};
}
分析:
核心成员变量
-
_threads
:std::vector<Thread>
类型,存储和管理工作线程对象 -
_num
:整数类型,记录线程池中的线程数量 -
_taskq
:std::queue<T>
类型,作为任务队列,存储待处理的任务 -
_cond
和_mutex
:条件变量和互斥锁,用于线程间同步和任务队列的线程安全访问
构造函数:构造函数接受一个整数参数num,表示线程池中线程的数量,默认值为gnum(5)。在构造函数中,我们创建了num个线程,并将每个线程的执行函数设置为HandlerTask(一个不断从任务队列中取任务并执行的函数)。这里使用了lambda表达式来包装HandlerTask。
成员函数HandlerTask:这是每个线程的工作函数。它在一个无限循环中不断从任务队列中取出任务并执行。在取任务时,需要先获取互斥锁,然后检查任务队列是否为空。如果为空,则调用条件变量的Wait方法等待;否则,从队列中取出一个任务,然后释放锁(通过LockGuard的作用域),接着执行任务。
注意:
- 在锁外执行任务处理(
t()
),避免任务执行时间过长阻塞其他线程
Start函数:启动所有线程。遍历线程向量,调用每个线程的Start方法,并打印日志。
**对于成员函数HandlerTask,**我们不想被外部调用,我们可以将其私有
2.2 线程池退出
我们新增一个成员变量,作为运行标志位,线程池运行时为true,停止为false
cpp
static const int gnum = 5; // 预创建5个线程
template <class T>
class ThreadPool
{
private:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
//t();
}
}
public:
ThreadPool(int num = gnum)
: _num(num)
,_isrunning(false)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
void Start()
{
if(_isrunning)
return;
_isrunning = true;
for(auto& thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}
void Stop()
{
if(!_isrunning)
return;
_isrunning = false;
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
bool _isrunning; // 运行标志位
};
**成员函数HandlerTask,**它在一个无限循环中不断从任务队列中取出任务并执行。
cpp
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空或者
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
//t();
}
}
要么是在循环等任务,要么就是在执行任务,那我们要怎么退出呢?
我们先来分析一下,当我们的线程池退出时,也就是将运行标志位置为false,我们的线程处于什么状态呢?
可能是在等待,有可能在等待唤醒,也有可能在执行任务
所以我们要想线程池退出,不能只是简单的将所有线程停止或取消,我们应该让任务队列中的任务都被执行完了,并且运行标志位也被置为false,这时候才能让线程池退出,也就是说如果我们队列中还有任务,或者运行标志位为true,那我们就不能退出
cpp
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空
while (_taskq.empty())
{
_cond.Wait(_mutex);
}
// 线程被唤醒
// 判断线程池是否退出------如果线程池要退出,并且任务队列为空就退出
if(!_isrunning && _taskq.empty())
{
LOG(LogLevel::INFO) << name << "退出了,线程池想退出且任务队列为空";
break;
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
//t();
}
}
但是如果我们的任务队列为空,此时所有线程都在条件变量Wait处等待唤醒,此时我们将线程池退出Stop,也就是将运行标志位置为false,那此时所有线程都会被阻塞在条件变量处休眠,等待被唤醒,那不就退出不了了吗?
所以我们线程池退出时还需要将那些在Wait的线程唤醒,判断条件也需要改,因为如果线程被唤醒,但是我们任务队列仍然为空,那就会再次进入循环继续Wait,但是我们线程池要退出呀,不能再让线程继续去Wait,所以还需要判断线程池是否退出
cpp
template <class T>
class ThreadPool
{
private:
void HandlerTask()
{
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T t;
{
LockGuard lockguard(_mutex);
// 队列为空, 或者线程池没有退出,我们才需要wait
while (_taskq.empty() && _isrunning)
{
_sleepernum++;
_cond.Wait(_mutex);
_sleepernum--;
}
// 线程被唤醒
// 判断线程池是否退出------如果线程池要退出,并且任务队列为空就退出
if (!_isrunning && _taskq.empty())
{
LOG(LogLevel::INFO) << name << "退出了,线程池想退出且任务队列为空";
break;
}
// 从任务队列中取任务
t = _taskq.front();
_taskq.pop();
}
// 处理任务,不需要在临界区内部,为什么?
// t();
}
}
void WakeUpAllThread()
{
LockGuard lockguard(_mutex);
if (_sleepernum)
{
_cond.Broadcast();
LOG(LogLevel::INFO) << "唤醒所有线程";
}
}
public:
ThreadPool(int num = gnum)
: _num(num), _isrunning(false)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}
void Stop()
{
if (!_isrunning)
return;
_isrunning = false;
// 唤醒所有线程
WakeUpAllThread();
}
~ThreadPool() {}
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
bool _isrunning; // 运行标志位
int _sleepernum; // 线程休眠的数量
};
在学习了线程控制章节后,我们知道线程退出后,需要join等待线程退出,这里我们也需要等待,代码如下:
cpp
void Join()
{
for(auto& thread : _threads)
{
thread.Join();
}
}
下面我们先来测试一下:
cpp
#include "Log.hpp"
#include "ThreadPool.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
int main()
{
Enable_Console_Log_Strategy();
ThreadPool<int> *tp = new ThreadPool<int>();
tp->Start();
sleep(5);
tp->Stop();
tp->Join();
return 0;
}
运行结果:
bash
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./threadpool
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-1
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-2
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-3
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-4
[2025-09-12 16:50:17] [DEBUG] [91391] [Thread.hpp] [71] - create thread success
[2025-09-12 16:50:17] [INFO] [91391] [ThreadPool.hpp] [90] - new thread start: thread-5
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [64] - 唤醒所有线程
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-2退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-1退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-3退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-4退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [INFO] [91391] [ThreadPool.hpp] [45] - thread-5退出了,线程池想退出且任务队列为空
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
[2025-09-12 16:50:22] [DEBUG] [91391] [Thread.hpp] [109] - join success
2.3 任务入队列
那接下来就需要将任务入到任务队列中
cpp
bool Enqueue(const T& in)
{
LockGuard lockguard(_mutex);
// 如果线程池退出就不能再将任务入队列
if(_isrunning)
{
_taskq.push(in);
// 有线程在休眠,就唤醒
if(_sleepernum > 0)
{
_cond.Signal();
}
return true;
}
return false;
}
那我们再来个任务试试,就和之前进程间通信时的任务一样,这里我们就只用一个任务来测试
cpp
#pragma once
#include <functional>
#include "Log.hpp"
using namespace LogModule;
// 定义了一个任务类型,返回值void,参数为空
using task_t = std::function<void()>;
void Download()
{
LOG(LogLevel::DEBUG) << "这是一个下载的任务...";
}
下面我们再来测试一下
cpp
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace LogModule;
using namespace ThreadPoolModule;
int main()
{
Enable_Console_Log_Strategy();
ThreadPool<task_t> *tp = new ThreadPool<task_t>();
tp->Start();
int count = 10;
while(count--)
{
tp->Enqueue(Download);
sleep(1);
}
tp->Stop();
tp->Join();
return 0;
}
运行结果:
bash
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./threadpool
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-1
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-2
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-3
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-4
[2025-09-12 17:31:52] [DEBUG] [92986] [Thread.hpp] [71] - create thread success
[2025-09-12 17:31:52] [INFO] [92986] [ThreadPool.hpp] [90] - new thread start: thread-5
[2025-09-12 17:31:52] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:53] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:54] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:55] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:56] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:57] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:58] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:31:59] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:32:00] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:32:01] [DEBUG] [92986] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [64] - 唤醒所有线程
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-1退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-2退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-3退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-4退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [INFO] [92986] [ThreadPool.hpp] [45] - thread-5退出了,线程池想退出且任务队列为空
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
[2025-09-12 17:32:02] [DEBUG] [92986] [Thread.hpp] [109] - join success
3. 线程安全的单例模式
3.1 单例模式概念
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式解决了需要全局唯一对象的场景,避免多个实例造成的资源浪费或状态不一致问题。
关键设计要点:
- 私有构造函数 :防止外部通过
new
创建实例 。 - 静态实例变量:存储类的唯一实例 。
- 静态访问方法 (如
getInstance()
):提供全局访问入口,控制实例的创建逻辑 。
应用场景:配置文件加载、线程池管理、数据库连接池、Session 实现等需全局唯一资源的场景 。
3.2 单例模式的特点
(1)唯一性
- 任何时刻仅存在一个类的实例,通过静态变量维护 。
- 例如:管理上百 GB 内存数据的服务器类,需单例避免重复加载 。
(2)全局访问点
- 通过静态方法(如
getInstance()
)提供统一访问入口,确保所有代码使用同一实例 。
(3)资源优化
- 减少开销:避免频繁创建/销毁对象(如数据库连接)。
- 数据一致性:唯一实例保证共享资源状态统一(如配置信息)。
(4)线程安全挑战
- 多线程环境下需额外机制(如锁、双重检查)防止创建多个实例 。
生活实例:正如一个男人只能有一个媳妇(在一夫一妻制社会中),某些系统组件也只需要一个实例。
服务器开发应用:在很多服务器开发场景中,经常需要让服务器加载大量数据(上百GB)到内存中。例如电商平台的商品信息、社交网络的用户关系图等。此时往往要用一个单例的类来管理这些数据,避免重复加载造成内存浪费。
3.3 饿汉与懒汉实现方式
饿汉方式 (Eager Initialization)
-
特点:在类加载时就创建实例
-
优点:线程安全,无需担心多线程问题
-
缺点:如果实例一直未被使用,会造成资源浪费
-
适用场景:实例初始化耗时短,且程序运行过程中一定会使用该实例
懒汉方式 (Lazy Initialization)
-
特点:在第一次使用时才创建实例
-
优点:资源利用率高,避免不必要的初始化
-
缺点:需要处理多线程安全问题
-
适用场景:实例初始化耗时长或资源占用大,且可能不会被立即使用
类比:
- 饿汉式 → 饭后立刻洗碗:下次直接使用,但可能洗了未用的碗 。
- 懒汉式 → 下次用餐前洗碗:节省资源,但需临时处理 。
3.4 饿汉方式实现单例模式
cpp
template <typename T>
class Singleton {
static T data; // 静态成员变量,在程序开始时初始化
public:
static T* GetInstance() {
return &data;
}
// 删除拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default; // 构造函数私有化
~Singleton() = default; // 析构函数私有化
};
关键点分析:
-
静态成员初始化:
-
static T data
是静态成员变量,在程序启动时(main函数执行前)就完成初始化 -
这确保了实例的早期创建,避免了多线程环境下的竞争条件
-
-
线程安全性:
-
由于实例在程序启动时就创建,不存在多线程同时创建实例的问题
-
天然线程安全,无需额外的同步机制
-
-
访问控制:
-
构造函数和析构函数私有化,防止外部创建或销毁实例
-
删除拷贝构造函数和赋值运算符,防止通过拷贝方式创建新实例
-
-
获取实例:
GetInstance()
方法直接返回静态实例的地址,简单高效
-
优缺点:
-
优点:实现简单,线程安全,性能高(无锁)
-
缺点:如果实例很大或初始化耗时,会延长程序启动时间;即使不使用也会占用资源
-
3.5 懒汉方式实现单例模式(基础版本)
cpp
template <typename T>
class Singleton {
static T* inst; // 静态指针,初始为nullptr
public:
static T* GetInstance() {
if (inst == nullptr) {
inst = new T(); // 第一次调用时创建实例
}
return inst;
}
// 删除拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
// 初始化静态成员
template <typename T>
T* Singleton<T>::inst = nullptr;
关键点分析:
-
延迟初始化:
-
使用静态指针
inst
初始化为nullptr
-
只有在第一次调用
GetInstance()
时才创建实例
-
-
线程安全问题:
-
这是基础版本的主要缺陷
-
如果多个线程同时检查
inst == nullptr
,都可能通过检查,导致创建多个实例 -
违反了单例模式的基本原则
-
-
内存管理:
-
使用
new
创建实例,但没有相应的delete
操作 -
可能导致内存泄漏(虽然程序结束时操作系统会回收内存)
-
-
优缺点:
-
优点:延迟初始化,节省资源
-
缺点:线程不安全,可能创建多个实例
-
3.6 懒汉方式实现单例模式(线程安全版本)
cpp
#include <mutex>
template <typename T>
class Singleton {
volatile static T* inst; // volatile防止编译器过度优化
static std::mutex lock; // 互斥锁保证线程安全
public:
static T* GetInstance() {
if (inst == nullptr) { // 第一次检查,避免不必要的锁竞争
std::lock_guard<std::mutex> guard(lock); // RAII方式加锁
if (inst == nullptr) { // 第二次检查,确保只有一个线程创建实例
inst = new T();
}
}
return inst;
}
// 删除拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
// 初始化静态成员
template <typename T>
volatile T* Singleton<T>::inst = nullptr;
template <typename T>
std::mutex Singleton<T>::lock;
关键点分析:
-
双重检查锁定模式:
-
第一重检查
if (inst == nullptr)
避免不必要的锁竞争 -
只有实例未创建时才进入同步块
-
第二重检查确保只有一个线程创建实例
-
-
线程安全:
-
使用
std::mutex
和std::lock_guard
保证线程安全 -
lock_guard
采用 RAII 技术,自动管理锁的生命周期
-
-
volatile 关键字:
-
防止编译器对指令进行重排序优化
-
确保多线程环境下读取的是最新值,而不是寄存器中的缓存值
-
-
内存屏障问题:
-
在 C++11 之前,双重检查锁定可能存在指令重排序问题
-
inst = new T()
可能被重排序为:分配内存 → 赋值给 inst → 调用构造函数 -
这可能导致其他线程看到非空但未完全构造的实例
-
C++11 的内存模型解决了这个问题,但使用
volatile
是额外的保障
-
-
构造函数和析构函数私有化:
-
防止外部创建实例
-
防止通过拷贝构造或赋值操作创建新实例
-
-
优缺点:
-
优点:线程安全,延迟初始化,性能较好(大部分情况下无需加锁)
-
缺点:实现相对复杂,需要注意指令重排序问题
-
这种实现方式既保证了线程安全,又避免了不必要的锁竞争,是懒汉单例模式的经典实现。
4. 单例式线程池
线程池本身是系统关键资源,创建多个线程池实例会导致:
-
线程数量过多,增加上下文切换开销
-
内存资源浪费(每个线程都需要分配栈空间)
-
难以监控和统计整体线程使用情况
-
多个线程池可能竞争相同的系统资源
-
任务分配不均衡,可能导致某些线程池过载而其他空闲
-
减少复杂的线程间协调和同步问题
实现单例式线程池是为了统一管理线程资源、提高系统效率、保证稳定性 ,并提供一个简洁全局的并发编程接口。
下面我们实现线程安全的懒汉方式来实现单例式线程池
首先需要将构造函数和析构函数私有,Start函数也需要私有,同时拷贝构造和赋值重载需要显式删除
cpp
private:
ThreadPool(int num = gnum)
: _num(num), _isrunning(false)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back(
[this]()
{
HandlerTask();
});
}
}
~ThreadPool() {}
// 删除拷贝构造函数和赋值运算符
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
void Start()
{
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
{
thread.Start();
LOG(LogLevel::INFO) << "new thread start: " << thread.Name();
}
}
定义静态指针,实现单例模式
cpp
class ThreadPool
{
...
private:
std::vector<Thread> _threads; // 管理线程
int _num; // 线程的数量
std::queue<T> _taskq; // 任务队列
Cond _cond;
Mutex _mutex;
bool _isrunning; // 运行标志位
int _sleepernum; // 线程休眠的数量
static ThreadPool<T>* _inst; // 单例指针
static Mutex _lock;
};
// 静态成员类外初始化
template<class T>
ThreadPool<T>* ThreadPool<T>::_inst = nullptr;
template<class T>
Mutex ThreadPool<T>::_lock;
实现Getinstance函数
cpp
static ThreadPool* GetInstance()
{
if(_inst == nullptr)
{
LockGuard lockguard(_lock);
LOG(LogLevel::DEBUG) << "获取单例...";
if(_inst == nullptr)
{
LOG(LogLevel::DEBUG) << "首次使用,创建单例...";
_inst = new ThreadPool<T>();
_inst->Start();
}
}
return _inst;
}
测试一下:
cpp
int main()
{
Enable_Console_Log_Strategy();
int count = 10;
while(count--)
{
ThreadPool<task_t>::GetInstance()->Enqueue(Download);
sleep(1);
}
ThreadPool<task_t>::GetInstance()->Stop();
ThreadPool<task_t>::GetInstance()->Join();
return 0;
}
运行结果:
bash
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/ThreadPool$ ./threadpool
[2025-09-12 22:33:16] [DEBUG] [107860] [ThreadPool.hpp] [105] - 获取单例...
[2025-09-12 22:33:16] [DEBUG] [107860] [ThreadPool.hpp] [108] - 首次使用,创建单例...
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-1
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-2
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-3
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-4
[2025-09-12 22:33:16] [DEBUG] [107860] [Thread.hpp] [71] - create thread success
[2025-09-12 22:33:16] [INFO] [107860] [ThreadPool.hpp] [51] - new thread start: thread-5
[2025-09-12 22:33:16] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:17] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:18] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:19] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:20] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:21] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:22] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:23] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:24] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:25] [DEBUG] [107860] [Task.hpp] [13] - 这是一个下载的任务...
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [95] - 唤醒所有线程
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-1退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-2退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-3退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-5退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [INFO] [107860] [ThreadPool.hpp] [76] - thread-4退出了,线程池想退出且任务队列为空
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
[2025-09-12 22:33:26] [DEBUG] [107860] [Thread.hpp] [109] - join success
从运行结果可以看到没得问题,并且在使用日志向控制台输出后,也没有出现打印信息全都混在一起的情况。
5. 线程安全和重入问题
5.1 概念
线程安全:指多个线程同时访问共享资源时,能够正确执行而不会相互干扰或破坏彼此的执行结果。通常情况下,当多个线程并发执行仅包含局部变量的同一段代码时,不会产生不同的结果。但如果对全局变量或静态变量进行操作且未加锁保护,就容易出现线程安全问题。
重入:指同一个函数被不同执行流调用时,在前一个流程尚未执行完成时,又有其他执行流进入该函数。若一个函数在重入情况下仍能保持运行结果一致且不出现任何问题,则称为可重入函数,反之则为不可重入函数。
目前我们已经能够理解重入主要分为两种情况:
- 多线程重入函数
- 信号导致执行流重复进入函数
常见线程不安全的情况
• 未对共享变量进行保护的函数
• 函数状态随调用次数发生变化的函数
• 返回静态变量指针的函数
• 调用其他线程不安全函数的函数
常见不可重入的情况
• 调用malloc/free函数,因为它们使用全局链表管理堆内存
• 调用标准I/O库函数,因其实现常依赖不可重入的全局数据结构
• 函数内部使用了静态数据结构
常见线程安全的情况
• 仅读取全局/静态变量而无写入操作
• 类或接口提供原子性操作
• 多线程切换不会导致接口执行结果产生歧义
常见可重入的情况
• 不使用全局或静态变量
• 不使用动态内存分配(malloc/new)
• 不调用不可重入函数
• 不返回静态/全局数据,所有数据由调用方提供
• 使用局部数据,或通过全局数据的本地副本来保护全局状态
5.2 可重入性与线程安全的关系解析
不要被专业术语的复杂性吓到,通过仔细分析你会发现这些概念本质上是相互关联的。让我们深入探讨可重入函数与线程安全之间的关系。
可重入与线程安全的联系
基本对应关系
• 可重入函数必然线程安全:如果一个函数被设计为可重入的,那么它自然就是线程安全的。这是最核心的要点,掌握这一点就抓住了关键。
示例:一个只使用局部变量的纯计算函数,既可以被多个线程安全调用,也可以在信号处理程序中安全使用。
• 不可重入函数潜在风险:不可重入的函数不能被多个线程同时使用,否则可能引发数据竞争、内存污染等线程安全问题。
典型例子:使用静态缓冲区的strtok()
函数,在多线程环境下会导致不可预期的结果。
• 全局变量的影响:使用全局变量的函数会同时丧失可重入性和线程安全性,因为全局状态会被所有调用者共享。
例如:一个使用static int counter
来统计调用次数的函数,在多线程环境下计数会出错。
可重入与线程安全的区别
概念范围
• 包含关系:可重入函数是线程安全函数的一个子集。所有可重入函数都是线程安全的,但并非所有线程安全函数都是可重入的。
类比:就像所有正方形都是矩形,但并非所有矩形都是正方形。
• 锁机制的影响:
-
通过加锁实现的线程安全函数:这些函数在多线程环境下是安全的,但如果涉及到重入(如信号处理程序中调用),可能导致死锁。
示例场景:一个已获得互斥锁的线程在执行期间被信号中断,信号处理程序又试图获取同一个锁。
-
真正的可重入函数:不依赖锁机制,通常通过避免共享状态或使用线程本地存储来实现。
特别注意事项
应用场景考量
• 信号处理的影响:在大多数情况下,如果不考虑信号导致执行流重入的特殊情况,线程安全和可重入在安全性角度可以不做严格区分。
• 关注点差异:
-
线程安全:侧重描述多线程并发访问共享资源时的安全特性,反映的是程序在并发环境中的行为表现。
应用场景:设计多线程服务器时,确保共享数据结构的线程安全。
-
可重入:描述的是函数能否被安全地"重复进入"的特性,体现的是函数本身的设计特点。
应用场景:编写信号处理函数或递归算法时,必须使用可重入函数。
实际开发建议
- 在信号处理程序中只使用明确标注为"可重入"的函数
- 多线程编程时优先选择线程安全的函数版本
- 对于性能关键代码,可重入实现通常比加锁的线程安全实现更高效
6. 常见锁概念
6.1 死锁
死锁 是指在多进程/线程系统中的一种资源竞争状态,当一组进程中的每个进程都持有至少一个不可抢占的资源(即该资源在被使用过程中不会被系统强制收回),同时又在等待获取该组中其他进程所占用的资源时,就会形成环形等待链,导致所有相关进程都无法继续执行下去,系统进入永久阻塞的状态。
为便于说明,假设线程A和线程B必须同时获取锁1和锁2,才能继续访问后续资源

申请单把锁是原子操作,但申请多把锁则未必能保证原子性。

这个时候造成的结果是:

6.2 死锁四个必要条件
互斥条件(Mutual Exclusion):这是死锁产生的四个必要条件之一,指在并发环境中,一个资源(如打印机、共享内存、文件等)在同一时间只能被一个执行流(线程或进程)独占使用。当某个执行流已经获取该资源时,其他执行流必须等待,直到该资源被释放。这个条件保证了资源的独占性,但同时也可能导致死锁的发生。
**请求与保持条件(Request and Hold Condition)**也是死锁产生的四个必要条件之一,指的是在并发系统中,当一个执行流(如进程或线程)因为请求新的资源而被阻塞时,仍然保持着已获得的资源不放。这种状况会导致多个执行流相互等待对方释放资源,从而形成死锁。
具体来说,请求与保持条件包含两个关键方面:
- 请求新资源:执行流在持有某些资源的同时,又尝试申请新的资源
- 保持已有资源:在申请新资源未成功时,不会释放已持有的资源

不剥夺条件(Non-preemptive Condition) 也称为不可抢占条件。该条件要求一个进程在执行过程中已获得的资源,在未使用完毕之前,其他进程或系统不能强行剥夺或抢占该资源。只有在进程主动释放资源后,其他进程才能获取这些资源。
关键点说明
-
资源持有状态:
- 进程在执行期间可能占用某些资源(如内存、I/O设备、文件锁等)。
- 这些资源一旦被分配,除非进程主动释放,否则系统不能强制收回。
-
剥夺的影响:
- 如果系统允许强行剥夺资源(如CPU时间片轮转),可能导致进程执行异常或数据不一致。
- 例如,一个进程正在写入文件时,若突然被剥夺磁盘访问权限,可能导致文件损坏。

**循环等待条件(Circular Wait Condition)**是多线程编程或操作系统资源分配中常见的一种死锁情况。具体表现为:有多个执行流(线程或进程)同时运行,每个执行流都在等待其他执行流释放资源,而这些等待关系形成了一个闭合的环形链。

6.3 避免死锁
死锁通常发生在多个进程或线程互相等待对方释放资源时。预防死锁需要从资源分配和请求策略入手。
破坏互斥条件
某些资源可以通过共享方式使用,避免独占。例如,只读文件可以允许多个进程同时访问,减少竞争。
破坏占有并等待条件
进程在开始执行前必须一次性申请所有所需资源。如果无法满足,则暂时不分配任何资源。这种方式可能导致资源利用率降低。
破坏非抢占条件
如果进程无法获得额外资源,必须释放已占有的资源。这种策略适用于状态容易保存和恢复的资源,如CPU寄存器。
破坏循环等待条件
对资源类型进行线性排序,要求进程按照编号顺序申请资源。例如,进程只能先申请编号较小的资源,再申请编号较大的资源。
避免死锁的算法
银行家算法
通过模拟资源分配检查系统是否处于安全状态。每次资源分配前,算法会判断剩余资源是否能满足至少一个进程的最大需求,从而避免进入不安全状态。
资源分配图算法
通过维护资源分配图检测是否存在环路。如果图中没有环路,则系统不会发生死锁;若存在环路,则可能发生死锁。
检测与恢复策略
定期检测死锁
通过资源分配图或等待图算法定期扫描系统状态。一旦检测到死锁,立即采取恢复措施。
终止进程
强制终止一个或多个死锁进程,释放其占用的资源。可以选择终止代价最小的进程,例如运行时间最短或资源占用最少的进程。
资源抢占
从某些进程中抢占资源分配给其他进程。被抢占资源的进程可能需要回滚到之前的检查点重新执行。
实际应用建议
- 在编写多线程程序时,尽量按照固定顺序获取锁。
- 使用超时机制,避免线程无限期等待资源。
- 减少锁的粒度,使用细粒度锁代替粗粒度锁。
- 优先使用高级并发工具(如信号量、条件变量)而非直接操作锁。
通过合理设计资源管理策略,可以显著降低死锁发生的概率。
7. STL、智能指针和线程安全
STL容器是否具备线程安全性?
答案是否定的。
这是由于STL在设计时优先考虑性能优化,而线程安全所需的锁机制会显著影响性能表现。此外,不同容器类型(如哈希表的表锁与桶锁,锁整个表(粗粒度)或锁单个桶(细粒度))需要采用不同的加锁策略,性能影响也各不相同。
因此,STL默认不提供线程安全保障。若要在多线程环境中使用,开发者需要自行实现线程安全机制。
智能指针是否是线程安全的?
unique_ptr 是线程安全的,这是因为:
- 所有权单一性:unique_ptr 采用独占所有权模式,任何时候一个资源只能由一个 unique_ptr 拥有
- 局部作用域:unique_ptr 的生命周期通常限定在当前代码块范围内,不会跨线程共享
- 转移所有权时的安全性:当通过 std::move 转移所有权时,操作是原子性的,不会产生竞态条件
shared_ptr 的线程安全性更为复杂,主要体现在以下几个方面:
-
引用计数的原子性:
- 标准库使用原子操作(CAS, Compare-And-Swap)保证引用计数操作是线程安全的
- 引用计数的增减操作是原子的,不会出现竞态条件
-
控制块的线程安全:
- shared_ptr 的实现包含一个控制块,其中存储引用计数和弱引用计数
- 控制块的修改都通过原子操作保护
-
数据访问的非原子性:
- 虽然引用计数操作是线程安全的,但对托管对象的访问仍需要额外同步
- 多个线程同时访问同一个 shared_ptr 管理的对象时,需要单独的互斥锁
使用建议
-
unique_ptr:当不需要跨线程共享所有权时优先使用,完全线程安全
-
shared_ptr:
- 引用计数操作本身是线程安全的
- 但需要共享数据时,仍需要额外的同步机制保护数据访问
- 跨线程传递 shared_ptr 时,最好通过复制而非引用传递
-
性能考虑:
- shared_ptr 的原子操作会有一定性能开销
- 在不需要线程安全的场景可以考虑使用 boost::shared_ptr 的非线程安全版本
8. 其他常见的各种锁(了解)
1. 悲观锁
-
核心思想 :"总有刁民想害朕"。
-
工作方式 :我认为只要我去操作数据(无论是读还是写),肯定会有其他线程来和我争抢、修改数据。所以,在操作数据之前,我一定会先加锁,把数据"锁"起来,这样其他线程就会被阻塞在外,无法操作。等我操作完释放锁之后,其他线程才能进来。
-
比喻 :就像你去一个只有一个坑位的公共卫生间,你悲观地认为肯定有人会来抢。所以你一进去就把门从里面反锁(加锁),这样别人就只能在外面等着(被阻塞)。等你上完厕所开门出来(释放锁),下一个人才能进去。
-
常见实现 :
synchronized
关键字、ReentrantLock
等。 -
优点:简单粗暴,能保证绝对的线程安全。
-
缺点:加锁和释放锁本身有性能开销,并且如果锁竞争激烈,会导致大量线程挂起和唤醒,非常耗时。
2. 乐观锁
-
核心思想 :"应该没人会改吧,我先干了再说"。
-
工作方式 :我认为在我操作数据的时候,大概率不会有其他线程来修改它。所以我在操作数据前不上锁 ,直接就去读。但在更新数据的时候,我会判断一下这个数据在我读完之后、到我更新之前,有没有被别人动过。如果没动过,我就安心更新;如果动过了,我的更新就失败,然后我会选择重试或者报错。
-
比喻:就像你用云笔记(如Git、Google Docs)。你打开文档直接编辑(不上锁)。当你写完点击"保存"时,系统会检查一下从你打开文档到现在,有没有别人也保存过(判断版本)。如果没有人保存过,你的内容就顺利存上去。如果别人已经保存过了,系统会提示你"你的版本已过期",让你基于最新的版本重新编辑(重试)。
-
常见实现 :版本号机制 、CAS操作。
-
优点:在读多写少的场景下,性能极高,因为它避免了加锁的巨大开销。
-
缺点:如果写操作非常频繁,更新失败重试的次数就会很多,反而可能降低性能(俗称"CPU空转")。
3. CAS操作
-
是什么 :Compare-And-Swap(比较并交换) ,是乐观锁 最常用的一种具体实现技术。
-
工作流程:它包含三个操作数:
-
V:内存中的当前值(我准备要更新的那个变量现在的值)
-
A:我原先读取到的旧值(我期望内存中的值还是这个)
-
B :我想要更新成的新值
CAS的操作是原子性 的(由CPU硬件指令保证)。它的逻辑是:"如果现在内存位置V的值等于我预期的旧值A,那么我就把它更新为新值B。否则,什么都不做,然后告诉我现在的实际值是多少。"
-
-
比喻 :你看中了一件商品,库存显示只剩1件(V = 1)。你赶紧下单,在最终付款时,系统会检查一下库存现在还是不是1(Compare)。如果是,就扣减库存,让你购买成功(Swap);如果不是(比如已经被别人买走了),就告诉你失败。
-
缺点:
-
ABA问题:别人可能把库存从1件买光,然后又补了1件回来,你看库存还是1,但已经不是当初那个1了。对于敏感业务,需要用版本号来辅助解决。
-
自旋时间长开销大:如果一直不成功,CAS会不停重试,消耗CPU。
-
只能保证一个共享变量的原子操作。
-
4. 自旋锁
-
是什么 :它是一种**"傻等"** 的锁,是悲观锁的一种实现方式。
-
工作方式 :当一个线程尝试获取锁失败时,它不会立刻被挂起 (进入阻塞状态),而是会执行一个忙循环(自旋),不停地尝试获取锁,直到成功为止。
-
比喻:你在等洗手间,里面有人。悲观锁的做法是:你去找个沙发躺着睡觉(线程被挂起),等里面的人出来大声叫你(唤醒)。而自旋锁的做法是:你不去睡觉,就在门口不停地敲门问"好了没?好了没?好了没?"(循环尝试)。
-
适用场景 :非常适合锁被持有时间非常短的情况。因为线程挂起和唤醒的代价远大于它自旋一小会儿的代价。
-
缺点:如果锁被持有时间很长,自旋的线程就会白白浪费CPU时间。
5. 读写锁
-
是什么 :一种特殊的悲观锁 ,它将锁的操作细分为读锁 和写锁。
-
工作规则(核心规则):
-
共享读 :多个线程可以同时持有读锁,进行读取操作。
-
独占写 :写锁是独占的。一个线程持有写锁时,其他所有线程(无论是想读还是想写)都必须等待。
-
读写互斥 :一个线程持有读锁时,其他线程可以读,但不能写 。一个线程持有写锁时,其他线程既不能读也不能写。
-
-
比喻:一个黑板报。
-
读:很多同学可以同时看黑板报(共享读)。
-
写:如果一个同学要上去修改黑板报(写),他必须等所有看的同学都走开(释放读锁),并且他会独占黑板报,不让别人看也不让别人写(独占写)。
-
-
适用场景 :读多写少的场景,能极大提升性能(因为读操作可以并发进行)。
-
常见实现 :
ReadWriteLock
接口及其实现ReentrantReadWriteLock
。
总结对比
锁类型 | 核心思想 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
悲观锁 | 先加锁,再操作 | 保证强一致性,简单安全 | 性能开销大 | 写多读少,临界区操作耗时 |
乐观锁 | 先操作,更新时检查 | 性能高,无锁开销 | 存在ABA问题,竞争激烈时重试开销大 | 读多写少,竞争不激烈 |
CAS | 比较并交换(乐观锁的实现) | 硬件实现,高效 | ABA问题,自旋开销 | 实现原子操作,无锁数据结构 |
自旋锁 | 失败后循环尝试(悲观锁的实现) | 避免线程切换开销 | 占用CPU空转 | 锁持有时间极短的场景 |
读写锁 | 读共享,写独占 | 允许多线程并发读,大幅提升读性能 | 实现相对复杂,写操作可能饿死 | 读多写少的并发场景 |