面试题答案
一键面试- 生产者 - 消费者模型概述:
- 生产者 - 消费者模型是一种经典的并发设计模式,用于在多线程或多进程环境中协调数据的生产和消费。生产者线程负责生成数据并将其放入共享缓冲区,消费者线程从共享缓冲区取出数据进行处理。
- 同步机制使用场景和作用:
- 互斥锁(Mutex):
- 使用场景:用于保护共享资源(这里指缓冲区),防止多个线程同时访问缓冲区导致数据不一致。
- 作用:保证在同一时刻只有一个线程能够访问缓冲区。例如,当生产者线程要向缓冲区写入数据时,它首先获取互斥锁,写完数据后释放互斥锁;消费者线程读取数据时同理。
- 条件变量(Condition Variable):
- 使用场景:用于线程间的同步通信,当缓冲区为空(消费者等待数据)或缓冲区已满(生产者等待空间)时使用。
- 作用:当缓冲区为空时,消费者线程可以等待在条件变量上,当生产者向缓冲区写入数据后,通过条件变量通知等待的消费者线程;同理,当缓冲区已满时,生产者线程等待在条件变量上,消费者取出数据后通知生产者。
- 互斥锁(Mutex):
- 代码示例(以C++为例):
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
std::mutex mtx;
std::condition_variable cv_producer;
std::condition_variable cv_consumer;
std::queue<int> buffer;
const int buffer_size = 5;
int data_to_produce = 0;
void producer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待缓冲区有空间
cv_producer.wait(lock, [] { return buffer.size() < buffer_size; });
buffer.push(data_to_produce++);
std::cout << "Produced: " << buffer.back() << std::endl;
lock.unlock();
cv_consumer.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待缓冲区有数据
cv_consumer.wait(lock, [] { return!buffer.empty(); });
int data = buffer.front();
buffer.pop();
std::cout << "Consumed: " << data << std::endl;
lock.unlock();
cv_producer.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
- 可能出现的问题及解决办法:
- 死锁:
- 原因:如果两个或多个线程相互等待对方释放资源,就会出现死锁。例如,生产者线程获取了互斥锁,然后等待条件变量通知缓冲区有空间,而此时消费者线程也获取了互斥锁,等待条件变量通知缓冲区有数据,这样两个线程就会一直等待下去,造成死锁。
- 解决办法:
- 避免嵌套锁:尽量减少锁的嵌套使用,在上述代码中,我们在等待条件变量时,使用
std::unique_lock
,它会在等待时自动释放互斥锁,在条件满足后重新获取锁,避免了死锁。 - 按照固定顺序获取锁:如果必须使用多个锁,确保所有线程按照相同的顺序获取锁。例如,在复杂场景下如果还有其他锁,所有线程都先获取
mtx
,再获取其他锁。
- 避免嵌套锁:尽量减少锁的嵌套使用,在上述代码中,我们在等待条件变量时,使用
- 死锁:
在Java中,也有类似的实现方式,使用synchronized
关键字和wait()
、notify()
方法来实现生产者 - 消费者模型:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Producer implements Runnable {
private BlockingQueue<Integer> queue;
private int dataToProduce = 0;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
queue.put(dataToProduce++);
System.out.println("Produced: " + (dataToProduce - 1));
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
private BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
int data = queue.take();
System.out.println("Consumed: " + data);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
Thread producerThread = new Thread(new Producer(queue));
Thread consumerThread = new Thread(new Consumer(queue));
producerThread.start();
consumerThread.start();
}
}
这里BlockingQueue
内部已经实现了同步机制,put()
方法在队列满时会等待,take()
方法在队列空时会等待,从而避免了手动管理锁和条件变量时可能出现的死锁等问题。