C++11并发编程:访问控制与锁机制如何协同保障线程安全
2026/8/27 3:06:12 网站建设 项目流程

1. 项目概述:为什么我们需要深入理解C++11的访问控制与并发原语?

如果你是从C++98/03时代过来的老手,或者正在学习现代C++的新人,看到“C++11相关特性”这个标题,可能会觉得这又是一个泛泛而谈的语法罗列。但今天,我想从一个不同的角度切入:我们为什么要在C++11这个节点上,重新审视classprotected/private/public这些“老掉牙”的访问控制,以及看似独立的“锁”机制?

答案很简单:因为C++11带来的不仅仅是新语法糖,它从根本上改变了我们构建软件,尤其是构建安全、高效并发程序的方式。在单线程时代,private数据成员意味着“类的内部实现细节,外部不可见”。但在多线程时代,一个被标记为privatestd::vector<int> m_data;,如果被多个线程通过public成员函数同时修改,它就不再是安全的“内部细节”,而是一个潜在的数据竞争(Data Race)炸弹。

C++11通过引入内存模型、原子操作、线程库等一系列特性,将并发编程正式纳入语言核心。这使得传统的面向对象封装(访问控制)与资源同步(锁)产生了深刻的联系。理解private不再仅仅是封装,更是思考“哪些数据需要被哪些线程以何种方式访问”的起点。而std::mutex等锁机制,则成为了守护这些数据访问边界、实现线程安全封装的关键工具

所以,这篇文章不会仅仅介绍autolambda(这些当然重要),而是聚焦于C++11如何重塑我们对类设计资源安全的认知。我们将深入探讨:在新的并发范式下,如何运用访问控制来设计线程安全的类接口?如何正确地将锁与数据成员结合?public接口设计要遵循哪些新原则?这些都是写出健壮现代C++代码必须跨越的坎。

2. 核心需求解析:从封装到线程安全的设计哲学转变

在C++11之前,我们设计一个类,核心诉求是封装职责清晰public是对外的承诺,private是内部的秘密。但在多线程成为标配的今天,类的设计增加了一个至关重要的维度:线程安全(Thread Safety)

2.1 传统封装的局限性

考虑一个经典的“银行账户”类,C++03风格:

// C++03 风格,非线程安全 class BankAccount { public: BankAccount(int balance) : balance_(balance) {} void deposit(int amount) { balance_ += amount; } void withdraw(int amount) { balance_ -= amount; } int getBalance() const { return balance_; } private: int balance_; // 私有数据,自以为安全 };

这个类完美地实践了封装:余额balance_private的,只能通过public成员函数修改。在单线程环境下,这没有问题。然而,一旦这个BankAccount对象被多个线程共享,灾难就来了。线程A和线程B可能同时调用deposit,它们都读取当前的balance_(比如100),分别加上50和30,然后写回。最终结果可能是130(丢失了一次加法),而不是预期的180。这就是数据竞争。

问题的根源在于:private关键字只提供了语法层面的访问限制,但并未提供运行时(Runtime)的并发访问保护。它阻止了外部函数直接访问balance_,但无法阻止两个线程通过合法的public接口同时操作它。

2.2 C++11引入的线程安全需求

C++11的<thread>库让创建线程变得轻而易举,同时也将并发安全的负担明确地交给了程序员。语言标准定义了“数据竞争”会导致未定义行为(Undefined Behavior),这意味着程序可能崩溃、产生错误结果,或者出现更诡异的状况。

因此,现代C++类的设计需求升级为:

  1. 保持封装性:内部状态(数据成员)对外部不可见。
  2. 提供线程安全:保证对象在并发访问下的行为是确定的、正确的。
  3. 维持接口简洁:不能因为线程安全而让类的使用者负担过重。

这直接引出了我们的核心工具:锁(Lock),特别是std::mutex。我们需要将锁与需要保护的数据绑定在一起思考,而访问控制关键字(private)则是实现这种绑定的蓝图。

3. 工具选型解析:C++11锁家族与访问控制的协同

C++11在<mutex>头文件中提供了一整套互斥量(Mutex)和锁(Lock)工具。选择正确的工具,并将其与类的访问控制策略结合,是设计的关键。

3.1 互斥量(Mutex)类型选择

  1. std::mutex(标准互斥量)

    • 是什么:最基本的互斥量,提供独占的、非递归的锁。
    • 何时用:保护那些不需要在同一个线程内重复上锁的普通数据成员。这是最常用、开销相对较小的选择。
    • 与访问控制结合:通常作为需要保护的private数据成员的“伙伴”,一起声明在类中。
  2. std::recursive_mutex(递归互斥量)

    • 是什么:允许同一个线程多次获取其锁,并在解锁相同次数后释放。
    • 何时用:当你设计的publicprivate成员函数可能发生递归调用,且这些调用都需要访问同一受保护资源时。例如,一个函数A加锁后调用了另一个也需要加锁的函数B,而B可能通过某种路径又调回A。
    • 注意:递归锁通常意味着设计复杂,应优先考虑重构代码来避免递归加锁需求。
  3. std::timed_mutex/std::recursive_timed_mutex(定时互斥量)

    • 是什么:在mutex基础上,提供了try_lock_fortry_lock_until方法,可以尝试在指定时间内获取锁。
    • 何时用:用于避免死锁或实现更复杂的同步策略,当线程不愿意无限期等待一个锁时。
    • 实操心得:在普通业务代码中较少使用,多用于底层库或对实时性有严格要求的系统。滥用可能导致逻辑复杂和性能问题。

3.2 锁管理器(Lock Wrapper)的选择

直接操作mutexlock()unlock()是危险的,因为异常或提前返回可能导致锁无法释放,造成死锁。C++11提供了RAII(Resource Acquisition Is Initialization)风格的锁管理器。

  1. std::lock_guard

    • 是什么:最简单的RAII锁管理器。构造时加锁,析构时自动解锁。
    • 何时用绝大多数情况下的首选。当作用域(Scope)内的代码都需要持有锁时使用。它不可复制,也不能手动解锁。
    void deposit(int amount) { std::lock_guard<std::mutex> lock(mutex_); // 进入函数即加锁 balance_ += amount; } // 函数结束,lock析构,自动解锁
  2. std::unique_lock

    • 是什么:比lock_guard更灵活的RAII锁管理器。支持延迟加锁、手动加解锁、转移所有权,并且可以配合条件变量使用。
    • 何时用
      • 需要配合std::condition_variable时(必须使用unique_lock)。
      • 需要延迟加锁(std::defer_lock)以实现多个互斥量的死锁避免排序(std::lock)。
      • 需要手动暂时解锁(unlock())以执行一些不需要锁的操作(如I/O),然后再重新加锁。
    • 注意:灵活性带来轻微的性能开销和复杂度,除非有上述需求,否则用lock_guard更清晰。

重要提示:永远优先考虑使用std::lock_guardstd::unique_lock,而不是直接调用mutex.lock()。这是利用C++ RAII机制避免资源泄漏的黄金法则。

3.3 访问控制与锁的声明位置

这是一个关键的设计决策:锁应该放在哪里?

// 方案A:锁作为私有成员 class ThreadSafeAccount { private: mutable std::mutex mutex_; // mutable,因为const成员函数也需要修改它(加锁) int balance_; public: // ... 成员函数使用 mutex_ ... }; // 方案B:锁作为公有成员(极其罕见且危险) class BadDesignAccount { public: std::mutex mutex_; // 危险! int balance_; };

最佳实践是方案A:将互斥量声明为类的private(或protected)成员,并标记为mutable(如果需要在const成员函数中加锁)。为什么?

  • 封装性:锁是实现线程安全内部机制的一部分,属于“实现细节”,不应该暴露给用户。
  • 安全性:如果锁是public的,用户代码可以随意获取和操作这个锁,极易造成死锁(例如,用户锁住后忘记解锁,或在错误的时间解锁)。
  • 不变性(Invariant)维护:类的线程安全不变性(如“余额修改是原子的”)应由类自己负责维护,而不是依赖用户正确使用一个公开的锁。

因此,private不仅用于隐藏数据,也用于隐藏同步原语,确保类的线程安全策略是自包含的、强制的。

4. 核心细节解析:设计线程安全的类接口

有了工具,我们来看如何设计类的publicprotectedprivate部分,来构建一个真正线程安全的类。

4.1 Public接口设计:提供完整操作,而非原始数据

这是最重要的原则。一个线程安全的类,其public接口应该提供完整的、原子的操作,而不是返回内部数据的引用或指针,让用户自己去组合可能不安全的操作。

反面教材

class UnsafeVector { public: std::vector<int>& getData() { return data_; } // 灾难!返回了内部数据的引用 std::mutex& getMutex() { return mutex_; } // 更糟!连锁都暴露了 private: std::vector<int> data_; mutable std::mutex mutex_; }; // 用户可能这样用,导致锁的持有范围不明确,极易死锁或数据竞争。

正确做法

class ThreadSafeVector { public: void push_back(int value) { std::lock_guard<std::mutex> lock(mutex_); data_.push_back(value); } int at(size_t index) const { std::lock_guard<std::mutex> lock(mutex_); // 可能还需要边界检查 return data_.at(index); } // 提供一个“快照”功能,而不是返回引用 std::vector<int> getSnapshot() const { std::lock_guard<std::mutex> lock(mutex_); return data_; // 返回副本,调用者可以安全使用 } // 或者提供一个接受函数对象的“线程安全访问器” template<typename Func> auto operateOnData(Func f) const -> decltype(f(data_)) { std::lock_guard<std::mutex> lock(mutex_); return f(data_); } private: std::vector<int> data_; mutable std::mutex mutex_; };

getSnapshot返回副本,虽然可能有性能开销,但保证了调用者拿到的是一个瞬间的、一致的状态,并且可以在无锁的情况下自由使用。operateOnData模式则允许用户在锁的保护下执行一个自定义操作,非常灵活。

4.2 Private数据与锁的“结对”管理

哪些数据需要锁保护?一个简单的规则是:所有非静态、非原子的数据成员,如果可能被多个线程并发访问,就需要被锁保护。通常,一个类有一个核心的内部状态,用一个互斥量保护就足够了。这被称为粗粒度锁,简单有效。

class SimpleCache { private: // 被保护的核心状态 std::unordered_map<std::string, std::string> cache_; // 保护上述状态的锁 mutable std::mutex cache_mutex_; // 其他可能不需要同步的辅助状态,比如线程ID std::thread::id owner_thread_id_; };

更复杂的情况可能需要多个互斥量(细粒度锁)来保护不同的数据子集,以提升并发度。但这会极大地增加死锁的风险,需要谨慎设计锁的获取顺序(例如使用std::lock来一次性锁定多个std::unique_lock)。

4.3 Protected成员与继承中的线程安全

protected成员在涉及继承时带来了特殊的挑战。基类的protected数据成员可以被派生类直接访问,这意味着:

  1. 如果基类用锁保护了这些数据,派生类在访问时也必须先获取基类的锁。但这要求派生类知晓基类锁的存在和获取方式,破坏了封装。
  2. 如果基类没有保护,那么线程安全的责任就完全落在了派生类上,容易出错。

建议方案

  • 避免在基类中提供protected数据成员。改为提供protected的、线程安全的访问函数。这样派生类只能通过基类规定的安全接口来访问数据。
  • 如果必须要有protected数据,那么基类应该提供一个protected的、返回锁引用或指针的函数,并要求派生类在访问数据前必须先获取该锁。但这是一种脆弱的设计。
  • 更现代的做法是考虑使用组合而非继承,或者使用纯虚接口,将线程安全的实现完全下放到派生类。

4.4 Const成员函数与Mutable Mutex

const成员函数承诺不修改对象的“逻辑状态”。但加锁这个动作本身需要修改互斥量(mutex)的内部状态。为了解决这个矛盾,我们需要将互斥量声明为mutable

class ThreadSafeCounter { public: int getCount() const { // const 成员函数 std::lock_guard<std::mutex> lock(mutex_); // 锁是 mutable 的,所以可以 return count_; } void increment() { std::lock_guard<std::mutex> lock(mutex_); ++count_; } private: int count_ = 0; mutable std::mutex mutex_; // 关键:mutable };

这里,getCountconst的,因为它不改变count_的逻辑值(它只是读取)。但为了线程安全地读取,它需要修改mutex_mutable允许在const成员函数中修改此类“与逻辑状态无关的物理状态”。

5. 实操过程:实现一个生产就绪的线程安全队列

让我们综合运用以上知识,实现一个经典的线程间通信组件:有界阻塞队列(Bounded Blocking Queue)。它将是public接口设计、private数据保护、锁与条件变量配合的绝佳示例。

5.1 类定义与成员变量

#include <queue> #include <mutex> #include <condition_variable> #include <chrono> #include <stdexcept> template<typename T> class ThreadSafeBoundedQueue { public: explicit ThreadSafeBoundedQueue(size_t max_size) : max_size_(max_size) {} // 核心接口:阻塞式推送和弹出 bool push(const T& value, std::chrono::milliseconds timeout = std::chrono::milliseconds(0)); bool pop(T& value, std::chrono::milliseconds timeout = std::chrono::milliseconds(0)); // 非阻塞接口 bool try_push(const T& value); bool try_pop(T& value); // 状态查询 bool empty() const; bool full() const; size_t size() const; private: // 内部存储 std::queue<T> queue_; const size_t max_size_; // 队列容量上限 // 同步原语 mutable std::mutex mutex_; // 保护整个内部状态 std::condition_variable not_empty_cv_; // 队列非空的条件变量 std::condition_variable not_full_cv_; // 队列未满的条件变量 // 辅助函数(私有,因为不直接暴露给用户) bool is_empty_unsafe() const { return queue_.empty(); } bool is_full_unsafe() const { return queue_.size() >= max_size_; } };

设计解析

  • private部分清晰分为三块:数据(queue_,max_size_)、同步原语(mutex_,*_cv_)、辅助函数。所有public接口都必须通过锁来访问这些private成员。
  • 使用了两个std::condition_variable,分别用于在队列空时等待弹出,在队列满时等待推送。这是实现高效阻塞操作的关键。
  • 辅助函数is_empty_unsafe等被标记为private,因为它们假设调用者已经持有锁(mutex_)。暴露它们会误导用户在不加锁的情况下调用。

5.2 核心成员函数实现:Push与Pop

我们以实现带有超时的pushpop为例:

template<typename T> bool ThreadSafeBoundedQueue<T>::push(const T& value, std::chrono::milliseconds timeout) { std::unique_lock<std::mutex> lock(mutex_); // 必须用unique_lock,以配合条件变量 // 1. 等待“队列未满”的条件 // 使用条件变量的“带谓词”等待,防止虚假唤醒 if (timeout.count() > 0) { // 超时等待 if (!not_full_cv_.wait_for(lock, timeout, [this]() { return !is_full_unsafe(); })) { return false; // 超时,推送失败 } } else { // 无限期等待 not_full_cv_.wait(lock, [this]() { return !is_full_unsafe(); }); } // 2. 条件满足,执行推送 queue_.push(value); // 3. 通知一个正在等待“非空”的线程 not_empty_cv_.notify_one(); // 也可以用notify_all(),但通常一个就够了 return true; } template<typename T> bool ThreadSafeBoundedQueue<T>::pop(T& value, std::chrono::milliseconds timeout) { std::unique_lock<std::mutex> lock(mutex_); // 等待“队列非空”的条件 if (timeout.count() > 0) { if (!not_empty_cv_.wait_for(lock, timeout, [this]() { return !is_empty_unsafe(); })) { return false; // 超时,弹出失败 } } else { not_empty_cv_.wait(lock, [this]() { return !is_empty_unsafe(); }); } // 条件满足,执行弹出 value = std::move(queue_.front()); // 使用移动语义提高效率 queue_.pop(); // 通知一个正在等待“未满”的线程 not_full_cv_.notify_one(); return true; }

关键点解析

  1. std::unique_lock的必要性std::condition_variable::wait会原子地释放锁并将线程挂起,当被唤醒时又会重新获取锁。这个“释放-获取”的操作必须由std::unique_lock来支持,std::lock_guard做不到。
  2. 带谓词(Predicate)的等待wait(lock, predicate)等同于while (!predicate()) wait(lock);。这是防止虚假唤醒(Spurious Wakeup)的标准做法。即使条件变量无缘无故返回了,循环检查也会确保条件真正满足后才继续。
  3. 移动语义:在pop中,我们使用std::move将队首元素移出,避免不必要的拷贝。这要求类型T支持移动构造或移动赋值。
  4. 通知策略:这里使用notify_one()。因为一次推送只让队列多了一个元素,通常只需要唤醒一个消费者线程。如果使用notify_all(),会唤醒所有等待的消费者,但只有一个能成功获取元素,其他线程会再次进入等待,造成不必要的上下文切换开销。但在某些特定场景(如多个线程等待同一个复杂条件),notify_all()可能是必要的。

5.3 非阻塞接口与状态查询

template<typename T> bool ThreadSafeBoundedQueue<T>::try_push(const T& value) { std::lock_guard<std::mutex> lock(mutex_); // 这里用lock_guard就够了 if (is_full_unsafe()) { return false; } queue_.push(value); not_empty_cv_.notify_one(); return true; } template<typename T> bool ThreadSafeBoundedQueue<T>::try_pop(T& value) { std::lock_guard<std::mutex> lock(mutex_); if (is_empty_unsafe()) { return false; } value = std::move(queue_.front()); queue_.pop(); not_full_cv_.notify_one(); return true; } template<typename T> bool ThreadSafeBoundedQueue<T>::empty() const { std::lock_guard<std::mutex> lock(mutex_); return is_empty_unsafe(); // 调用内部辅助函数 } template<typename T> bool ThreadSafeBoundedQueue<T>::full() const { std::lock_guard<std::mutex> lock(mutex_); return is_full_unsafe(); } template<typename T> size_t ThreadSafeBoundedQueue<T>::size() const { std::lock_guard<std::mutex> lock(mutex_); return queue_.size(); }

注意empty(),full(),size()这些状态查询函数也是线程安全的,因为它们内部也加了锁。但这里有一个经典的竞态条件(Race Condition)陷阱需要向使用者说明:你调用empty()得到false,然后调用pop(),在这两个调用之间,可能其他线程已经把最后一个元素弹出了,导致pop()阻塞或失败。因此,这类状态查询函数的结果通常是“瞬间的”,不能作为后续操作的绝对依据。可靠的模式是直接使用会阻塞或返回成功状态的pop操作。

6. 常见问题与排查技巧实录

在实际使用C++11的并发特性和设计线程安全类时,你会遇到各种坑。以下是我踩过的一些坑和总结的技巧。

6.1 死锁(Deadlock)的预防与排查

死锁通常发生在需要锁定多个互斥量时。C++11提供了std::lock函数来帮助解决此问题。

问题场景:你需要同时锁住两个账户才能完成转账。

// 错误示例:可能死锁 void transfer(ThreadSafeAccount& from, ThreadSafeAccount& to, int amount) { std::lock_guard<std::mutex> lock1(from.mutex); // 线程A锁from,线程B锁to std::lock_guard<std::mutex> lock2(to.mutex); // 线程A尝试锁to(被B持有),线程B尝试锁from(被A持有)-> 死锁 // ... 转账操作 }

解决方案:使用std::lock一次性锁定多个锁,并配合std::adopt_lock

// 正确示例:使用std::lock避免死锁 void transfer(ThreadSafeAccount& from, ThreadSafeAccount& to, int amount) { std::unique_lock<std::mutex> lock1(from.mutex, std::defer_lock); // 延迟加锁 std::unique_lock<std::mutex> lock2(to.mutex, std::defer_lock); std::lock(lock1, lock2); // 一次性锁定两个,内部使用算法避免死锁 // ... 转账操作 // lock1和lock2会在析构时自动解锁 }

排查技巧

  • 锁顺序:如果无法使用std::lock,确保所有线程都以相同的全局顺序获取锁。例如,按照账户ID排序后加锁。
  • 工具辅助:在Linux下可以使用helgrindThreadSanitizer-fsanitize=thread)来检测死锁和数据竞争。
  • 简化设计:尽可能减少需要同时持有的锁的数量。考虑是否可以重构代码,使得一个操作只涉及一个受保护资源。

6.2 条件变量使用陷阱

  1. 虚假唤醒:前面已经提到,必须使用带谓词的等待循环:cv.wait(lock, []{ return condition; });
  2. 丢失唤醒(Lost Wake-up):如果在调用wait()之前,另一个线程就调用了notify_*(),那么这个通知可能会被丢失,导致等待线程永远休眠。
    • 根源:检查条件和进入等待不是原子的。
    • 解决:这正是条件变量与互斥量配合使用的意义。在调用wait之前,你必须已经持有与条件变量关联的锁(lock),并且条件的检查必须在锁的保护下进行。wait函数会原子地释放锁并进入等待,从而保证了在调用wait的那个时间点,不会有通知被错过。
  3. notify_one()vsnotify_all()
    • notify_one():唤醒一个等待线程。效率高,但如果你不确定哪个或多少个线程应该被唤醒,可能造成线程“饿死”。
    • notify_all():唤醒所有等待线程。确保不会漏掉该被唤醒的线程,但可能引起“惊群效应”,大量线程被唤醒但只有一个能继续工作,造成性能抖动。
    • 经验:如果每次条件满足只允许一个线程继续工作(如单元素队列),用notify_one()。如果条件满足允许多个线程继续(如资源池有多个资源可用),或者你无法精确知道该唤醒谁,用notify_all()更安全。

6.3 性能优化考量

锁是性能瓶颈。以下是一些优化思路:

  1. 减小锁的粒度(锁范围):只锁住真正需要保护的代码段。尽快释放锁。
    void processData(const Data& d) { // 一些不需要锁的预处理 Result intermediate = expensive_preprocess(d); { std::lock_guard<std::mutex> lock(mutex_); // 只锁住共享数据访问部分 shared_storage_.update(intermediate); } // 锁在这里释放 // 一些不需要锁的后处理 log_result(intermediate); }
  2. 使用读写锁(C++14引入std::shared_timed_mutex,C++17引入std::shared_mutex:对于读多写少的场景,允许多个线程同时读,但写独占。这可以显著提升并发读性能。
  3. 考虑无锁(Lock-Free)数据结构:对于极端性能要求的场景,可以使用std::atomic和相关内存序(Memory Order)来设计无锁结构。但这非常复杂,容易出错,除非有充分证据表明锁是性能瓶颈,否则不要轻易尝试。
  4. 避免在锁内调用用户代码或执行慢操作:例如I/O、内存分配、虚函数调用(可能指向未知实现)等。这会导致锁被长时间持有,严重影响并发度。

6.4 线程安全与异常安全

异常安全(Exception Safety)与线程安全紧密相关。如果在一个持有锁的成员函数中抛出了异常,并且异常未被捕获,那么锁可能无法释放,导致死锁。

解决方案:充分利用RAII。std::lock_guardstd::unique_lock在析构时会自动解锁,即使异常发生。这是为什么我们绝对推荐使用它们而不是手动lock/unlock的原因。

但是,你需要确保在加锁和析构锁之间,对象的不变性(Invariants)始终被保持。例如,在转账操作中,先从A账户扣钱,然后给B账户加钱。如果在“扣钱成功”但“加钱失败”(抛出异常)时,你需要决定是回滚(把A的钱加回去)还是让系统处于一个中间状态。这通常需要更高级的事务语义或补偿机制,超出了简单锁的保护范围。

一个实用的建议是:尽量让受锁保护的操作保持简单、原子,避免在锁内进行可能失败或复杂的操作。如果操作复杂,考虑先拷贝数据到局部变量,在锁外进行计算,最后在锁内快速完成状态更新。

7. 从C++11到C++14/17/20:访问控制与并发的发展

C++11奠定了现代C++并发的基础,但后续标准带来了更多便利工具,让我们能写出更安全、更清晰的代码。

  • C++14:std::shared_timed_mutex提供了读写锁,用于读多写少的场景。std::shared_lock用于共享(读)锁,std::unique_lock用于独占(写)锁。

  • C++17:std::scoped_lock这是std::lock_guard的增强版,可以同时锁住多个互斥量,并且内部使用std::lock来避免死锁。语法更简洁:

    // C++17 更优雅的死锁避免 void transfer(ThreadSafeAccount& from, ThreadSafeAccount& to, int amount) { std::scoped_lock lock(from.mutex, to.mutex); // 自动推导模板参数 // ... 操作 }
  • C++20:协程与std::atomic_refC++20的协程为异步编程提供了新的范式,虽然不直接替代锁,但改变了我们组织并发代码的方式。std::atomic_ref允许将现有对象作为原子对象进行访问,为某些特定场景提供了无锁编程的便利。

尽管工具在进化,但核心设计原则不变:通过private封装数据与同步原语,通过精心设计的public接口提供线程安全的原子操作。理解C++11带来的这一根本性转变,是编写健壮、高效现代C++程序的基石。当你下次声明一个private成员时,不妨多思考一句:它需要被哪些线程访问?我该用什么样的锁来保护它?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询