阿里面试:如何写一个缓冲区?
缓冲区是可以被多个线程存数据和取数据,那么这个时候可能会发生线程安全问题,比如不能同时对缓冲区进行存或取,缓冲区没有数据就不能取,缓冲区满了就不能存,这其实就是生产者消费者模型。
注意,这与读者-写者问题的本质不同在于,读者可以有多个,他们可以一起读,不会发生安全问题,只是读和写是互斥的。而生产者是对数据进行增加,消费者对数据进行删除,是会发生安全问题的,生产者与生产者、生产者与消费者、消费者与消费者之间都应该互斥
简易版生产者—消费者模型
所谓简易版指的是生产者和消费者都只有一个线程
生产者
生产者要做的事就是往缓冲区存数据
public class Producer extends Thread{
private Buffer<Integer> buffer;
public Producer(Buffer<Integer> buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("生产者正在生产数据"+i);
buffer.add(i);
}
}
}
消费者
public class Consumer extends Thread{
private Buffer<Integer> buffer;
public Consumer(Buffer<Integer> buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
Integer data = buffer.pull();
System.out.println("消费者拿到数据:"+data);
}
}
}
缓冲区
public class Buffer<T> {
private Queue<T> bufferData = new LinkedList<>();
private int size;
public Buffer(int size) {
this.size = size;
}
public synchronized void add(T data){
if(bufferData.size() >= size){
System.out.println("缓冲区已满,请等待消费者消费数据");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
bufferData.offer(data);
notify();
}
public synchronized T pull(){
if(bufferData.size() == 0){
System.out.println("缓冲区无数据,请等待生产者生产数据");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T data = bufferData.poll();
notify();
return data;
}
}
测试
public class ProducerConsumer {
public static void main(String[] args) {
Buffer<Integer> buffer = new Buffer<>(3);
new Producer(buffer).start();
new Consumer(buffer).start();
}
}
测试结果
进阶版本
那其实我们平时实际场景中,生产者和消费者都可以有多个,那就需要对上面的代码进行更改
我们看看上面缓冲区的存数据方法的问题
public synchronized void add(T data){
if(bufferData.size() >= size){
wait();
bufferData.offer(data);
notify();
}
因为有很多线程,notify应该变成notifyAll,我们希望缓冲区满的时候唤醒的是消费者线程,让他来消费。而notify只能唤醒一个线程,你不知道唤醒的这个线程到底是生产者还是消费者,notifyAll可以唤醒所有线程。
那么if应该也要变成while(),因为你notifyAll也会唤醒其他生产者,而这个时候缓冲区可能还是满的,所以应该是循环等待,不然就出线程安全问题了
这就是改进版代码,同样地取数据也应该改成这样
public synchronized void add(T data){
while(bufferData.size() >= size){
wait();
bufferData.offer(data);
notifyAll();
}
而上面的方法,将生产者消费者糅杂在一起了,一次性全都唤醒会导致大量的锁竞争,而这非常消耗系统资源。我们的想法是缓冲区满了,生产者不生产,我只想唤醒消费者去竞争锁,我不想让生产者也唤醒,那么怎么处理呢?
那么我们可以使用Condition条件,来将消费者和生产者加入到不同的等待队列,这样就解决了,这就要用到ReentrantLock
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Buffer<T> {
private Queue<T> bufferData = new LinkedList<>();
Lock lock;
Condition producerCondition;
Condition consumerCondition;
private int size;
public Buffer(int size) {
lock = new ReentrantLock();
producerCondition = lock.newCondition();
consumerCondition = lock.newCondition();
this.size = size;
}
public void add(T data){
try {
lock.lock();
while(bufferData.size() >= size){
System.out.println("缓冲区已满,请等待消费者消费数据");
producerCondition.await();
}
System.out.println("生产者"+Thread.currentThread().getName()+"正在生产数据"+data);
bufferData.offer(data);
consumerCondition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
public synchronized T pull(){
T data = null;
try {
lock.lock();
while(bufferData.size() == 0){
System.out.println("缓冲区无数据,请等待生产者生产数据");
consumerCondition.await();
}
data = bufferData.poll();
System.out.println("消费者"+Thread.currentThread().getName()+"拿到数据:"+data);
producerCondition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
return data;
}
}