【Linux系统】单例式线程池

现在,我们将基于之前完成的封装来设计一个线程池。在正式编码前,需要做好以下准备工作:

  1. 完成线程的基本封装
  2. 实现锁和条件变量的封装
  3. 引入日志系统,完善线程功能封装

这些准备工作我们已经做完了,下面我们就来设计一个线程池

1. 线程池概念

核心概念与产生背景

线程池 是一种基于池化技术(Pooling)管理线程的并发编程模型。其核心思想是:预先创建好一定数量的线程,放入一个"池子"中统一管理。当有任务需要执行时,不是直接创建一个新线程,而是从池中获取一个空闲线程来执行任务;任务执行完毕后,线程并不立即被销毁,而是返回池中等待执行下一个任务。

产生背景:

在早期并发模型中,"即时创建,即时销毁"的线程生命周期管理方式存在显著瓶颈:

  1. 资源消耗大:线程的创建和销毁是昂贵的操作,涉及操作系统内核的调用、内存分配、资源初始化等。频繁操作会消耗大量系统资源。

  2. 响应延迟高:当任务到达时,需要先等待线程创建完毕才能执行,增加了任务的响应时间。

  3. 系统稳定性风险:无限制地创建线程会耗尽系统资源(如内存、CPU时间片)。每个线程都需要占用一定的内存(如JVM中每个线程有自己的栈空间),大量线程会导致内存溢出(OOM),且过多的线程上下文切换会加剧CPU负载,导致系统效率急剧下降甚至崩溃。

线程池技术通过复用线程、控制并发数量、统一管理生命周期,完美地解决了上述问题,成为了高并发应用不可或缺的基础组件。

核心优势与价值

  1. 降低资源消耗:通过重复利用已创建的线程,极大地减少了线程频繁创建和销毁所造成的系统开销。

  2. 提高响应速度:当任务到达时,无需等待线程创建,任务可以立即由空闲线程执行,减少了任务执行的延迟。

  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性。线程池允许对线程进行统一的分配、调优和监控。例如,可以控制线程的最大并发数,防止过度调度;可以监控线程的运行状态,进行任务队列的管理等。

  4. 提供更强大的功能 :线程池提供了丰富的扩展点,例如支持定时、延时、周期性的任务执行(如 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;

    };
}

分析:

核心成员变量

  • _threadsstd::vector<Thread> 类型,存储和管理工作线程对象

  • _num:整数类型,记录线程池中的线程数量

  • _taskqstd::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 单例模式概念

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式解决了需要全局唯一对象的场景,避免多个实例造成的资源浪费或状态不一致问题。

关键设计要点:

  1. 私有构造函数 :防止外部通过 new 创建实例 。
  2. 静态实例变量:存储类的唯一实例 。
  3. 静态访问方法 (如 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; // 析构函数私有化
};

关键点分析:

  1. 静态成员初始化

    • static T data 是静态成员变量,在程序启动时(main函数执行前)就完成初始化

    • 这确保了实例的早期创建,避免了多线程环境下的竞争条件

  2. 线程安全性

    • 由于实例在程序启动时就创建,不存在多线程同时创建实例的问题

    • 天然线程安全,无需额外的同步机制

  3. 访问控制

    • 构造函数和析构函数私有化,防止外部创建或销毁实例

    • 删除拷贝构造函数和赋值运算符,防止通过拷贝方式创建新实例

  4. 获取实例

    • GetInstance() 方法直接返回静态实例的地址,简单高效
  5. 优缺点

    • 优点:实现简单,线程安全,性能高(无锁)

    • 缺点:如果实例很大或初始化耗时,会延长程序启动时间;即使不使用也会占用资源


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;

关键点分析:

  1. 延迟初始化

    • 使用静态指针 inst 初始化为 nullptr

    • 只有在第一次调用 GetInstance() 时才创建实例

  2. 线程安全问题

    • 这是基础版本的主要缺陷

    • 如果多个线程同时检查 inst == nullptr,都可能通过检查,导致创建多个实例

    • 违反了单例模式的基本原则

  3. 内存管理

    • 使用 new 创建实例,但没有相应的 delete 操作

    • 可能导致内存泄漏(虽然程序结束时操作系统会回收内存)

  4. 优缺点

    • 优点:延迟初始化,节省资源

    • 缺点:线程不安全,可能创建多个实例


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;

关键点分析:

  1. 双重检查锁定模式

    • 第一重检查 if (inst == nullptr) 避免不必要的锁竞争

    • 只有实例未创建时才进入同步块

    • 第二重检查确保只有一个线程创建实例

  2. 线程安全

    • 使用 std::mutexstd::lock_guard 保证线程安全

    • lock_guard 采用 RAII 技术,自动管理锁的生命周期

  3. volatile 关键字

    • 防止编译器对指令进行重排序优化

    • 确保多线程环境下读取的是最新值,而不是寄存器中的缓存值

  4. 内存屏障问题

    • 在 C++11 之前,双重检查锁定可能存在指令重排序问题

    • inst = new T() 可能被重排序为:分配内存 → 赋值给 inst → 调用构造函数

    • 这可能导致其他线程看到非空但未完全构造的实例

    • C++11 的内存模型解决了这个问题,但使用 volatile 是额外的保障

  5. 构造函数和析构函数私有化

    • 防止外部创建实例

    • 防止通过拷贝构造或赋值操作创建新实例

  6. 优缺点

    • 优点:线程安全,延迟初始化,性能较好(大部分情况下无需加锁)

    • 缺点:实现相对复杂,需要注意指令重排序问题

这种实现方式既保证了线程安全,又避免了不必要的锁竞争,是懒汉单例模式的经典实现。


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 概念

线程安全:指多个线程同时访问共享资源时,能够正确执行而不会相互干扰或破坏彼此的执行结果。通常情况下,当多个线程并发执行仅包含局部变量的同一段代码时,不会产生不同的结果。但如果对全局变量或静态变量进行操作且未加锁保护,就容易出现线程安全问题。

重入:指同一个函数被不同执行流调用时,在前一个流程尚未执行完成时,又有其他执行流进入该函数。若一个函数在重入情况下仍能保持运行结果一致且不出现任何问题,则称为可重入函数,反之则为不可重入函数。

目前我们已经能够理解重入主要分为两种情况:

  1. 多线程重入函数
  2. 信号导致执行流重复进入函数

常见线程不安全的情况

• 未对共享变量进行保护的函数

• 函数状态随调用次数发生变化的函数

• 返回静态变量指针的函数

• 调用其他线程不安全函数的函数

常见不可重入的情况

• 调用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)**也是死锁产生的四个必要条件之一,指的是在并发系统中,当一个执行流(如进程或线程)因为请求新的资源而被阻塞时,仍然保持着已获得的资源不放。这种状况会导致多个执行流相互等待对方释放资源,从而形成死锁。

具体来说,请求与保持条件包含两个关键方面:

  1. 请求新资源:执行流在持有某些资源的同时,又尝试申请新的资源
  2. 保持已有资源:在申请新资源未成功时,不会释放已持有的资源

不剥夺条件(Non-preemptive Condition) 也称为不可抢占条件。该条件要求一个进程在执行过程中已获得的资源,在未使用完毕之前,其他进程或系统不能强行剥夺或抢占该资源。只有在进程主动释放资源后,其他进程才能获取这些资源。

关键点说明

  1. 资源持有状态

    • 进程在执行期间可能占用某些资源(如内存、I/O设备、文件锁等)。
    • 这些资源一旦被分配,除非进程主动释放,否则系统不能强制收回。
  2. 剥夺的影响

    • 如果系统允许强行剥夺资源(如CPU时间片轮转),可能导致进程执行异常或数据不一致。
    • 例如,一个进程正在写入文件时,若突然被剥夺磁盘访问权限,可能导致文件损坏。

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


6.3 避免死锁

死锁通常发生在多个进程或线程互相等待对方释放资源时。预防死锁需要从资源分配和请求策略入手。

破坏互斥条件

某些资源可以通过共享方式使用,避免独占。例如,只读文件可以允许多个进程同时访问,减少竞争。

破坏占有并等待条件

进程在开始执行前必须一次性申请所有所需资源。如果无法满足,则暂时不分配任何资源。这种方式可能导致资源利用率降低。

破坏非抢占条件

如果进程无法获得额外资源,必须释放已占有的资源。这种策略适用于状态容易保存和恢复的资源,如CPU寄存器。

破坏循环等待条件

对资源类型进行线性排序,要求进程按照编号顺序申请资源。例如,进程只能先申请编号较小的资源,再申请编号较大的资源。


避免死锁的算法

银行家算法

通过模拟资源分配检查系统是否处于安全状态。每次资源分配前,算法会判断剩余资源是否能满足至少一个进程的最大需求,从而避免进入不安全状态。

资源分配图算法

通过维护资源分配图检测是否存在环路。如果图中没有环路,则系统不会发生死锁;若存在环路,则可能发生死锁。


检测与恢复策略

定期检测死锁

通过资源分配图或等待图算法定期扫描系统状态。一旦检测到死锁,立即采取恢复措施。

终止进程

强制终止一个或多个死锁进程,释放其占用的资源。可以选择终止代价最小的进程,例如运行时间最短或资源占用最少的进程。

资源抢占

从某些进程中抢占资源分配给其他进程。被抢占资源的进程可能需要回滚到之前的检查点重新执行。

实际应用建议

  • 在编写多线程程序时,尽量按照固定顺序获取锁。
  • 使用超时机制,避免线程无限期等待资源。
  • 减少锁的粒度,使用细粒度锁代替粗粒度锁。
  • 优先使用高级并发工具(如信号量、条件变量)而非直接操作锁。

通过合理设计资源管理策略,可以显著降低死锁发生的概率。


7. STL、智能指针和线程安全

STL容器是否具备线程安全性?

答案是否定的。

这是由于STL在设计时优先考虑性能优化,而线程安全所需的锁机制会显著影响性能表现。此外,不同容器类型(如哈希表的表锁与桶锁,锁整个表(粗粒度)或锁单个桶(细粒度))需要采用不同的加锁策略,性能影响也各不相同。

因此,STL默认不提供线程安全保障。若要在多线程环境中使用,开发者需要自行实现线程安全机制。

智能指针是否是线程安全的?

unique_ptr 是线程安全的,这是因为:

  1. 所有权单一性:unique_ptr 采用独占所有权模式,任何时候一个资源只能由一个 unique_ptr 拥有
  2. 局部作用域:unique_ptr 的生命周期通常限定在当前代码块范围内,不会跨线程共享
  3. 转移所有权时的安全性:当通过 std::move 转移所有权时,操作是原子性的,不会产生竞态条件

shared_ptr 的线程安全性更为复杂,主要体现在以下几个方面:

  1. 引用计数的原子性

    • 标准库使用原子操作(CAS, Compare-And-Swap)保证引用计数操作是线程安全的
    • 引用计数的增减操作是原子的,不会出现竞态条件
  2. 控制块的线程安全

    • shared_ptr 的实现包含一个控制块,其中存储引用计数和弱引用计数
    • 控制块的修改都通过原子操作保护
  3. 数据访问的非原子性

    • 虽然引用计数操作是线程安全的,但对托管对象的访问仍需要额外同步
    • 多个线程同时访问同一个 shared_ptr 管理的对象时,需要单独的互斥锁

使用建议

  1. unique_ptr:当不需要跨线程共享所有权时优先使用,完全线程安全

  2. shared_ptr

    • 引用计数操作本身是线程安全的
    • 但需要共享数据时,仍需要额外的同步机制保护数据访问
    • 跨线程传递 shared_ptr 时,最好通过复制而非引用传递
  3. 性能考虑

    • shared_ptr 的原子操作会有一定性能开销
    • 在不需要线程安全的场景可以考虑使用 boost::shared_ptr 的非线程安全版本

8. 其他常见的各种锁(了解)

1. 悲观锁

  • 核心思想"总有刁民想害朕"

  • 工作方式 :我认为只要我去操作数据(无论是读还是写),肯定会有其他线程来和我争抢、修改数据。所以,在操作数据之前,我一定会先加锁,把数据"锁"起来,这样其他线程就会被阻塞在外,无法操作。等我操作完释放锁之后,其他线程才能进来。

  • 比喻 :就像你去一个只有一个坑位的公共卫生间,你悲观地认为肯定有人会来抢。所以你一进去就把门从里面反锁(加锁),这样别人就只能在外面等着(被阻塞)。等你上完厕所开门出来(释放锁),下一个人才能进去。

  • 常见实现synchronized 关键字、ReentrantLock 等。

  • 优点:简单粗暴,能保证绝对的线程安全。

  • 缺点:加锁和释放锁本身有性能开销,并且如果锁竞争激烈,会导致大量线程挂起和唤醒,非常耗时。


2. 乐观锁

  • 核心思想"应该没人会改吧,我先干了再说"

  • 工作方式 :我认为在我操作数据的时候,大概率不会有其他线程来修改它。所以我在操作数据前不上锁 ,直接就去读。但在更新数据的时候,我会判断一下这个数据在我读完之后、到我更新之前,有没有被别人动过。如果没动过,我就安心更新;如果动过了,我的更新就失败,然后我会选择重试或者报错。

  • 比喻:就像你用云笔记(如Git、Google Docs)。你打开文档直接编辑(不上锁)。当你写完点击"保存"时,系统会检查一下从你打开文档到现在,有没有别人也保存过(判断版本)。如果没有人保存过,你的内容就顺利存上去。如果别人已经保存过了,系统会提示你"你的版本已过期",让你基于最新的版本重新编辑(重试)。

  • 常见实现版本号机制CAS操作

  • 优点:在读多写少的场景下,性能极高,因为它避免了加锁的巨大开销。

  • 缺点:如果写操作非常频繁,更新失败重试的次数就会很多,反而可能降低性能(俗称"CPU空转")。


3. CAS操作

  • 是什么Compare-And-Swap(比较并交换) ,是乐观锁 最常用的一种具体实现技术

  • 工作流程:它包含三个操作数:

    1. V:内存中的当前值(我准备要更新的那个变量现在的值)

    2. A:我原先读取到的旧值(我期望内存中的值还是这个)

    3. 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空转 锁持有时间极短的场景
读写锁 读共享,写独占 允许多线程并发读,大幅提升读性能 实现相对复杂,写操作可能饿死 读多写少的并发场景

相关推荐
pofenx2 小时前
使用nps创建隧道,进行内网穿透
linux·网络·内网穿透·nps
desssq3 小时前
ubuntu 18.04 泰山派编译报错
linux·运维·ubuntu
Lzc7743 小时前
Linux的多线程
linux·linux的多线程
清风笑烟语3 小时前
Ubuntu 24.04 搭建k8s 1.33.4
linux·ubuntu·kubernetes
Dovis(誓平步青云)3 小时前
《Linux 基础指令实战:新手入门的命令行操作核心教程(第一篇)》
linux·运维·服务器
好名字更能让你们记住我3 小时前
MYSQL数据库初阶 之 MYSQL用户管理
linux·数据库·sql·mysql·adb·数据库开发·数据库架构
半桔4 小时前
【网络编程】TCP 服务器并发编程:多进程、线程池与守护进程实践
linux·服务器·网络·c++·tcp/ip
维尔切4 小时前
Shell 脚本编程:函数
linux·运维·自动化
穷人小水滴5 小时前
胖喵必快 (pmbs): btrfs 自动快照工具 (每分钟快照)
linux·rust