面试题答案
一键面试面临的问题
- 不同线程对常对象的访问:如果多个线程同时读取常对象,虽然不会有数据竞争问题,但可能由于缓存一致性问题导致某些线程读取到旧数据。如果有线程试图修改常对象(即使通过一些非法手段,如
const_cast
),会破坏对象的常量性,导致不可预测的行为。 - 常对象内部状态一致性:常对象的设计初衷是其状态不可变,但在多线程环境下,若对象内部存在一些依赖外部状态或需要动态更新的部分(例如,缓存某些全局配置信息且该配置信息可能被其他线程修改),很难保证其状态一致性。
解决方案及优缺点
- 使用互斥锁(
std::mutex
)- 实现方式:在访问常对象的成员函数中,使用互斥锁来保护对常对象内部数据的访问。例如:
class MyConstClass {
public:
MyConstClass() {}
void readData() const {
std::lock_guard<std::mutex> lock(mutex_);
// 读取数据操作
}
private:
mutable std::mutex mutex_;
// 常对象的数据成员
};
- **优点**:实现简单直观,能有效避免数据竞争,保证常对象内部状态的一致性。
- **缺点**:会引入锁开销,降低程序性能,尤其是在高并发场景下,锁竞争可能会成为性能瓶颈。
2. 使用读写锁(std::shared_mutex
)
- 实现方式:当线程只读取常对象时,使用共享锁(std::shared_lock
);当需要修改常对象(假设在一些特殊情况下允许通过特定接口修改)时,使用独占锁(std::unique_lock
)。例如:
class MyConstClass {
public:
MyConstClass() {}
void readData() const {
std::shared_lock<std::shared_mutex> lock(mutex_);
// 读取数据操作
}
void modifyData() {
std::unique_lock<std::shared_mutex> lock(mutex_);
// 修改数据操作
}
private:
mutable std::shared_mutex mutex_;
// 常对象的数据成员
};
- **优点**:读操作可以并发执行,提高了读性能,适合读多写少的场景。
- **缺点**:实现相对复杂,需要根据操作类型选择合适的锁,且写操作时仍然会阻塞其他读写操作,可能导致写操作饥饿。
实际项目应用场景
- 数据库连接池:常对象可以表示数据库连接池的配置信息,如最大连接数、超时时间等。多个线程可以同时读取这些配置信息来获取数据库连接,而配置信息在运行时通常不会改变,保证了常对象的常量性。通过使用互斥锁或读写锁,可以确保在多线程环境下配置信息的一致性,避免因并发访问导致的错误。
- 全局配置对象:在大型项目中,常对象可以用来存储全局配置,如日志级别、服务器地址等。各个线程可以安全地读取这些配置,而不会产生数据竞争。当需要动态更新配置时,可以通过特定的线程安全接口进行修改,保证配置信息的一致性。