自己动手实现一个阻塞队列--ReentrantLock使用小结

背景

  • 前几天看到一道面试题:实现一个阻塞队列,就萌生了动手操作一把的想法。看着挺简单的,思路也和清晰,就是用ReentantLock和Condition来实现,但在实际操作过程中还是遇到了问题,总结一下,仅供参考。

阻塞队列第一版

  • 先附上第一版的代码。内部存储,为了方便就使用LinkedList来实现了。
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * NOTE:
 *
 * @author lizhiyang
 * @Date 2019-11-27 19:38
 */
public class MyBlockingQueue {
    private LinkedList list = new LinkedList<>();

    private ReentrantLock lock = new ReentrantLock();
    private Condition notFullCondition = lock.newCondition();
    private Condition notEmptyCondition = lock.newCondition();

    private int maxSize = 0;

    public MyBlockingQueue(int maxSize) {
        this.maxSize = maxSize;
    }

    public void push(E e) throws InterruptedException {
        lock.lock();
        try {
            if(list.size() >= maxSize) {
                notFullCondition.await();
            }
            list.add(e);
            notEmptyCondition.signalAll();
        } finally {
            lock.unlock();
        }

    }

    public E pop() throws InterruptedException {
        lock.lock();
        try {
            if(list.size() == 0) {
                notEmptyCondition.await();
            }
            E e = list.pop();
            notFullCondition.signalAll();
            return e;
        } finally {
            lock.unlock();
        }

    }
}

第一版遇到的问题

  • 初始测试的时候,使用一个线程循环放数据,另一个线程循环取数据,一放一取有序进行,没有什么问题。
  • 接着增加消费线程,两个线程取数据,此时一个消费线程执行时抛出了异常,在取数据的时候,因为LinkedList里已经没有元素了。很明显的并发问题。但是疑问就来了,pop方法入口明明用了lock.lock()方法,已经加锁了,怎么还会有并发问题了
java.util.NoSuchElementException
	at java.util.LinkedList.removeFirst(LinkedList.java:270)
	at java.util.LinkedList.pop(LinkedList.java:801)
	at com.lizhy.test.MyBlockingQueue.pop(MyBlockingQueue.java:63)
	at com.lizhy.test.ThreadTest.lambda$blockingQueue$6(ThreadTest.java:94)
	at java.lang.Thread.run(Thread.java:748)
  • 为了排查这个疑问,在加锁前后和释放锁的地方加了一些日志,打印lock的状态,从日志来看,两个线程都加锁成功了,并且也没有释放锁的日志
lock afterjava.util.concurrent.locks.ReentrantLock@771ae0c1[Locked by thread t1]
lock afterjava.util.concurrent.locks.ReentrantLock@771ae0c1[Locked by thread t3]
  • 那么就很奇怪了,为什么两个线程都会加锁成功呢。排查代码,感觉可能出问题的就是在condition.await方法了。为了确定问题所在,把这个代码注释,重新运行,这个时候就只有一个线程加锁成功了。

condion.await()解惑

  • 翻看了Consition.await()API,有这么一段话

Causes the current thread to wait until it is signalled or interrupted.
The lock associated with this Condition is atomically released and the current thread becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:

  • Some other thread invokes the signal method for this Condition and the current thread happens to be chosen as the thread to be awakened; or
  • Some other thread invokes the signalAll method for this Condition; or
  • Some other thread interrupts the current thread, and interruption of thread suspension is supported; or
  • A “spurious wakeup” occurs.

In all cases, before this method can return the current thread must re-acquire the lock associated with this condition. When the thread returns it is guaranteed to hold this lock.

  • 大概的意思就是说:调用这个方法,当前线程会被阻塞,直到有人唤醒或者被打断,并且Condition关联的锁会被自动释放。有四种情况能够唤醒线程。在所有的4中情况中,线程被唤醒,方法返回之前,都必须重新获取关联Condition的锁。必须确定方法返回时,当前线程已经重新获取到了锁。
  • 看了以上的说明,也就明了了。当线程一调用lock.lock()获取了锁,调用Condition.await()方法时,释放锁,线程阻塞,此时,线程二也就可以正常获取锁了

NoSuchElement的错误又是怎么来的呢

  • 通过以上分析,我们知道调用Condition.await()方法会释放锁,但如果线程被唤醒肯定是因为list中有数据了,可以取了,那怎么又报这个错误了呢?
  • 继续分析代码,await被唤醒肯定是因为有数据了,并且也获取到了锁,那怎么后边取的时候又没有了呢?唤醒的时候用的是signalAll,会唤醒所有的线程,但是只有一个线程能获取到锁,其他线程会持续尝试获取锁,当第一个线程获取锁之后,执行了代码,最后释放锁,此时,被唤醒的其他线程就又能够获取锁了,注意:此时队列里的数据已经空了,因此就出现了NoSuchElement的错误
  • 那要怎么解决呢?
    方法一:用signal,只唤醒一个线程,因为是阻塞队列,每次也只能有一个线程执行。
    方法二:await之后,再次判断一下队列是否为空就行了,把if改成while即可

阻塞队列第二版

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * NOTE:
 *
 * @author lizhiyang
 * @Date 2019-11-27 19:38
 */
public class MyBlockingQueue {
    private LinkedList list = new LinkedList<>();

    private ReentrantLock lock = new ReentrantLock();
    private Condition notFullCondition = lock.newCondition();
    private Condition notEmptyCondition = lock.newCondition();

    private int maxSize = 0;

    public MyBlockingQueue(int maxSize) {
        this.maxSize = maxSize;
    }

    public void push(E e) throws InterruptedException {
        lock.lock();
        try {
            while(list.size() >= maxSize) {
                notFullCondition.await();
            }
            list.add(e);
            notEmptyCondition.signal();
        } finally {
            lock.unlock();
        }

    }

    public E pop() throws InterruptedException {
        lock.lock();
        try {
            while(list.size() == 0) {
                notEmptyCondition.await();
            }
            E e = list.pop();
            notFullCondition.signal();
            return e;
        } finally {
            lock.unlock();
        }

    }
}
  • 本次实现阻塞队列遇到的问题,究其原因还是因为对await和signal、signalAll方法的含义理解不清楚导致的。以后还得继续加深,不能想当然,一定得动手实践。

你可能感兴趣的:(Java知识总结)