之前看队列,都是停留在看和使用的阶段。再次看队列的时候,忽然发现并没有深入到底层。比如:阻塞队列时如何阻塞的呢?是监听,还是等待呢?然后看着看着就看到了Lock和ReentrantLock,为什么不使用synchronized呢?为什么使用Condition,Condition是什么呢?wait,notify,notifyAll和await,signal,signalAll有什么区别呢?
能不能自己写一个阻塞队列呢?
有时候是需要动手写写的,写着写着发现已经理解了,所以这里就写一下吧。
1.第一步,了解队列容器
并发容器中有两个比较常用的队列容器,分别是ArrayBlockingQueue和LinkedBlockingQueue,
ArrayBlockingQueue内部是使用数组实现了,LinkedBlockingQueue内部是使用链表实现的。
数组和链表熟不熟悉呢?如果不熟悉,可不可以直接使用已经有的集合类实现呢?
所以本次尝试着使用ArrayList作为队列容器。
2.第二步,了解队列提供的方法
一个队列,起码具有两个方法:存和取。
所以这里就定义两个方法,add()往队列中存数据,take()从队列中取数据。
3.第三步,阻塞功能
take()方法应该具有阻塞功能,在没有数据的时候处于等待状态,在有数据的时候立刻可以从队列中取出数据。
4.第四步,如何阻塞
可不可以设计成wait()和notify()呢?在take()方法如果取不出数据的时候调用wait()进行阻塞,在add()方法存数据的时候,调用notify()方法,唤醒wait()。嘿,貌似可以哈。
5.第五步,synchronized和ReentrantLock
使用wait()和notify()方法貌似有些问题呢?
wait()和notify()需要和synchronized配合使用,wait()阻塞会释放锁,但是notify()却不会释放锁。如果有一个生产者一直占用着存方法,即使唤醒了wait()线程,那wait()线程也取不出数据,只有等到存方法执行完成,释放了锁,wait()才可以取数据。这个是有问题的,所以选择得ReentrantLock。
6.第六步,ReentrantLock的Condition
wait()和notify()都是数据Object对象的方法,如果不能用,那该怎么监听呢?这时候需要用到Condition对象,可以通过Condition唤醒指定的某个线程。
7.第七步,开始写自己的阻塞队列
/**
* 仿照阻塞队列,实现自己的阻塞队列。
* 中间会使用到Lock接口,ReentrantLock,Condition
*/
public class SimpleBlockingQueue {
//队列容器
private List<Integer> container = new ArrayList<Integer>();
//用于记录容器中元素的个数
private int count;
//申明锁
private Lock lock = new ReentrantLock();
//标识,可以表示具体的线程
private final Condition conditionNull = lock.newCondition();
private final Condition conditionFull = lock.newCondition();
/**
* 将数据加入到队列中
* @param item
* @throws InterruptedException
*/
public void add(Integer item) throws InterruptedException {
if(item == null){
throw new NullPointerException();
}
//申明可中断锁,简单起见也可以直接使用lock.lock(),lock.tryLock()
lock.lockInterruptibly();
try{
System.out.println("添加元素:"+item);
++count;
container.add(item);
System.out.println("唤醒阻塞线程...");
//添加成功之后,调用signal()发出唤醒通知
//注意这里的唤醒一定要在unlock()之前
conditionNull.signal();
Thread.sleep(2000);
}finally {
System.out.println("添加方法释放锁...");
//手动释放锁
lock.unlock();
}
}
/**
* 从队列中后去数据
* @return
* @throws InterruptedException
*/
public Integer take() throws InterruptedException {
lock.lockInterruptibly();
try {
try {
while (count == 0){
System.out.println("队列元素为空,进入阻塞...");
conditionNull.await();
}
} catch (InterruptedException ie) {
System.out.println("出现异常,唤醒阻塞线程conditionNull");
conditionNull.signal();
throw ie;
}
--count;
Integer x = container.get(0);
System.out.println("取出方法取出元素:"+x);
conditionFull.signal();
return x;
} finally {
System.out.println("取出方法释放锁...");
lock.unlock();
}
}
}
8.第八步,测试
public static void main(String[] args) {
SimpleBlockingQueue sbq = new SimpleBlockingQueue();
Thread t1 = new Thread(){
public void run() {
try {
sbq.add(1);
Thread.sleep(1000);
sbq.add(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t2 = new Thread(){
public void run() {
try {
while (true){
Integer item = sbq.take();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
}
执行结果:
队列元素为空,进入阻塞...
添加元素:1
唤醒阻塞线程...
添加方法释放锁...
取出方法取出元素:1
取出方法释放锁...
队列元素为空,进入阻塞...
添加元素:2
唤醒阻塞线程...
添加方法释放锁...
取出方法取出元素:1
取出方法释放锁...
队列元素为空,进入阻塞...
到此,一个简单的阻塞队列就完成了。
这里只是简单的实现了队列的存取和阻塞唤醒,很多的细节问题并没有考虑。所以生产环境慎用,仅作为理解阻塞队列原理的小练习。