有的同学会很疑惑,为什么要自己封装一个阻塞队列呢,java不是有提供现成的阻塞队列吗。当然,如果我们仅仅是使用,完全用java提供的就可以了。之所以封装一个,只是为了更深刻的了解线程同步和锁的使用。也是为了记录一下自己犯的错误。
假如我们的目的是:使用LinkedList实现一个类似于阻塞队列的功能,put添加,超出最大值阻塞,take取出,没有时阻塞。
我最初的想法是使用synchronize来做,下面是我第一次实现的代码:
public class LinkedListWrapper<E> {
private Object lock = new Object();
private final int max = 10;
private int num = 0;
private LinkedList<E> list = new LinkedList<>();
public void put(E e) {
synchronized (lock) {
while (num == max) {
lock.notifyAll();
System.out.println("max");
try {
lock.wait();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
list.addLast(e);
num++;
lock.notifyAll();
System.out.println("notmax");
}
}
public E take() {
synchronized (lock) {
while (num == 0) {
lock.notifyAll();
System.out.println("take num==0");
try {
lock.wait();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
E e = list.removeFirst();
num--;
lock.notifyAll();
System.out.println("take num > 0");
return e;
}
}
}
测试代码是这样子的:
LinkedListWrapper l = new ...;
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
l.put("---->" + i);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("===>"+l.take());
}
}
}).start();
一个线程往队列中放,一个从队列中取。运行一下发现挺好,队列满了阻塞了,取时如果队列为空也会阻塞。到这种程度我就认为是很完美的实现了目标,并没有更深入的思考这种实现方式的问题,直到后来。。。测试代码变成了这样:
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
i++;
l.put("--1-->" + i);
System.out.println("put1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"1").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
i++;
l.put("--2-->" + i);
System.out.println("put2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"2").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("==1=>" + l.take());
}
}
},"3").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("==2=>" + l.take());
}
}
},"4").start();
和上面的测试代码区别就是线程变多了,2个放的,2个取的。
这时候发现问题了:take方法的take num==0一直在输出。为什么呢?因为第三个线程take的时候发现队列为空,就会输出这句话,然后notifyAll,接着自己就wait了。而notifyAll唤醒了线程4,这时候队列可能依然为空,线程4又去notifyAll了线程3,接着线程4wait了。也就是说线程3和4相互唤醒着玩了。。。即使队列为空。。。至于我是为啥发现了以前自己犯的错误,大家就忽略吧。。。
这种情况下该怎么正确的实现阻塞队列呢?其实大家可以看看java的阻塞队列怎么实现的。简单来说就是借助Condition来做的。下面一起来看一个简单版的Condition实现的阻塞队列吧:
public class LinkedListWrapper2<E> {
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
private final int max = 10;
private int num = 0;
private LinkedList<E> list = new LinkedList<>();
public void put(E e) {
lock.lock();
try {
while (num == max) {
try {
notFull.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
list.addLast(e);
num++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public E take() {
lock.lock();
try {
while (num == 0) {
try {
notEmpty.await();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
E e = list.removeFirst();
num--;
notFull.signal();
return e;
} finally {
lock.unlock();
}
}
}
上面这种实现方式是一种比较正确的实现方式,而且java的LinkedBlockingQueue
就是这样实现的。大家可以参考java的实现方式具体学习一下Condition的使用。
第二种实现方式无论多少个线程测试,都是没有问题的。至于Condition的实现原理,还没有深入研究,这里就不做讨论了。。。
关于Condition和synchronize的使用,这也算是吃一堑长一智了。。。