面试题答案
一键面试挑战体现
- 线程安全问题:
- 多个异步操作可能同时访问和修改共享资源,比如共享内存中的数据结构。例如,一个异步读操作和一个异步写操作同时针对同一个缓冲区进行操作,可能导致数据不一致。
- 锁争用:
- 为了保证共享资源的一致性,通常会使用锁机制。但在高并发情况下,大量线程竞争锁会导致性能瓶颈。例如,多个异步任务都需要获取同一把锁来访问共享资源,频繁的锁获取和释放会增加系统开销。
- 死锁风险:
- 当多个异步操作相互等待对方释放资源时,可能发生死锁。比如,线程A持有资源R1并等待资源R2,而线程B持有资源R2并等待资源R1,就形成了死锁。
- 资源泄漏:
- 在异步操作中,如果资源(如文件描述符、网络连接等)没有正确释放,就会导致资源泄漏。例如,一个异步I/O操作创建了一个网络连接,但由于异常或逻辑错误没有关闭连接,随着时间推移,系统资源会被耗尽。
- 同步原语的复杂性:
- 使用信号量、条件变量等同步原语来协调异步操作时,其使用方式较为复杂。如果使用不当,可能导致同步逻辑错误。例如,条件变量的唤醒时机不当,可能导致线程长时间等待或不必要的唤醒。
解决方案
- 技术选型:
- 互斥锁(Mutex):用于保护共享资源,确保同一时间只有一个线程可以访问。在C++中可以使用
std::mutex
,在Java中可以使用synchronized
关键字或ReentrantLock
类。例如在C++中:
- 互斥锁(Mutex):用于保护共享资源,确保同一时间只有一个线程可以访问。在C++中可以使用
#include <iostream>
#include <mutex>
std::mutex mtx;
int shared_variable = 0;
void increment() {
mtx.lock();
shared_variable++;
mtx.unlock();
}
- 读写锁(Read - Write Lock):适用于读多写少的场景,允许多个线程同时进行读操作,但写操作时需要独占资源。在C++中有
std::shared_mutex
,在Java中可以使用ReadWriteLock
接口及其实现类。例如在Java中:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RWExample {
private ReadWriteLock lock = new ReentrantReadWriteLock();
private int data = 0;
public void read() {
lock.readLock().lock();
try {
System.out.println("Reading data: " + data);
} finally {
lock.readLock().unlock();
}
}
public void write(int newData) {
lock.writeLock().lock();
try {
data = newData;
System.out.println("Writing data: " + newData);
} finally {
lock.writeLock().unlock();
}
}
}
- 信号量(Semaphore):可以控制同时访问共享资源的线程数量。在C++中可以使用
std::counting_semaphore
,在Java中可以使用Semaphore
类。例如在Java中:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final int MAX_CONNECTIONS = 5;
private static Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);
public void accessResource() throws InterruptedException {
semaphore.acquire();
try {
System.out.println("Accessing resource...");
} finally {
semaphore.release();
}
}
}
- 条件变量(Condition Variable):用于线程间的同步,线程可以等待某个条件满足后再继续执行。在C++中可以使用
std::condition_variable
,在Java中可以使用Condition
接口。例如在C++中:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print_id(int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
-
理论依据:
- 互斥锁:基于互斥原则,通过限制同一时间只有一个线程访问共享资源,保证数据一致性。
- 读写锁:利用读操作不改变数据的特性,允许多个读操作并行,提高系统并发性能。写操作需要独占资源以保证数据修改的原子性和一致性。
- 信号量:基于资源计数的思想,通过控制信号量的计数值来限制同时访问共享资源的线程数量,避免资源过度使用。
- 条件变量:提供了一种线程间的同步机制,使得线程可以等待某个条件满足后再继续执行,避免无效的忙等待,提高系统效率。
-
资源管理:
- 智能指针:在C++中使用智能指针(如
std::unique_ptr
、std::shared_ptr
)来管理动态分配的资源,确保资源在不再使用时自动释放,防止内存泄漏。例如:
- 智能指针:在C++中使用智能指针(如
#include <memory>
class Resource {
public:
Resource() { std::cout << "Resource created" << std::endl; }
~Resource() { std::cout << "Resource destroyed" << std::endl; }
};
void func() {
std::unique_ptr<Resource> res = std::make_unique<Resource>();
// 函数结束时,res自动释放资源
}
- 资源池:对于一些昂贵的资源(如数据库连接、网络连接等),可以使用资源池技术。预先创建一定数量的资源,当需要使用时从资源池中获取,使用完毕后归还到资源池。在Java中可以使用
Apache Commons DBCP
等连接池库来管理数据库连接。例如:
import org.apache.commons.dbcp2.BasicDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class ConnectionPoolExample {
private static BasicDataSource dataSource = new BasicDataSource();
static {
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("root");
dataSource.setPassword("password");
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
- 死锁检测与预防:
- 死锁预防:破坏死锁产生的四个必要条件(互斥、占有并等待、不可剥夺、循环等待)。例如,采用资源分配图算法,在资源分配前进行检查,确保不会形成循环等待。
- 死锁检测:定期检查系统中是否存在死锁。在操作系统层面,可以使用死锁检测算法,如资源分配图算法(RAG算法)。在应用层面,可以记录每个线程获取和等待资源的情况,通过分析这些信息来检测死锁。一旦检测到死锁,可以选择终止某个或某些线程来打破死锁。