今天做了一个笔试题,使用数组实现一个队列。现在记录一下当时的实现,之后再对这个实现进行改造升级。
/**
* @program: read-write
* @description: 数组实现队列
* @author: WangJie
* @create: 2019-03-03 21:45
**/
public class MyQueue<T> {
private Object[] queue;
private int in;
private int out;
private int size;
private int length;
MyQueue(int size) {
queue = new Object[size];
in = out = length = 0;
this.size = size;
}
public boolean offerLast(T obj) {
if (in == out && queue[in] != null) {
return false;
}
queue[in] = obj;
in = (in + 1) % size;
return true;
}
public T pollFirst() {
T target = get(out);
queue[out] = null;
if (in == out && target == null) {
throw new RuntimeException("当前队列无元素");
}
out = (out+1)%size;
return target;
}
private T get(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException();
}
return (T) queue[index];
}
public static void main(String[] args) {
MyQueue<String> queue = new MyQueue<>(3);
queue.offerLast("a");
queue.offerLast("b");
queue.offerLast("c");
queue.offerLast("d");
System.out.println(queue.pollFirst());
System.out.println(queue.pollFirst());
System.out.println(queue.pollFirst());
System.out.println(queue.pollFirst());
}
}
仔细想想,上面代码有些问题。一是没有加锁同步,那么这个队列是无法在多线程环境下使用的,二是入队会返回true或false,而出队时如果队列已经为空,则会抛出异常(当然也可以返回null),出队入队都需要使用该队列的客户端去检查结果,并作出相应的处理,当队列已满,客户端可能会使用忙等待,循环入队直到入队成功,出队也是如此,这会消耗大量CPU时间。所以,应该由队列提供出队入队的阻塞功能。
修改后的阻塞队列代码如下:
public synchronized boolean offerLast(T obj) throws InterruptedException {
while (in == out && queue[in] != null) {
wait();
}
queue[in] = obj;
in = (in + 1) % size;
notifyAll();
return true;
}
public synchronized T pollFirst() throws InterruptedException {
while (in == out && target == null) {
wait();
}
T target = get(out);
queue[out] = null;
out = (out+1)%size;
notifyAll();
return target;
}
以下是用来测试阻塞队列的代码:
@Test
public void blockingQueueTest() throws InterruptedException {
MyBlockingArrayQueue<Integer> queue = new MyBlockingArrayQueue<>(5);
Runnable provider = new Runnable() {
private int a =0;
@Override
public void run() {
while (true) {
try {
queue.offerLast(a);
System.out.println("生产者添加了:"+a);
a++;
} catch (InterruptedException e) {
System.out.println("生产终止");
break;
}
}
}
};
Runnable consumer1 =()->{
while (true){
try {
Integer number = queue.pollFirst();
System.out.println("1号消费者消费:"+number);
Thread.sleep(1000L);
} catch (InterruptedException e) {
System.out.println("1号消费者终止");
break;
}
}
};
Runnable consumer2 =()->{
while (true){
try {
Integer number = queue.pollFirst();
System.out.println("2号消费者消费:"+number);
Thread.sleep(1000L);
} catch (InterruptedException e) {
System.out.println("2号消费者终止");
break;
}
}
};
ExecutorService executorService = Executors.newCachedThreadPool();
Future<?> providerFuture = executorService.submit(provider);
Future<?> consumer1Future = executorService.submit(consumer1);
Future<?> consumer2Future = executorService.submit(consumer2);
Thread.sleep(20000L);
providerFuture.cancel(true);
consumer1Future.cancel(true);
consumer2Future.cancel(true);
}
以上代码还是有些可以改进的地方,notifyAll会唤醒所有在该阻塞队列上等待的消费者和生产者,而当他们醒来时可能会发现自己需要的条件并不满足,比如对于一个已满队列,消费者在消费一个后调用notifyAll,本意是提醒某个生产者可以再生产一个了,但是他提醒了所有的生产者和消费者,只有一个生产者会满足条件进行生产,而其他生产者和所有消费者醒来后都重新进入等待。这一过程中会发生锁争用,以及线程上下文的切换,占用CPU时间。如果将notifyAll改成notify。
使用wait和notifyAll带来问题的主要原因是所有的线程都会在这这一个条件队列上等待,这就导致消费者要想将可以继续生产了的信号传递给生产者,那么他必须notifyAll,如果使用notify,那么可能会被另一个消费者线程收到,其他线程就收不到了,之后就造成了生产者消费者的全部阻塞。所以使用多个条件队列,让线程在各自的条件上等待,那么就不需要唤醒所有线程。这一实现可以通过Lock的Condition来实现。
private ReentrantLock lock = new ReentrantLock();
private Condition inCondition = lock.newCondition();
private Condition outCondition = lock.newCondition();
public boolean offerLast(T obj) throws InterruptedException {
try {
lock.lock();
while (in == out && queue[in] != null) {
inCondition.await();
}
queue[in] = obj;
in = (in + 1) % size;
outCondition.signal();
return true;
}finally {
lock.unlock();
}
}
public T pollFirst() throws InterruptedException {
try {
lock.lock();
while (in == out && get(out) == null) {
outCondition.await();
}
T target = get(out);
queue[out] = null;
out = (out + 1) % size;
inCondition.signal();
return target;
}finally {
lock.unlock();
}
}